diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 1c63b78dc..a73d8ae20 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -16,8 +16,6 @@ name: "Linting" on: pull_request: branches: - - fast-dev - - fast-dev-gke - master tags: - ci @@ -71,4 +69,7 @@ jobs: - name: Check python formatting id: yapf run: | - yapf --style="{based_on_style: google, indent_width: 2, SPLIT_BEFORE_NAMED_ASSIGNS: false}" -p -d tools/*.py + yapf --style="{based_on_style: google, indent_width: 2, SPLIT_BEFORE_NAMED_ASSIGNS: false}" -p -d \ + tools/*.py \ + blueprints/cloud-operations/network-dashboard/src/*py \ + blueprints/cloud-operations/network-dashboard/src/plugins/*py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3fc3fe56f..3e452275b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,12 +14,10 @@ name: "Tests" on: - schedule: - - cron: "45 2 * * *" + # schedule: + # - cron: "45 2 * * *" pull_request: branches: - - fast-dev - - fast-dev-gke - master tags: - ci diff --git a/.gitignore b/.gitignore index a2d19a2c5..91778178c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,13 +21,13 @@ bundle.zip **/*.pkrvars.hcl fixture_* fast/configs -fast/stages/**/[0-9]*providers.tf -fast/stages/**/terraform.tfvars -fast/stages/**/terraform.tfvars.json -fast/stages/**/terraform-*.auto.tfvars.json -fast/stages/**/0*.auto.tfvars* +fast/**/[0-9]*providers.tf +fast/**/terraform.tfvars +fast/**/terraform.tfvars.json +fast/**/terraform-*.auto.tfvars.json +fast/**/[0-9]*.auto.tfvars* **/node_modules -fast/stages/**/globals.auto.tfvars.json +fast/**/globals.auto.tfvars.json cloud_sql_proxy examples/cloud-operations/binauthz/tenant-setup.yaml examples/cloud-operations/binauthz/app/app.yaml @@ -37,8 +37,15 @@ examples/cloud-operations/adfs/ansible/gssh.sh examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/vars.yaml examples/cloud-operations/multi-cluster-mesh-gke-fleet-api/ansible/gssh.sh blueprints/cloud-operations/network-dashboard/cloud-function.zip -blueprints/cloud-operations/apigee/bundle-export.zip -blueprints/cloud-operations/apigee/bundle-gcs2bq.zip -blueprints/cloud-operations/apigee/apiproxy.zip -blueprints/cloud-operations/apigee/create-datastore.sh -blueprints/cloud-operations/apigee/deploy-apiproxy.sh +blueprints/apigee/bigquery-analytics/bundle-export.zip +blueprints/apigee/bigquery-analytics/bundle-gcs2bq.zip +blueprints/apigee/bigquery-analytics/apiproxy.zip +blueprints/apigee/bigquery-analytics/create-datastore.sh +blueprints/apigee/bigquery-analytics/deploy-apiproxy.sh +blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/bundle/apiproxy/targets/default.xml +blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/bundle.zip +blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/deploy-apiproxy.sh +blueprints/apigee/hybrid-gke/apiproxy.zip +blueprints/apigee/hybrid-gke/deploy-apiproxy.sh +blueprints/apigee/hybrid-gke/ansible/gssh.sh +blueprints/apigee/hybrid-gke/ansible/vars/vars.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 75497c848..4d6ab8295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,116 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - + + +### DOCUMENTATION + +- [[#1052](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1052)] **incompatible change:** FAST multitenant bootstrap and resource management, rename org-level FAST stages ([ludoo](https://github.com/ludoo)) + +### FAST + +- [[#1052](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1052)] **incompatible change:** FAST multitenant bootstrap and resource management, rename org-level FAST stages ([ludoo](https://github.com/ludoo)) + +### MODULES + +- [[#1052](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1052)] **incompatible change:** FAST multitenant bootstrap and resource management, rename org-level FAST stages ([ludoo](https://github.com/ludoo)) + +### TOOLS + +- [[#1052](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1052)] **incompatible change:** FAST multitenant bootstrap and resource management, rename org-level FAST stages ([ludoo](https://github.com/ludoo)) + +## [20.0.0] - 2023-02-04 + ### BLUEPRINTS +- [[#1038](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1038)] Vertex Pipelines MLOps framework blueprint ([javiergp](https://github.com/javiergp)) +- [[#1124](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1124)] Removed unused file package-lock.json ([apichick](https://github.com/apichick)) +- [[#1119](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1119)] **incompatible change:** Multi-Cluster Ingress gateway api config ([wiktorn](https://github.com/wiktorn)) +- [[#1111](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1111)] **incompatible change:** In the apigee module now both the /22 and /28 peering IP ranges are p… ([apichick](https://github.com/apichick)) +- [[#1106](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1106)] Network Dashboard: PSA support for Filestore and Memorystore ([aurelienlegrand](https://github.com/aurelienlegrand)) +- [[#1110](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1110)] Bump cookiejar from 2.1.3 to 2.1.4 in /blueprints/apigee/bigquery-analytics/functions/export ([dependabot[bot]](https://github.com/dependabot[bot])) +- [[#1097](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1097)] Use terraform resource to activate Anthos Service Mesh ([wiktorn](https://github.com/wiktorn)) +- [[#1104](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1104)] Updated apigee hybrid for gke README ([apichick](https://github.com/apichick)) +- [[#1107](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1107)] Check linting for Python dashboard files ([ludoo](https://github.com/ludoo)) +- [[#1102](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1102)] Improvements in apigee hybrid-gke: now using workload identity and GLB ([apichick](https://github.com/apichick)) +- [[#1098](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1098)] Add shared-vpc support on data-playground blueprint ([lcaggio](https://github.com/lcaggio)) +- [[#1095](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1095)] [Data Platform] Fix Table in readme ([lcaggio](https://github.com/lcaggio)) +- [[#1089](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1089)] Update Data Platform ([lcaggio](https://github.com/lcaggio)) +- [[#1081](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1081)] Apigee hybrid on GKE ([apichick](https://github.com/apichick)) +- [[#1082](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1082)] Fixes in Apigee Bigquery Analytics blueprint ([apichick](https://github.com/apichick)) +- [[#1071](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1071)] Moved apigee bigquery analytics blueprint, added apigee network patterns ([apichick](https://github.com/apichick)) +- [[#1073](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1073)] Allow setting no ranges in firewall module custom rules ([ludoo](https://github.com/ludoo)) +- [[#1072](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1072)] **incompatible change:** Add gc_policy to Bigtable module, bump provider versions to 4.47 ([iht](https://github.com/iht)) +- [[#1063](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1063)] Network dashboard: PSA ranges support, starting with Cloud SQL ([aurelienlegrand](https://github.com/aurelienlegrand)) +- [[#1062](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1062)] Fixes for GKE ([wiktorn](https://github.com/wiktorn)) +- [[#1060](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1060)] Update src/README.md for Network Dashboard ([aurelienlegrand](https://github.com/aurelienlegrand)) +- [[#1020](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1020)] Networking dashboard and discovery tool refactor ([ludoo](https://github.com/ludoo)) + +### DOCUMENTATION + +- [[#1101](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1101)] First batch of testing updates to core modules ([juliocc](https://github.com/juliocc)) +- [[#1089](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1089)] Update Data Platform ([lcaggio](https://github.com/lcaggio)) +- [[#1084](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1084)] Fixes in Apigee blueprints README files ([apichick](https://github.com/apichick)) +- [[#1081](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1081)] Apigee hybrid on GKE ([apichick](https://github.com/apichick)) +- [[#1074](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1074)] Adding new section for Authentication issues ([agutta](https://github.com/agutta)) +- [[#1071](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1071)] Moved apigee bigquery analytics blueprint, added apigee network patterns ([apichick](https://github.com/apichick)) +- [[#1057](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1057)] Adding new file FAQ and an image ([agutta](https://github.com/agutta)) + +### FAST + +- [[#1118](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1118)] Add missing logging admin role for initial user ([ludoo](https://github.com/ludoo)) +- [[#1099](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1099)] Fix destroy in stage 1 outputs ([ludoo](https://github.com/ludoo)) +- [[#1089](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1089)] Update Data Platform ([lcaggio](https://github.com/lcaggio)) +- [[#1085](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1085)] fix restricted services not being added to the perimeter configurations ([drebes](https://github.com/drebes)) +- [[#1057](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1057)] Adding new file FAQ and an image ([agutta](https://github.com/agutta)) +- [[#1054](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1054)] FAST: fix typo in bootstrap stage README ([agutta](https://github.com/agutta)) +- [[#1051](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1051)] FAST: add instructions for billing export to stage 0 README ([KPRepos](https://github.com/KPRepos)) + +### MODULES + +- [[#1127](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1127)] Skip node config for autopilot ([ludoo](https://github.com/ludoo)) +- [[#1125](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1125)] Added mesh_certificates setting in GKE cluster ([rosmo](https://github.com/rosmo)) +- [[#1094](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1094)] Added GLB example with MIG as backend ([eliamaldini](https://github.com/eliamaldini)) +- [[#1119](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1119)] **incompatible change:** Multi-Cluster Ingress gateway api config ([wiktorn](https://github.com/wiktorn)) +- [[#1111](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1111)] **incompatible change:** In the apigee module now both the /22 and /28 peering IP ranges are p… ([apichick](https://github.com/apichick)) +- [[#1116](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1116)] Include cloudbuild API in project module ([aymanfarhat](https://github.com/aymanfarhat)) +- [[#1115](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1115)] add new parameters support in apigee module ([blackillzone](https://github.com/blackillzone)) +- [[#1112](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1112)] Add HTTPS frontend with SNEG example ([juliodiez](https://github.com/juliodiez)) +- [[#1097](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1097)] Use terraform resource to activate Anthos Service Mesh ([wiktorn](https://github.com/wiktorn)) +- [[#1101](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1101)] First batch of testing updates to core modules ([juliocc](https://github.com/juliocc)) +- [[#1098](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1098)] Add shared-vpc support on data-playground blueprint ([lcaggio](https://github.com/lcaggio)) +- [[#1096](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1096)] [VPC-SC] Add support for scoped Policies ([lcaggio](https://github.com/lcaggio)) +- [[#1093](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1093)] Added tags to gke-cluster module ([apichick](https://github.com/apichick)) +- [[#1078](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1078)] Fixed delete_rule in compute-mig module for stateful disks ([rosmo](https://github.com/rosmo)) +- [[#1080](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1080)] Added device_name field to compute-vm attached_disks parameter ([rosmo](https://github.com/rosmo)) +- [[#1079](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1079)] Reorder org policy rules ([juliocc](https://github.com/juliocc)) +- [[#1075](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1075)] **incompatible change:** Add cluster replicas to Bigtable module. ([iht](https://github.com/iht)) +- [[#1073](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1073)] Allow setting no ranges in firewall module custom rules ([ludoo](https://github.com/ludoo)) +- [[#1072](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1072)] **incompatible change:** Add gc_policy to Bigtable module, bump provider versions to 4.47 ([iht](https://github.com/iht)) +- [[#1070](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1070)] Fix MIG health check variable ([ludoo](https://github.com/ludoo)) +- [[#1069](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1069)] Allow tables with several column families in Bigtable ([iht](https://github.com/iht)) +- [[#1068](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1068)] Added endpoint_attachment_hosts output to apigee module ([apichick](https://github.com/apichick)) +- [[#1067](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1067)] Corrected load balancing scheme in backend service ([apichick](https://github.com/apichick)) +- [[#1066](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1066)] Refactor GCS module and tests for Terraform 1.3 ([ludoo](https://github.com/ludoo)) +- [[#1062](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1062)] Fixes for GKE ([wiktorn](https://github.com/wiktorn)) +- [[#1061](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1061)] **incompatible change:** Allow using dynamically generated address in LB modules NEGs ([ludoo](https://github.com/ludoo)) +- [[#1059](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1059)] Read ranges from correct fields in firewall factory ([juliocc](https://github.com/juliocc)) +- [[#1056](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1056)] Feature - CloudSQL pre-allocation private IP range and GKE Cluster ignore_change lifecycle hook. ([itsavvy-ankur](https://github.com/itsavvy-ankur)) + +### TOOLS + +- [[#1107](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1107)] Check linting for Python dashboard files ([ludoo](https://github.com/ludoo)) +- [[#1101](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1101)] First batch of testing updates to core modules ([juliocc](https://github.com/juliocc)) +- [[#1091](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1091)] Fix check_documentation output ([juliocc](https://github.com/juliocc)) +- [[#1053](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1053)] Extend inventory-based testing to examples ([juliocc](https://github.com/juliocc)) + +## [19.0.0] - 2022-12-13 + + +### BLUEPRINTS + +- [[#1045](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1045)] Assorted module fixes ([ludoo](https://github.com/ludoo)) - [[#1044](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1044)] **incompatible change:** Refactor net-glb module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) - [[#982](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/982)] Adding Secondary IP Utilization calculation ([brianhmj](https://github.com/brianhmj)) - [[#1037](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1037)] Bump qs and formidable in /blueprints/cloud-operations/apigee/functions/export ([dependabot[bot]](https://github.com/dependabot[bot])) @@ -69,6 +175,8 @@ All notable changes to this project will be documented in this file. ### DOCUMENTATION +- [[#1048](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1048)] Document new testing approach ([ludoo](https://github.com/ludoo)) +- [[#1045](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1045)] Assorted module fixes ([ludoo](https://github.com/ludoo)) - [[#1014](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1014)] Update typos in `net-vpc-firewall` README.md ([aymanfarhat](https://github.com/aymanfarhat)) - [[#1044](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1044)] **incompatible change:** Refactor net-glb module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) - [[#1009](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1009)] Fix encryption in Data Playground blueprint ([lcaggio](https://github.com/lcaggio)) @@ -123,6 +231,8 @@ All notable changes to this project will be documented in this file. ### MODULES +- [[#1049](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1049)] Add ssl certs to cloudsql instance ([prabhaarya](https://github.com/prabhaarya)) +- [[#1045](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1045)] Assorted module fixes ([ludoo](https://github.com/ludoo)) - [[#1040](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1040)] Fix name in google_pubsub_schema resource ([VictorCavalcanteLG](https://github.com/VictorCavalcanteLG)) - [[#1043](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1043)] added reverse lookup feature to module dns #1042 ([chemapolo](https://github.com/chemapolo)) - [[#1044](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1044)] **incompatible change:** Refactor net-glb module for Terraform 1.3 ([ludoo](https://github.com/ludoo)) @@ -211,6 +321,7 @@ All notable changes to this project will be documented in this file. ### TOOLS +- [[#1048](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1048)] Document new testing approach ([ludoo](https://github.com/ludoo)) - [[#1029](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1029)] Testing framework revamp ([juliocc](https://github.com/juliocc)) - [[#1022](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1022)] Replace `set-output` with env variable and remove single quotes on labels ([kunzese](https://github.com/kunzese)) - [[#1021](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1021)] Add OpenContainers annotations to published container images ([kunzese](https://github.com/kunzese)) @@ -387,7 +498,7 @@ All notable changes to this project will be documented in this file. - fix `tag` output on `data-catalog-policy-tag` module - add shared-vpc support on `gcs-to-bq-with-least-privileges` - new `net-ilb-l7` module -- new [02-networking-peering](fast/stages/02-networking-peering) networking stage +- new `02-networking-peering` networking stage - **incompatible change** the variable for PSA ranges in networking stages have changed ## [14.0.0] - 2022-02-25 @@ -406,8 +517,8 @@ All notable changes to this project will be documented in this file. - **incompatible change** removed `ingress_settings` configuration option in the `cloud-functions` module. - new [m4ce VM example](blueprints/cloud-operations/vm-migration/) - Support for resource management tags in the `organization`, `folder`, `project`, `compute-vm`, and `kms` modules -- new [data platform](fast/stages/03-data-platform) stage 3 -- new [02-networking-nva](fast/stages/02-networking-nva) networking stage +- new `data platform` stage 3 +- new `02-networking-nva` networking stage - allow customizing the names of custom roles - added `environment` and `context` resource management tags - use resource management tags to restrict scope of roles/orgpolicy.policyAdmin @@ -842,7 +953,9 @@ 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/v18.0.0...HEAD +[Unreleased]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v20.0.0...HEAD +[20.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v19.0.0...v20.0.0 +[19.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v18.0.0...v19.0.0 [18.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v16.0.0...v18.0.0 [16.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v15.0.0...v16.0.0 [15.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v14.0.0...v15.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d82b1ac77..d34ad2d99 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,11 +72,13 @@ pytest tests/examples Once everything looks good, add/commit any pending changes then push and open a PR on GitHub. We typically enforce a set of design and style conventions, so please make sure you have familiarized yourself with the following sections and implemented them in your code, to avoid lengthy review cycles. HINT: if you work on high-latency or low-bandwidth network use `TF_PLUGIN_CACHE_DIR` environment variable to dramatically speed up the tests, for example: + ```bash TF_PLUGIN_CACHE_DIR=/tmp/tfcache pytest tests ``` Or just add into your [terraformrc](https://developer.hashicorp.com/terraform/cli/config/config-file): + ``` plugin_cache_dir = "$HOME/.terraform.d/plugin-cache" ``` @@ -544,6 +546,7 @@ locals { #### The `prefix` variable If you would like to use a "prefix" variable for resource names, please keep its definition consistent across all modules: + ```hcl # variables.tf variable "prefix" { @@ -563,6 +566,7 @@ locals { ``` For blueprints the prefix is mandatory: + ```hcl variable "prefix" { description = "Prefix used for resource names." @@ -608,6 +612,7 @@ The linting workflow tests: - that the correct copyright boilerplate is present in all files, using `tools/check_boilerplate.py` - that all Terraform code is linted via `terraform fmt` +- that Terraform variables and outputs are sorted alphabetically - that all README files have up to date outputs, variables, and files (where relevant) tables, via `tools/check_documentation.py` - that all links in README files are syntactically correct and valid if internal, via `tools/check_links.py` - that resource names used in FAST stages stay within a length limit, via `tools/check_names.py` @@ -639,90 +644,123 @@ The test workflow runs test suites in parallel. Refer to the next section for mo #### Using and writing tests -Our testing approach follows a simple philosophy: we mainly test to ensure code works, and it does not break due to changes to dependencies (modules) or provider resources. +Our testing approach follows a simple philosophy: we mainly test to ensure code works, and that it does not break due to changes to dependencies (modules) or provider resources. This makes testing very simple, as a successful `terraform plan` run in a test case is often enough. We only write more specialized tests when we need to check the output of complex transformations in `for` loops. -As our testing needs are very simple, we also wanted to reduce the friction required to write new tests as much as possible: our tests are written in Python, and use `pytest` which is the standard for the language. We adopted this approach instead of others (Inspec/Kitchen, Terratest) as it allows writing simple functions as test units using Python which is simple and widely known. +As our testing needs are very simple, we also wanted to reduce the friction required to write new tests as much as possible: our tests are written in Python and use `pytest` which is the standard for the language, leveraging our [`tftest`](https://pypi.org/project/tftest/) library, which wraps the Terraform executable and returns familiar data structures for most commands. -The last piece of our testing framework is our [`tftest`](https://pypi.org/project/tftest/) library, which wraps the Terraform executable and returns familiar data structures for most commands. +Writing `pytest` unit tests to check plan results is really easy, but since wrapping modules and examples in dedicated fixtures and hand-coding checks gets annoying after a while, we developed a thin layer that allows us to use `tfvars` files to run tests, and `yaml` results to check results. In some specific situations you might still want to interact directly with `tftest` via Python, if that's the case skip to the legacy approach below. -##### Testing end-to-end examples +##### Testing end-to-end examples via `tfvars` and `yaml` -Putting it all together, here is how an end-to-end blueprint test works. +Our new approach to testing requires you to: -Each example is a Python module in its own directory, and a Terraform fixture that calls the example as a module: +- create a folder in the right `tests` hierarchy where specific test files will be hosted +- define `tfvars` files each with a specific variable configuration to test +- define `yaml` "inventory" files with the plan and output results you want to test +- declare which of these files need to be run as tests in a `tftest.yaml` file + +Let's go through each step in succession, assuming you are testing the new `net-glb` module. + +First create a new folder under `tests/modules` replacing any dash in the module name with underscores. You also need to create an empty `__init__.py` file in it, since the folder represents a package from the point of view of `pytest`. Note that if you were testing a blueprint the folder would go in `tests/blueprints`. ```bash -tests/blueprints/cloud_operations/iam_delegated_role_grants/ -├── fixture -│   ├── main.tf -│   └── variables.tf -├── __init__.py -└── test_plan.py +mkdir tests/modules/net_glb +touch tests/modules/net_glb/__init__.py ``` -One point of note is that the folder contains a Python module, so any dash needs to be replaced with underscores to make it importable. The actual test in the `test_plan.py` file looks like this: - -```python -def test_resources(e2e_plan_runner): - "Test that plan works and the numbers of resources is as expected." - modules, resources = e2e_plan_runner() - assert len(modules) == 6 - assert len(resources) == 18 -``` - -It uses our pytest `e2e_plan_runner` fixture, which assumes a Terraform test setup is present in the `fixture` folder alongside the test file, runs `plan` on it, and returns the number of modules and resources. - -The Terraform fixture is a single block that runs the whole example as a module, and a handful of variables that can be used to test different configurations (not used above so they could be replaced with static strings). +Then define a `tfvars` file with one of the module configurations you want to test. If you have a lot of variables which are shared across different tests, you can group all the common variables in a single `tfvars` file and associate it with each test's specific `tfvars` file (check the [organization module test](./tests/modules/organization/tftest.yaml) for an example). ```hcl -module "test" { - source = "../../../../../blueprints/cloud-operations/asset-inventory-feed-remediation" - project_create = var.project_create - project_id = var.project_id +# file: tests/modules/net_glb/test-simple.tfvars +name = "glb-test-0" +project_id = "my-project" +backend_buckets_config = { + default = { + bucket_name = "my-bucket" + } } ``` -You can run this test as part of or entire suite of tests, the blueprints suite, or individually: +Next define the corresponding inventory `yaml` file which will be used to assert values from the plan that uses the `tfvars` file above. In the inventory file you have three sections available: + +- `values` is a map of resource indexes (the same ones used by Terraform state) and their attribute name and values; you can define just the attributes you are interested in and the other will be ignored +- `counts` is a map of resource types (eg `google_compute_engine`) and the number of times each type occurs in the plan; here too just define the ones the need checking +- `outputs` is a map of outputs and their values; where a value is unknown at plan time use the special `__missing__` token + +```yaml +# file: tests/modules/net_glb/test-simple.yaml +values: + google_compute_global_forwarding_rule.default: + description: Terraform managed. + load_balancing_scheme: EXTERNAL + google_compute_target_http_proxy.default[0]: + name: glb-test-1 +counts: + google_compute_backend_bucket: 1 + google_compute_global_forwarding_rule: 1 + google_compute_health_check: 1 + google_compute_target_http_proxy: 1 + google_compute_url_map: 1 +outputs: + address: __missing__ + backend_service_ids: __missing__ + forwarding_rule: __missing__ + group_ids: __missing__ + health_check_ids: __missing__ + neg_ids: __missing__ +``` + +Create as many pairs of `tfvars`/`yaml` files as you need to test every scenario and feature, then create the file that triggers our fixture and converts them into `pytest` tests. + +```yaml +# file: tests/modules/net_glb/tftest.yaml +module: modules/net-glb +# if there are variables shared among all tests you can define a common file +# common_tfvars: +# - defaults.tfvars +tests: + test-plan: + tfvars: + - test-plan.tfvars + - test-plan-extra.tfvars +``` + +A good example of tests showing different ways of leveraging our framework is in the [`tests/modules/organization`](./tests/modules/organization) folder. + +##### Writing tests in Python (legacy approach) + +Where possible, we recommend using the testing framework described in the previous section. However, if you need it, you can still write tests using Python directly. + +In general, you should try to use the `plan_summary` fixture, which runs a a terraform plan and returns a `PlanSummary` object. The most important arguments to `plan_summary` are: +- the path of the Terraform module you want to test, relative to the root of the repository +- a list of paths representing the tfvars file to pass in to terraform. These paths are relative to the python file defining the test. + +If successful, `plan_summary` will return a `PlanSummary` object with the `values`, `counts` and `outputs` attributes following the same semantics described in the previous section. You can use this fields to write your custom tests. + +Like before let's imagine we're writing a (python) test for `net-glb` module. First create a new folder under `tests/modules` replacing any dash in the module name with underscores. You also need to create an empty `__init__.py` file in it, to ensure `pytest` discovers you new tests automatically. ```bash -# run all tests -pytest -# only run example tests -pytest tests/blueprints -# only run this example tests -pytest tests/blueprints/cloud_operations/iam_delegated_role_grants/ -# only run a single unit -pytest tests/blueprints/cloud_operations/iam_delegated_role_grants/test_plan.py::test_resources +mkdir tests/modules/net_glb +touch tests/modules/net_glb/__init__.py ``` -##### Testing modules - -The same approach used above can also be used for testing modules when a simple plan is enough to validate code. When specific features need to be tested though, the `plan_runner` pytest fixture can be used so that plan resources are returned for inspection. - -The following example from the `project` module leverages variables in the Terraform fixture to define which module resources are returned from plan. - +Now create a file containing your tests, e.g. `test_plan.py`: ```python -def test_iam(plan_runner): - "Test IAM bindings." - iam = ( - '{"roles/owner" = ["user:one@example.org"],' - '"roles/viewer" = ["user:two@example.org", "user:three@example.org"]}' - ) - _, resources = plan_runner(iam=iam) - roles = dict((r['values']['role'], r['values']['members']) - for r in resources if r['type'] == 'google_project_iam_binding') - assert roles == { - 'roles/owner': ['user:one@example.org'], - 'roles/viewer': ['user:three@example.org', 'user:two@example.org']} +def test_name(plan_summary, tfvars_to_yaml, tmp_path): + s = plan_summary('modules/net-glb', tf_var_files=['test-plan.tfvars']) + address = 'google_compute_url_map.default' + assert s.values[address]['project'] == 'my-project' ``` +For more examples on how to write python tests, check the tests for the [`organization`](./tests/modules/organization/test_plan_org_policies.py) module. + #### Testing documentation examples -Most of our documentation examples are also tested via the `examples` test suite. To enable an example for testing just use the special `tftest` comment as the last line in the example, listing the number of modules and resources tested. +Most of our documentation examples are also tested via the `examples` test suite. To enable an example for testing just use the special `tftest` comment as the last line in the example, listing the number of modules and resources expected. -A few preset variables are available for use, as shown in this example from the `dns` module documentation. +A [few preset variables](./tests/examples/variables.tf) are available for use, as shown in this example from the `dns` module documentation. ```hcl module "private-dns" { @@ -739,6 +777,25 @@ module "private-dns" { # tftest modules=1 resources=2 ``` +Note that all HCL code examples in READMEs are automatically tested. To prevent this behavior, include `tftest skip` somewhere in the code. + +#### Running tests from a temporary directory + + Most of the time you can run tests using the `pytest` command as described in previous. However, the `plan_summary` fixture allows copying the root module and running the test from a temporary directory. + +To enable this option, just define the environment variable `TFTEST_COPY` and any tests using the `plan_summary` fixture will automatically run from a temporary directory. + +Running tests from temporary directories is useful if: +- you're running tests in parallel using `pytest-xdist`. In this case, just run you tests as follows: + ```bash + TFTEST_COPY=1 pytest -n 4 + ``` +- you're running tests for the `fast/` directory which contain tfvars and auto.tfvars files (which are read by terraform automatically) making your tests fail. In this case, you can run + ``` + TFTEST_COPY=1 pytest fast/ + ``` + + #### Fabric tools The main tool you will interact with in development is `tfdoc`, used to generate file, output and variable tables in README documents. diff --git a/blueprints/README.md b/blueprints/README.md index 49bf16ab7..60a84912d 100644 --- a/blueprints/README.md +++ b/blueprints/README.md @@ -4,11 +4,12 @@ This section provides **[networking blueprints](./networking/)** that implement Currently available blueprints: +- **apigee** - [Apigee Hybrid on GKE](./apigee/hybrid-gke/), [Apigee X analytics in BigQuery](./apigee/bigquery-analytics), [Apigee network patterns](./apigee/network-patterns/) - **cloud operations** - [Active Directory Federation Services](./cloud-operations/adfs), [Cloud Asset Inventory feeds for resource change tracking and remediation](./cloud-operations/asset-inventory-feed-remediation), [Fine-grained Cloud DNS IAM via Service Directory](./cloud-operations/dns-fine-grained-iam), [Cloud DNS & Shared VPC design](./cloud-operations/dns-shared-vpc), [Delegated Role Grants](./cloud-operations/iam-delegated-role-grants), [Networking Dashboard](./cloud-operations/network-dashboard), [Managing on-prem service account keys by uploading public keys](./cloud-operations/onprem-sa-key-management), [Compute Image builder with Hashicorp Packer](./cloud-operations/packer-image-builder), [Packer example](./cloud-operations/packer-image-builder/packer), [Compute Engine quota monitoring](./cloud-operations/quota-monitoring), [Scheduled Cloud Asset Inventory Export to Bigquery](./cloud-operations/scheduled-asset-inventory-export-bq), [Configuring workload identity federation for Terraform Cloud/Enterprise workflow](./cloud-operations/terraform-enterprise-wif), [TCP healthcheck and restart for unmanaged GCE instances](./cloud-operations/unmanaged-instances-healthcheck), [Migrate for Compute Engine (v5) blueprints](./cloud-operations/vm-migration), [Configuring workload identity federation to access Google Cloud resources from apps running on Azure](./cloud-operations/workload-identity-federation) -- **data solutions** - [GCE and GCS CMEK via centralized Cloud KMS](./data-solutions/cmek-via-centralized-kms), [Cloud Composer version 2 private instance, supporting Shared VPC and external CMEK key](./data-solutions/composer-2), [Cloud SQL instance with multi-region read replicas](./data-solutions/cloudsql-multiregion), [Data Platform](./data-solutions/data-platform-foundations), [Spinning up a foundation data pipeline on Google Cloud using Cloud Storage, Dataflow and BigQuery](./data-solutions/gcs-to-bq-with-least-privileges), [#SQL Server Always On Groups blueprint](./data-solutions/sqlserver-alwayson), [Data Playground](./data-solutions/data-playground) +- **data solutions** - [GCE and GCS CMEK via centralized Cloud KMS](./data-solutions/cmek-via-centralized-kms), [Cloud Composer version 2 private instance, supporting Shared VPC and external CMEK key](./data-solutions/composer-2), [Cloud SQL instance with multi-region read replicas](./data-solutions/cloudsql-multiregion), [Data Platform](./data-solutions/data-platform-foundations), [Spinning up a foundation data pipeline on Google Cloud using Cloud Storage, Dataflow and BigQuery](./data-solutions/gcs-to-bq-with-least-privileges), [#SQL Server Always On Groups blueprint](./data-solutions/sqlserver-alwayson), [Data Playground](./data-solutions/data-playground), [MLOps with Vertex AI](./data-solutions/vertex-mlops), [Shielded Folder](./data-solutions/shielded-folder) - **factories** - [The why and the how of Resource Factories](./factories), [Google Cloud Identity Group Factory](./factories/cloud-identity-group-factory), [Google Cloud BQ Factory](./factories/bigquery-factory), [Google Cloud VPC Firewall Factory](./factories/net-vpc-firewall-yaml), [Minimal Project Factory](./factories/project-factory) - **GKE** - [Binary Authorization Pipeline Blueprint](./gke/binauthz), [Storage API](./gke/binauthz/image), [Multi-cluster mesh on GKE (fleet API)](./gke/multi-cluster-mesh-gke-fleet-api), [GKE Multitenant Blueprint](./gke/multitenant-fleet), [Shared VPC with GKE support](./networking/shared-vpc-gke/) -- **networking** - [Decentralized firewall management](./networking/decentralized-firewall), [Decentralized firewall validator](./networking/decentralized-firewall/validator), [Network filtering with Squid](./networking/filtering-proxy), [Network filtering with Squid with isolated VPCs using Private Service Connect](./networking/filtering-proxy-psc), [HTTP Load Balancer with Cloud Armor](./networking/glb-and-armor), [Hub and Spoke via VPN](./networking/hub-and-spoke-vpn), [Hub and Spoke via VPC Peering](./networking/hub-and-spoke-peering), [Internal Load Balancer as Next Hop](./networking/ilb-next-hop), [On-prem DNS and Google Private Access](./networking/onprem-google-access-dns), [Calling a private Cloud Function from On-premises](./networking/private-cloud-function-from-onprem), [Hybrid connectivity to on-premise services through PSC](./networking/psc-hybrid), [PSC Producer](./networking/psc-hybrid/psc-producer), [PSC Consumer](./networking/psc-hybrid/psc-consumer), [Shared VPC with optional GKE cluster](./networking/shared-vpc-gke) +- **networking** - [Decentralized firewall management](./networking/decentralized-firewall), [Decentralized firewall validator](./networking/decentralized-firewall/validator), [Network filtering with Squid](./networking/filtering-proxy), [Network filtering with Squid with isolated VPCs using Private Service Connect](./networking/filtering-proxy-psc), [HTTP Load Balancer with Cloud Armor](./networking/glb-and-armor), [Hub and Spoke via VPN](./networking/hub-and-spoke-vpn), [Hub and Spoke via VPC Peering](./networking/hub-and-spoke-peering), [Internal Load Balancer as Next Hop](./networking/ilb-next-hop), On-prem DNS and Google Private Access, [Calling a private Cloud Function from On-premises](./networking/private-cloud-function-from-onprem), [Hybrid connectivity to on-premise services through PSC](./networking/psc-hybrid), [PSC Producer](./networking/psc-hybrid/psc-producer), [PSC Consumer](./networking/psc-hybrid/psc-consumer), [Shared VPC with optional GKE cluster](./networking/shared-vpc-gke) - **serverless** - [Creating multi-region deployments for API Gateway](./serverless/api-gateway) - **third party solutions** - [OpenShift on GCP user-provisioned infrastructure](./third-party-solutions/openshift), [Wordpress deployment on Cloud Run](./third-party-solutions/wordpress/cloudrun) diff --git a/blueprints/apigee/README.md b/blueprints/apigee/README.md new file mode 100644 index 000000000..4cec9de9c --- /dev/null +++ b/blueprints/apigee/README.md @@ -0,0 +1,24 @@ +# Apigee Blueprints + +The blueprints in this folder contain a variety of deployment scenarios for Apigee Hybrid and Apigee X. + +## Blueprints + +### Apigee Hybrid on GKE + + This [blueprint](./hybrid-gke/) shows how to do a non-prod deployment of Apigee Hybrid on GKE(../factories/net-vpc-firewall-yaml/). + +
+ +### Apigee X analytics in BigQuery + +This [blueprint](./bigquery-analytics/) shows how to export on a daily basis the Apigee analytics of an organization to a BigQuery table. + +
+ +### Apigee X network patterns + +The following blueprints demonstrate a set of networking scenarios that can be implemented for Apigee X deployments. + +#### Apigee X - Northbound: GLB with PSC Neg, Southbouth: PSC with ILB (L7) and Hybrid NEG +This [blueprint](./network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/) shows how to expose an on-prem target backend to clients in the Internet. \ No newline at end of file diff --git a/blueprints/cloud-operations/apigee/README.md b/blueprints/apigee/bigquery-analytics/README.md similarity index 83% rename from blueprints/cloud-operations/apigee/README.md rename to blueprints/apigee/bigquery-analytics/README.md index 0802c433b..027f28ead 100644 --- a/blueprints/cloud-operations/apigee/README.md +++ b/blueprints/apigee/bigquery-analytics/README.md @@ -19,7 +19,7 @@ Note: This setup only works if you are not using custom analytics. ## Running the blueprint -1. Clone this repository or [open it in cloud shell](https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2Fterraform-google-modules%2Fcloud-foundation-fabric&cloudshell_print=cloud-shell-readme.txt&cloudshell_working_dir=blueprints%2Fcloud-operations%apigee), then go through the following steps to create resources: +1. Clone this repository or [open it in cloud shell](https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2Fterraform-google-modules%2Fcloud-foundation-fabric&cloudshell_print=cloud-shell-readme.txt&cloudshell_working_dir=blueprints%2Fapigee%2Fbigquery-analytics), then go through the following steps to create resources: 2. Copy the file [terraform.tfvars.sample](./terraform.tfvars.sample) to a file called ```terraform.tfvars``` and update the values if required. @@ -60,14 +60,14 @@ Do the following to verify that everything works as expected. |---|---|:---:|:---:|:---:| | [envgroups](variables.tf#L24) | Environment groups (NAME => [HOSTNAMES]). | map(list(string)) | ✓ | | | [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) | ✓ | | +| [instances](variables.tf#L45) | Instance. | map(object({…})) | ✓ | | +| [project_id](variables.tf#L92) | Project ID. | string | ✓ | | +| [psc_config](variables.tf#L98) | PSC configuration. | map(string) | ✓ | | | [datastore_name](variables.tf#L17) | Datastore. | string | | "gcs" | -| [organization](variables.tf#L59) | Apigee organization. | object({…}) | | {…} | -| [path](variables.tf#L75) | Bucket path. | string | | "/analytics" | -| [project_create](variables.tf#L82) | Parameters for the creation of the new project. | object({…}) | | null | -| [vpc_create](variables.tf#L103) | Boolean flag indicating whether the VPC should be created or not. | bool | | true | +| [organization](variables.tf#L60) | Apigee organization. | object({…}) | | {…} | +| [path](variables.tf#L76) | Bucket path. | string | | "/analytics" | +| [project_create](variables.tf#L83) | Parameters for the creation of the new project. | object({…}) | | null | +| [vpc_create](variables.tf#L104) | Boolean flag indicating whether the VPC should be created or not. | bool | | true | ## Outputs diff --git a/blueprints/cloud-operations/apigee/diagram1.png b/blueprints/apigee/bigquery-analytics/diagram1.png similarity index 100% rename from blueprints/cloud-operations/apigee/diagram1.png rename to blueprints/apigee/bigquery-analytics/diagram1.png diff --git a/blueprints/cloud-operations/apigee/diagram2.png b/blueprints/apigee/bigquery-analytics/diagram2.png similarity index 100% rename from blueprints/cloud-operations/apigee/diagram2.png rename to blueprints/apigee/bigquery-analytics/diagram2.png diff --git a/blueprints/cloud-operations/apigee/functions/export/index.js b/blueprints/apigee/bigquery-analytics/functions/export/index.js similarity index 100% rename from blueprints/cloud-operations/apigee/functions/export/index.js rename to blueprints/apigee/bigquery-analytics/functions/export/index.js diff --git a/blueprints/cloud-operations/apigee/functions/export/package.json b/blueprints/apigee/bigquery-analytics/functions/export/package.json similarity index 100% rename from blueprints/cloud-operations/apigee/functions/export/package.json rename to blueprints/apigee/bigquery-analytics/functions/export/package.json diff --git a/blueprints/cloud-operations/apigee/functions/gcs2bq/index.js b/blueprints/apigee/bigquery-analytics/functions/gcs2bq/index.js similarity index 100% rename from blueprints/cloud-operations/apigee/functions/gcs2bq/index.js rename to blueprints/apigee/bigquery-analytics/functions/gcs2bq/index.js diff --git a/blueprints/cloud-operations/apigee/functions/gcs2bq/package.json b/blueprints/apigee/bigquery-analytics/functions/gcs2bq/package.json similarity index 100% rename from blueprints/cloud-operations/apigee/functions/gcs2bq/package.json rename to blueprints/apigee/bigquery-analytics/functions/gcs2bq/package.json diff --git a/blueprints/cloud-operations/apigee/functions/gcs2bq/schema.json b/blueprints/apigee/bigquery-analytics/functions/gcs2bq/schema.json similarity index 100% rename from blueprints/cloud-operations/apigee/functions/gcs2bq/schema.json rename to blueprints/apigee/bigquery-analytics/functions/gcs2bq/schema.json diff --git a/blueprints/cloud-operations/apigee/main.tf b/blueprints/apigee/bigquery-analytics/main.tf similarity index 94% rename from blueprints/cloud-operations/apigee/main.tf rename to blueprints/apigee/bigquery-analytics/main.tf index 9781a4de4..68e672d25 100644 --- a/blueprints/cloud-operations/apigee/main.tf +++ b/blueprints/apigee/bigquery-analytics/main.tf @@ -68,9 +68,12 @@ module "vpc" { region = k }] psa_config = { - ranges = { - for k, v in var.instances : "apigee-${k}" => v.psa_ip_cidr_range - } + ranges = merge({ for k, v in var.instances : + "apigee-runtime-${k}" => v.runtime_ip_cidr_range + }, { for k, v in var.instances : + "apigee-troubleshooting-${k}" => v.troubleshooting_ip_cidr_range + } + ) } } @@ -94,8 +97,9 @@ module "glb" { use_classic_version = false backend_service_configs = { default = { - backends = [for k, v in var.instances : { backend = k }] - protocol = "HTTPS" + backends = [for k, v in var.instances : { backend = k }] + protocol = "HTTPS" + health_checks = [] } } health_check_configs = { @@ -116,8 +120,10 @@ module "glb" { } } ssl_certificates = { - managed_config = { - for k, v in var.envgroups : k => { domains = [v] } + managed_configs = { + default = { + domains = flatten([for k, v in var.envgroups : v]) + } } } } diff --git a/blueprints/cloud-operations/apigee/outputs.tf b/blueprints/apigee/bigquery-analytics/outputs.tf similarity index 100% rename from blueprints/cloud-operations/apigee/outputs.tf rename to blueprints/apigee/bigquery-analytics/outputs.tf diff --git a/blueprints/cloud-operations/apigee/send-requests.sh b/blueprints/apigee/bigquery-analytics/send-requests.sh similarity index 100% rename from blueprints/cloud-operations/apigee/send-requests.sh rename to blueprints/apigee/bigquery-analytics/send-requests.sh diff --git a/blueprints/cloud-operations/apigee/templates/create-datastore.sh.tpl b/blueprints/apigee/bigquery-analytics/templates/create-datastore.sh.tpl similarity index 100% rename from blueprints/cloud-operations/apigee/templates/create-datastore.sh.tpl rename to blueprints/apigee/bigquery-analytics/templates/create-datastore.sh.tpl diff --git a/blueprints/cloud-operations/apigee/templates/deploy-apiproxy.sh.tpl b/blueprints/apigee/bigquery-analytics/templates/deploy-apiproxy.sh.tpl similarity index 100% rename from blueprints/cloud-operations/apigee/templates/deploy-apiproxy.sh.tpl rename to blueprints/apigee/bigquery-analytics/templates/deploy-apiproxy.sh.tpl diff --git a/tests/blueprints/cloud_operations/apigee/fixture/test.regular.tfvars b/blueprints/apigee/bigquery-analytics/terraform.tfvars.sample similarity index 66% rename from tests/blueprints/cloud_operations/apigee/fixture/test.regular.tfvars rename to blueprints/apigee/bigquery-analytics/terraform.tfvars.sample index 867cdb119..5a25a9f37 100644 --- a/tests/blueprints/cloud_operations/apigee/fixture/test.regular.tfvars +++ b/blueprints/apigee/bigquery-analytics/terraform.tfvars.sample @@ -1,10 +1,10 @@ project_create = { - billing_account_id = "12345-12345-12345" + billing_account_id = "12345-12345-123456" parent = "folders/123456789" } project_id = "my-project" envgroups = { - test = ["test.cool-demos.space"] + test = ["test.myorg.org"] } environments = { apis-test = { @@ -15,7 +15,8 @@ instances = { instance-ew1 = { region = "europe-west1" environments = ["apis-test"] - psa_ip_cidr_range = "10.0.4.0/22" + runtime_ip_cidr_range = "10.0.4.0/22" + troubleshooting_ip_cidr_range = "10.1.1.0/28" } } psc_config = { diff --git a/blueprints/cloud-operations/apigee/variables.tf b/blueprints/apigee/bigquery-analytics/variables.tf similarity index 86% rename from blueprints/cloud-operations/apigee/variables.tf rename to blueprints/apigee/bigquery-analytics/variables.tf index ba7f5d78a..1bd6cb0ac 100644 --- a/blueprints/cloud-operations/apigee/variables.tf +++ b/blueprints/apigee/bigquery-analytics/variables.tf @@ -45,13 +45,14 @@ variable "environments" { variable "instances" { description = "Instance." type = map(object({ - display_name = optional(string) - description = optional(string) - region = string - environments = list(string) - psa_ip_cidr_range = string - disk_encryption_key = optional(string) - consumer_accept_list = optional(list(string)) + display_name = optional(string) + description = optional(string) + region = string + environments = list(string) + runtime_ip_cidr_range = string + troubleshooting_ip_cidr_range = string + disk_encryption_key = optional(string) + consumer_accept_list = optional(list(string)) })) nullable = false } diff --git a/blueprints/networking/onprem-google-access-dns/versions.tf b/blueprints/apigee/bigquery-analytics/versions.tf similarity index 91% rename from blueprints/networking/onprem-google-access-dns/versions.tf rename to blueprints/apigee/bigquery-analytics/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/networking/onprem-google-access-dns/versions.tf +++ b/blueprints/apigee/bigquery-analytics/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/apigee/hybrid-gke/README.md b/blueprints/apigee/hybrid-gke/README.md new file mode 100644 index 000000000..ae5c03648 --- /dev/null +++ b/blueprints/apigee/hybrid-gke/README.md @@ -0,0 +1,69 @@ +# Apigee Hybrid on GKE + +This example installs Apigee hybrid in a non-prod environment on a GKE private cluster using Terraform and Ansible. +The Terraform configuration deploys all the required infrastructure including a management VM used to run an ansible playbook to the actual Apigee Hybrid setup. + +The diagram below depicts the architecture. + +![Diagram](./diagram.png) + +## Running the blueprint + +1. Clone this repository or [open it in cloud shell](https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2Fterraform-google-modules%2Fcloud-foundation-fabric&cloudshell_print=cloud-shell-readme.txt&cloudshell_working_dir=blueprints%2Fapigee%2Fhybrid), then go through the following steps to create resources: + +2. Copy the file [terraform.tfvars.sample](./terraform.tfvars.sample) to a file called ```terraform.tfvars``` and update the values if required. + +3. Initialize the terraform configuration + + ``` + terraform init + ``` + +4. Apply the terraform configuration + + ``` + terraform apply + ``` + + Create an A record in your DNS registrar to point the environment group hostname to the public IP address returned after the terraform configuration was applied. You might need to wait some time until the certificate is provisioned. + +5. Install Apigee hybrid using de ansible playbook that is in the ansible folder by running this command + + ansible-playbook playbook.yaml -vvvß + +## Testing the blueprint + +2. Deploy an api proxy + + ``` + ./deploy-apiproxy.sh apis-test + ``` + +3. Send a request + + ``` + curl -v https://HOSTNAME/httpbin/headers + ``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [hostname](variables.tf#L43) | Host name. | string | ✓ | | +| [project_id](variables.tf#L79) | Project ID. | string | ✓ | | +| [cluster_machine_type](variables.tf#L17) | Cluster nachine type. | string | | "e2-standard-4" | +| [cluster_network_config](variables.tf#L23) | Cluster network configuration. | object({…}) | | {…} | +| [mgmt_server_config](variables.tf#L48) | Mgmt server configuration. | object({…}) | | {…} | +| [mgmt_subnet_cidr_block](variables.tf#L64) | Management subnet CIDR block. | string | | "10.0.2.0/28" | +| [project_create](variables.tf#L70) | Parameters for the creation of the new project. | object({…}) | | null | +| [region](variables.tf#L84) | Region. | string | | "europe-west1" | +| [zone](variables.tf#L90) | Zone. | string | | "europe-west1-c" | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [ip_address](outputs.tf#L17) | GLB IP address. | | + + diff --git a/blueprints/apigee/hybrid-gke/ansible.tf b/blueprints/apigee/hybrid-gke/ansible.tf new file mode 100644 index 000000000..b7694ab1f --- /dev/null +++ b/blueprints/apigee/hybrid-gke/ansible.tf @@ -0,0 +1,40 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Ansible generated files. + +resource "local_file" "vars_file" { + content = yamlencode({ + cluster = module.cluster.name + region = var.region + project_id = module.project.project_id + envgroups = local.envgroups + environments = local.environments + service_accounts = local.google_sas + ingress_ip_name = local.ingress_ip_name + }) + filename = "${path.module}/ansible/vars/vars.yaml" + file_permission = "0666" +} + +resource "local_file" "gssh_file" { + content = templatefile("${path.module}/templates/gssh.sh.tpl", { + project_id = module.project.project_id + zone = var.zone + }) + filename = "${path.module}/ansible/gssh.sh" + file_permission = "0777" +} diff --git a/blueprints/apigee/hybrid-gke/ansible/ansible.cfg b/blueprints/apigee/hybrid-gke/ansible/ansible.cfg new file mode 100644 index 000000000..654f1729d --- /dev/null +++ b/blueprints/apigee/hybrid-gke/ansible/ansible.cfg @@ -0,0 +1,8 @@ +[defaults] +inventory = inventory/hosts.ini +timeout = 900 + +[ssh_connection] +pipelining = True +ssh_executable = ./gssh.sh +transfer_method = piped \ No newline at end of file diff --git a/blueprints/apigee/hybrid-gke/ansible/inventory/hosts.ini b/blueprints/apigee/hybrid-gke/ansible/inventory/hosts.ini new file mode 100644 index 000000000..842da83f4 --- /dev/null +++ b/blueprints/apigee/hybrid-gke/ansible/inventory/hosts.ini @@ -0,0 +1 @@ +mgmt \ No newline at end of file diff --git a/blueprints/apigee/hybrid-gke/ansible/playbook.yaml b/blueprints/apigee/hybrid-gke/ansible/playbook.yaml new file mode 100644 index 000000000..1daa4d86a --- /dev/null +++ b/blueprints/apigee/hybrid-gke/ansible/playbook.yaml @@ -0,0 +1,26 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +- hosts: mgmt + gather_facts: "no" + vars_files: + - vars/vars.yaml + environment: + USE_GKE_GCLOUD_AUTH_PLUGIN: True + roles: + - role: prerequisites + become: yes + become_method: sudo + - role: apigee-hybrid + \ No newline at end of file diff --git a/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/k8s_service_accounts.yaml b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/k8s_service_accounts.yaml new file mode 100644 index 000000000..e74ca1596 --- /dev/null +++ b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/k8s_service_accounts.yaml @@ -0,0 +1,28 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +- name: Create and annotate k8s service account + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: ServiceAccount + metadata: + name: "{{ k8s_service_account }}" + namespace: apigee + annotations: + iam.gke.io/gcp-service-account: "{{ google_service_account }}@{{ project_id }}.iam.gserviceaccount.com" + with_items: "{{ k8s_service_accounts }}" + loop_control: + loop_var: k8s_service_account \ No newline at end of file diff --git a/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/main.yaml b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/main.yaml new file mode 100644 index 000000000..0907846fd --- /dev/null +++ b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/main.yaml @@ -0,0 +1,333 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +- name: Get cluster credentials + shell: > + gcloud container clusters get-credentials {{ cluster }} \ + --region {{ region }} \ + --project {{ project_id }} \ + --internal-ip + +- name: Download cert-manager + uri: + url: https://github.com/jetstack/cert-manager/releases/download/v1.7.2/cert-manager.yaml + dest: ~/cert-manager.yaml + +- name: Apply metrics-server manifest to the cluster. + kubernetes.core.k8s: + state: present + src: ~/cert-manager.yaml + +- name: + kubernetes.core.k8s_info: + kind: Pod + wait: yes + label_selectors: + - "app.kubernetes.io/instance=cert-manager" + namespace: cert-manager + wait_timeout: 90 + wait_condition: + type: Ready + status: True + +- name: Fetch apigeectl version + uri: + url: https://storage.googleapis.com/apigee-release/hybrid/apigee-hybrid-setup/current-version.txt?ignoreCache=1 + return_content: yes + register: version + +- name: Download apigeectl bundle + uri: + url: https://storage.googleapis.com/apigee-release/hybrid/apigee-hybrid-setup/{{ version.content }}/apigeectl_linux_64.tar.gz + dest: "~/apigeectl.tar.gz" + status_code: [200, 304] + +- name: Extract apigeectl bundle + unarchive: + src: "~/apigeectl.tar.gz" + dest: "~" + remote_src: yes + +- name: Move apigeectl folder + shell: > + mv ~/apigeectl_* ~/apigeectl + +- name: Create hybrid-files + file: + path: "~/hybrid-files/{{ item }}" + state: directory + with_items: + - overrides + - certs + +- name: Create a symbolic links + file: + src: ~/apigeectl/{{ item }} + dest: "~/hybrid-files/{{ item }}" + state: link + with_items: + - tools + - config + - templates + - plugins + +- name: Create apigee namespace + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Namespace + metadata: + name: apigee + +- name: Create k8s service accounts + include_tasks: k8s_service_accounts.yaml + vars: + google_service_account: "{{ item.key }}" + k8s_service_accounts: "{{ item.value }}" + with_dict: "{{ service_accounts }}" + +- name: Set hostnames + set_fact: + hostnames: "{{ hostnames | default([]) + item.value }}" + with_dict: "{{ envgroups }}" + +- name: Create certificate and private key + shell: > + openssl req \ + -nodes \ + -new \ + -x509 \ + -keyout ~/hybrid-files/certs/server.key \ + -out ~/hybrid-files/certs/server.crt \ + -subj "/CN=apigee.com' \ + -addext "subjectAltName={{ hostnames | map('regex_replace', '^', 'DNS:') | join(',') }}"" + -days 3650 + +- name: Read certificate + slurp: + src: ~/hybrid-files/certs/server.crt + register: certificate_output + +- name: Read private ket + slurp: + src: ~/hybrid-files/certs/server.key + register: privatekey_output + +- name: Create secret + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Secret + metadata: + name: tls-hybrid-ingress + namespace: apigee + type: kubernetes.io/tls + data: + tls.crt: "{{ certificate_output.content }}" + tls.key: "{{ privatekey_output.content }}" + +- name: Create overrides.yaml + template: + src: templates/overrides.yaml.j2 + dest: ~/hybrid-files/overrides/overrides.yaml + +- name: Enable syncronizer access + shell: > + curl -X POST -H "Authorization: Bearer $(gcloud auth print-access-token)" \ + -H "Content-Type:application/json" \ + "https://apigee.googleapis.com/v1/organizations/{{ project_id }}:setSyncAuthorization" \ + -d '{"identities":["'"serviceAccount:apigee-synchronizer@{{ project_id }}.iam.gserviceaccount.com"'"]}' + +- name: Dry-run (init) + shell: > + ~/apigeectl/apigeectl init -f overrides/overrides.yaml --dry-run=client + args: + chdir: ~/hybrid-files + +- name: Install the Apigee deployment services Apigee Deployment Controller and Apigee Admission Webhook. + shell: > + ~/apigeectl/apigeectl init -f overrides/overrides.yaml + args: + chdir: ~/hybrid-files + +- name: Wait for apigee-controller pod to be ready + kubernetes.core.k8s_info: + kind: Pod + wait: yes + label_selectors: + - "app=apigee-controller" + namespace: apigee-system + wait_timeout: 600 + wait_condition: + type: Ready + status: True + +- name: Wait for apigee-selfsigned-issuer issuer to be ready + kubernetes.core.k8s_info: + kind: Issuer + wait: yes + name: apigee-selfsigned-issuer + namespace: apigee-system + wait_timeout: 600 + wait_condition: + type: Ready + status: True + +- name: Wait for apigee-serving-cert certificate to be ready + kubernetes.core.k8s_info: + kind: Certificate + wait: yes + name: apigee-serving-cert + namespace: apigee-system + wait_timeout: 600 + wait_condition: + type: Ready + status: True + +- name: Wait for apigee-resources-install job to be complete + kubernetes.core.k8s_info: + kind: Job + wait: yes + name: apigee-resources-install + namespace: apigee-system + wait_timeout: 360 + wait_condition: + type: Complete + status: True + +- name: Dry-run (apply) + shell: > + ~/apigeectl/apigeectl apply -f overrides/overrides.yaml --dry-run=client + args: + chdir: ~/hybrid-files + +- name: Install the Apigee runtime components + shell: > + ~/apigeectl/apigeectl apply -f overrides/overrides.yaml + args: + chdir: ~/hybrid-files + +- name: Wait for apigee-runtime pod to be ready + kubernetes.core.k8s_info: + kind: Pod + wait: yes + label_selectors: + - "app=apigee-runtime" + namespace: apigee + wait_timeout: 360 + wait_condition: + type: Ready + status: True + +- name: + kubernetes.core.k8s: + state: present + definition: + apiVersion: apigee.cloud.google.com/v1alpha1 + kind: ApigeeRoute + metadata: + name: apigee-wildcard + namespace: apigee + spec: + hostnames: + - '*' + ports: + - number: 443 + protocol: HTTPS + tls: + credentialName: tls-hybrid-ingress + mode: SIMPLE + selector: + app: apigee-ingressgateway + enableNonSniClient: true + +- name: Create google-managed certificate + kubernetes.core.k8s: + state: present + definition: + apiVersion: networking.gke.io/v1 + kind: ManagedCertificate + metadata: + name: "apigee-cert-hybrid" + namespace: apigee + spec: + domains: "{{ hostnames }}" + +- name: Create backend config + kubernetes.core.k8s: + state: present + definition: + apiVersion: cloud.google.com/v1 + kind: BackendConfig + metadata: + name: apigee-ingress-backendconfig + namespace: apigee + spec: + healthCheck: + requestPath: /healthz/ready + port: 15021 + type: HTTP + logging: + enable: true + sampleRate: 0.5 + +- name: Create service + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Service + metadata: + name: apigee-ingressgateway-hybrid + namespace: apigee + annotations: + cloud.google.com/backend-config: '{"default": "apigee-ingress-backendconfig"}' + cloud.google.com/neg: '{"ingress": true}' + cloud.google.com/app-protocols: '{"https":"HTTPS", "status-port": "HTTP"}' + labels: + app: apigee-ingressgateway-hybrid + spec: + ports: + - name: status-port + port: 15021 + targetPort: 15021 + - name: https + port: 443 + targetPort: 8443 + selector: + app: apigee-ingressgateway + ingress_name: ingress + type: ClusterIP + +- name: Create ingress + kubernetes.core.k8s: + state: present + definition: + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + annotations: + networking.gke.io/managed-certificates: "apigee-cert-hybrid" + kubernetes.io/ingress.global-static-ip-name: "{{ ingress_ip_name }}" + kubernetes.io/ingress.allow-http: "false" + name: xlb-apigee + namespace: apigee + spec: + defaultBackend: + service: + name: apigee-ingressgateway-hybrid + port: + number: 443 \ No newline at end of file diff --git a/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/templates/overrides.yaml.j2 b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/templates/overrides.yaml.j2 new file mode 100644 index 000000000..691cc6d5d --- /dev/null +++ b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/templates/overrides.yaml.j2 @@ -0,0 +1,42 @@ +gcp: + region: {{ region }} + projectID: {{ project_id }} + workloadIdentityEnabled: true + +k8sCluster: + name: {{ cluster }} + region: {{ region }} # Must be the closest Google Cloud region to your cluster. +org: {{ project_id }} + +instanceID: "{{ cluster }}-{{ region }}" + +cassandra: + hostNetwork: false + +virtualhosts: +{% for k in envgroups %} + - name: {{ k }} + sslSecret: tls-hybrid-ingress + additionalGateways: ["apigee-wildcard"] + selector: + app: apigee-ingressgateway +{% endfor %} + +ao: + args: + # This configuration is introduced in hybrid v1.8 + disableIstioConfigInAPIServer: true + +# This configuration is introduced in hybrid v1.8 +ingressGateways: +- name: ingress # maximum 17 characters. See Known issue 243167389. + replicaCountMin: 2 + replicaCountMax: 10 + +envs: +{% for k in environments %} + - name: {{ k }} +{% endfor %} + +logger: + enabled: false diff --git a/blueprints/apigee/hybrid-gke/ansible/roles/prerequisites/tasks/main.yaml b/blueprints/apigee/hybrid-gke/ansible/roles/prerequisites/tasks/main.yaml new file mode 100644 index 000000000..b438a6342 --- /dev/null +++ b/blueprints/apigee/hybrid-gke/ansible/roles/prerequisites/tasks/main.yaml @@ -0,0 +1,37 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +- name: Download the Google Cloud SDK package repository signing key + get_url: + url: https://packages.cloud.google.com/apt/doc/apt-key.gpg + dest: /usr/share/keyrings/cloud.google.gpg + +- name: Add Google Cloud SDK package repository source + apt_repository: + filename: google-cloud-sdk.list + repo: "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" + state: present + update_cache: yes + +- name: Install dependencies + apt: + pkg: + - kubectl + - google-cloud-sdk-gke-gcloud-auth-plugin + state: present + +- name: Install gke-gcloud-auth-plugin + apt: + name: google-cloud-sdk-gke-gcloud-auth-plugin + state: present \ No newline at end of file diff --git a/blueprints/apigee/hybrid-gke/apigee.tf b/blueprints/apigee/hybrid-gke/apigee.tf new file mode 100644 index 000000000..b92592aab --- /dev/null +++ b/blueprints/apigee/hybrid-gke/apigee.tf @@ -0,0 +1,93 @@ +/** + * 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 { + envgroups = { + test = [var.hostname] + } + environments = { + apis-test = { + envgroups = ["test"] + } + } + org_short_name = (length(module.project.project_id) < 16 ? + module.project.project_id : + substr(module.project.project_id, 0, 15)) + org_hash = format("%s-%s", local.org_short_name, substr(sha256(module.project.project_id), 0, 7)) + org_env_hashes = { + for k, v in local.environments : + k => format("%s-%s-%s", local.org_short_name, length(k) < 16 ? k : substr(k, 0, 15), substr(sha256("${module.project.project_id}:${k}"), 0, 7)) + } + google_sas = { + apigee-metrics = [ + "apigee-metrics-sa" + ] + apigee-cassandra = [ + "apigee-cassandra-schema-setup-${local.org_hash}-sa", + "apigee-cassandra-user-setup-${local.org_hash}-sa" + ] + apigee-mart = [ + "apigee-mart-${local.org_hash}-sa", + "apigee-connect-agent-${local.org_hash}-sa" + ] + apigee-watcher = [ + "apigee-watcher-${local.org_hash}-sa" + ] + apigee-udca = concat([ + "apigee-udca-${local.org_hash}-sa" + ], + [for k, v in local.org_env_hashes : + "apigee-udca-${local.org_env_hashes[k]}-sa" + ]) + apigee-synchronizer = [ + for k, v in local.org_env_hashes : + "apigee-synchronizer-${local.org_env_hashes[k]}-sa" + ] + apigee-runtime = [for k, v in local.org_env_hashes : + "apigee-runtime-${local.org_env_hashes[k]}-sa" + ] + } +} + +module "apigee" { + source = "../../../modules/apigee" + project_id = module.project.project_id + organization = { + analytics_region = var.region + runtime_type = "HYBRID" + } + envgroups = local.envgroups + environments = local.environments +} + +module "sas" { + for_each = local.google_sas + source = "../../../modules/iam-service-account" + project_id = module.project.project_id + name = each.key + # authoritative roles granted *on* the service accounts to other identities + iam = { + "roles/iam.workloadIdentityUser" = [for v in each.value : "serviceAccount:${module.project.project_id}.svc.id.goog[apigee/${v}]"] + } +} + +resource "local_file" "deploy_apiproxy_file" { + content = templatefile("${path.module}/templates/deploy-apiproxy.sh.tpl", { + org = module.project.project_id + }) + filename = "${path.module}/deploy-apiproxy.sh" + file_permission = "0777" +} diff --git a/blueprints/apigee/hybrid-gke/diagram.png b/blueprints/apigee/hybrid-gke/diagram.png new file mode 100644 index 000000000..57e07ca30 Binary files /dev/null and b/blueprints/apigee/hybrid-gke/diagram.png differ diff --git a/blueprints/apigee/hybrid-gke/gke.tf b/blueprints/apigee/hybrid-gke/gke.tf new file mode 100644 index 000000000..22cf06fab --- /dev/null +++ b/blueprints/apigee/hybrid-gke/gke.tf @@ -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. + */ + +module "cluster" { + source = "../../../modules/gke-cluster" + project_id = module.project.project_id + 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" + } + master_authorized_ranges = var.cluster_network_config.master_authorized_cidr_blocks + master_ipv4_cidr_block = var.cluster_network_config.master_cidr_block + } + max_pods_per_node = 32 + private_cluster_config = { + enable_private_endpoint = true + master_global_access = false + } + enable_features = { + workload_identity = true + } +} + +module "apigee-data-nodepool" { + source = "../../../modules/gke-nodepool" + project_id = module.project.project_id + cluster_name = module.cluster.name + location = var.region + name = "apigee-data-nodepool" + nodepool_config = { + autoscaling = { + min_node_count = 1 + max_node_count = 3 + } + } + node_config = { + machine_type = var.cluster_machine_type + } + service_account = { + create = true + } + tags = ["node"] +} + +module "apigee-runtime-nodepool" { + source = "../../../modules/gke-nodepool" + project_id = module.project.project_id + cluster_name = module.cluster.name + location = var.region + name = "apigee-runtime-nodepool" + nodepool_config = { + autoscaling = { + min_node_count = 1 + max_node_count = 3 + } + } + node_config = { + machine_type = var.cluster_machine_type + } + service_account = { + create = true + } + tags = ["node"] +} \ No newline at end of file diff --git a/tests/blueprints/data_solutions/cmek_via_centralized_kms/fixture/variables.tf b/blueprints/apigee/hybrid-gke/glb.tf similarity index 63% rename from tests/blueprints/data_solutions/cmek_via_centralized_kms/fixture/variables.tf rename to blueprints/apigee/hybrid-gke/glb.tf index 6a534739f..80ff2269c 100644 --- a/tests/blueprints/data_solutions/cmek_via_centralized_kms/fixture/variables.tf +++ b/blueprints/apigee/hybrid-gke/glb.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,13 +14,12 @@ * limitations under the License. */ -variable "billing_account" { - type = string - default = "123456-123456-123456" +locals { + ingress_ip_name = "apigee" } -variable "root_node" { - description = "The resource name of the parent Folder or Organization. Must be of the form folders/folder_id or organizations/org_id." - type = string - default = "folders/12345678" +module "addresses" { + source = "../../../modules/net-address" + project_id = module.project.project_id + global_addresses = [local.ingress_ip_name] } diff --git a/blueprints/apigee/hybrid-gke/main.tf b/blueprints/apigee/hybrid-gke/main.tf new file mode 100644 index 000000000..5be174ef5 --- /dev/null +++ b/blueprints/apigee/hybrid-gke/main.tf @@ -0,0 +1,51 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module "project" { + source = "../../../modules/project" + billing_account = (var.project_create != null + ? var.project_create.billing_account_id + : null + ) + parent = (var.project_create != null + ? var.project_create.parent + : null + ) + project_create = var.project_create != null + name = var.project_id + services = [ + "apigee.googleapis.com", + "apigeeconnect.googleapis.com", + "cloudresourcemanager.googleapis.com", + "compute.googleapis.com", + "container.googleapis.com", + "pubsub.googleapis.com" + ] + iam = { + "roles/apigee.admin" = [module.mgmt_server.service_account_iam_email] + "roles/container.admin" = [module.mgmt_server.service_account_iam_email] + "roles/resourcemanager.projectIamAdmin" = [module.mgmt_server.service_account_iam_email] + "roles/iam.serviceAccountAdmin" = [module.mgmt_server.service_account_iam_email] + "roles/iam.serviceAccountKeyAdmin" = [module.mgmt_server.service_account_iam_email] + "roles/monitoring.metricWriter" = [module.sas["apigee-metrics"].iam_email] + "roles/storage.objectAdmin" = [module.sas["apigee-cassandra"].iam_email] + "roles/apigeeconnect.Agent" = [module.sas["apigee-mart"].iam_email] + "roles/apigee.runtimeAgent" = [module.sas["apigee-watcher"].iam_email] + "roles/apigee.analyticsAgent" = [module.sas["apigee-udca"].iam_email] + "roles/apigee.synchronizerManager" = [module.sas["apigee-synchronizer"].iam_email] + "roles/cloudtrace.agent" = [module.sas["apigee-runtime"].iam_email] + } +} diff --git a/blueprints/apigee/hybrid-gke/mgmt.tf b/blueprints/apigee/hybrid-gke/mgmt.tf new file mode 100644 index 000000000..538940e7b --- /dev/null +++ b/blueprints/apigee/hybrid-gke/mgmt.tf @@ -0,0 +1,45 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Management server. + +module "mgmt_server" { + source = "../../../modules/compute-vm" + project_id = module.project.project_id + zone = var.zone + name = "mgmt" + instance_type = var.mgmt_server_config.instance_type + network_interfaces = [{ + network = module.vpc.self_link + subnetwork = module.vpc.subnet_self_links["${var.region}/subnet-mgmt"] + nat = false + addresses = null + }] + service_account_create = true + boot_disk = { + image = var.mgmt_server_config.image + type = var.mgmt_server_config.disk_type + size = var.mgmt_server_config.disk_size + } + metadata = { + startup-script = <This [blueprint](./nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/) shows how to expose an on-prem target backend to clients in the Internet.g \ No newline at end of file diff --git a/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/README.md b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/README.md new file mode 100644 index 000000000..690458f03 --- /dev/null +++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/README.md @@ -0,0 +1,69 @@ +# Apigee X - Northbound GLB with PSC Neg, Southbouth PSC with ILB (L7) and Hybrid NEG + +The following blueprint shows how to expose an on-prem target backend to clients in the Internet. + +The architecture is the one depicted below. + +![Diagram](diagram.png) + +To emulate an service deployed on-premise, we have used a managed instance group of instances running Nginx exposed via a regional internalload balancer (L7). The service is accesible through VPN. + +## Running the blueprint + +1. Clone this repository or [open it in cloud shell](https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2Fterraform-google-modules%2Fcloud-foundation-fabric&cloudshell_print=cloud-shell-readme.txt&cloudshell_working_dir=blueprints%2F%apigee%2F/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg), then go through the following steps to create resources: + +2. Copy the file [terraform.tfvars.sample](./terraform.tfvars.sample) to a file called ```terraform.tfvars``` and update the values if required. + +3. Initialize the terraform configuration + + ```terraform init``` + +4. Apply the terraform configuration + + ```terraform apply``` + +Once the resources have been created, do the following: + +Create an A record in your DNS registrar to point the environment group hostname to the public IP address returned after the terraform configuration was applied. You might need to wait some time until the certificate is provisioned. + +## Testing the blueprint + +Do the following to verify that everything works as expected. + +1. Deploy the API proxy + + ./deploy-apiproxy.sh + +2. Send a request + + curl -v https://HOSTNAME/test/ + + You should get back an HTTP 200 OK response. + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [apigee_project_id](variables.tf#L17) | Project ID. | string | ✓ | | +| [billing_account_id](variables.tf#L53) | Parameters for the creation of the new project. | string | ✓ | | +| [hostname](variables.tf#L58) | Host name. | string | ✓ | | +| [onprem_project_id](variables.tf#L63) | Project ID. | string | ✓ | | +| [parent](variables.tf#L81) | Parent (organizations/organizationID or folders/folderID). | string | ✓ | | +| [apigee_proxy_only_subnet_ip_cidr_range](variables.tf#L23) | Subnet IP CIDR range. | string | | "10.2.1.0/24" | +| [apigee_psc_subnet_ip_cidr_range](variables.tf#L29) | Subnet IP CIDR range. | string | | "10.2.2.0/24" | +| [apigee_runtime_ip_cidr_range](variables.tf#L35) | Apigee PSA IP CIDR range. | string | | "10.0.4.0/22" | +| [apigee_subnet_ip_cidr_range](variables.tf#L41) | Subnet IP CIDR range. | string | | "10.2.0.0/24" | +| [apigee_troubleshooting_ip_cidr_range](variables.tf#L47) | Apigee PSA IP CIDR range. | string | | "10.1.0.0/28" | +| [onprem_proxy_only_subnet_ip_cidr_range](variables.tf#L69) | Subnet IP CIDR range. | string | | "10.1.1.0/24" | +| [onprem_subnet_ip_cidr_range](variables.tf#L75) | Subnet IP CIDR range. | string | | "10.1.0.0/24" | +| [region](variables.tf#L86) | Region. | string | | "europe-west1" | +| [zone](variables.tf#L92) | Zone. | string | | "europe-west1-c" | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [ip_address](outputs.tf#L17) | GLB IP address. | | + + 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 new file mode 100644 index 000000000..8860e404c --- /dev/null +++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/apigee.tf @@ -0,0 +1,98 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + envgroup = "test" + environment = "apis-test" +} + +module "apigee_project" { + source = "../../../../modules/project" + billing_account = var.billing_account_id + parent = var.parent + name = var.apigee_project_id + services = [ + "apigee.googleapis.com", + "compute.googleapis.com", + "servicenetworking.googleapis.com", + ] +} + +module "apigee_vpc" { + source = "../../../../modules/net-vpc" + project_id = module.apigee_project.project_id + name = "vpc" + subnets_proxy_only = [ + { + ip_cidr_range = var.apigee_proxy_only_subnet_ip_cidr_range + name = "regional-proxy" + region = var.region + active = true + } + ] + subnets = [ + { + ip_cidr_range = var.apigee_subnet_ip_cidr_range + name = "subnet" + region = var.region + } + ] + subnets_psc = [{ + ip_cidr_range = var.apigee_psc_subnet_ip_cidr_range + name = "subnet-psc" + region = var.region + }] + psa_config = { + ranges = { + "apigee-runtime" = var.apigee_runtime_ip_cidr_range + "apigee-troubleshooting" = var.apigee_troubleshooting_ip_cidr_range + } + } +} + +module "apigee" { + source = "../../../../modules/apigee" + project_id = module.apigee_project.project_id + organization = { + authorized_network = module.apigee_vpc.network.name + analytics_region = var.region + } + envgroups = { + (local.envgroup) = [var.hostname] + } + environments = { + (local.environment) = { + envgroups = [local.envgroup] + } + } + instances = { + instance-1 = { + region = 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 + } + } + endpoint_attachments = { + backend = { + region = var.region + service_attachment = google_compute_service_attachment.service_attachment.id + } + } + depends_on = [ + module.apigee_vpc + ] +} diff --git a/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/apigee_nb.tf b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/apigee_nb.tf new file mode 100644 index 000000000..b568da9a0 --- /dev/null +++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/apigee_nb.tf @@ -0,0 +1,50 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module "glb" { + source = "../../../../modules/net-glb" + name = "glb" + project_id = module.apigee_project.project_id + protocol = "HTTPS" + use_classic_version = false + backend_service_configs = { + default = { + backends = [{ backend = "neg-0" }] + protocol = "HTTPS" + health_checks = [] + } + } + neg_configs = { + neg-0 = { + psc = { + region = var.region + target_service = module.apigee.instances["instance-1"].service_attachment + network = module.apigee_vpc.network.self_link + subnetwork = ( + module.apigee_vpc.subnets_psc["${var.region}/subnet-psc"].self_link + ) + } + } + } + ssl_certificates = { + managed_configs = { + default = { + domains = [var.hostname] + } + } + } + +} diff --git a/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/apigee_sb.tf b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/apigee_sb.tf new file mode 100644 index 000000000..e6df149b2 --- /dev/null +++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/apigee_sb.tf @@ -0,0 +1,68 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module "apigee_ilb_l7" { + source = "../../../../modules/net-ilb-l7" + name = "apigee-ilb" + project_id = module.apigee_project.project_id + region = var.region + backend_service_configs = { + default = { + backends = [{ + balancing_mode = "RATE" + group = "my-neg" + max_rate = { per_endpoint = 1 } + }] + } + } + neg_configs = { + my-neg = { + hybrid = { + zone = var.zone + endpoints = { + e-0 = { + ip_address = module.onprem_ilb_l7.address + port = 80 + } + } + } + } + } + health_check_configs = { + default = { + http = { + port = 80 + } + } + } + vpc_config = { + network = module.apigee_vpc.self_link + subnetwork = module.apigee_vpc.subnet_self_links["${var.region}/subnet"] + } + depends_on = [ + module.apigee_vpc.subnets_proxy_only + ] +} + +resource "google_compute_service_attachment" "service_attachment" { + name = "service-attachment" + project = module.apigee_project.project_id + region = var.region + enable_proxy_protocol = false + connection_preference = "ACCEPT_AUTOMATIC" + nat_subnets = [module.apigee_vpc.subnets_psc["${var.region}/subnet-psc"].self_link] + target_service = module.apigee_ilb_l7.forwarding_rule.id +} diff --git a/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/apiproxy.tf b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/apiproxy.tf new file mode 100644 index 000000000..a94b11eec --- /dev/null +++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/apiproxy.tf @@ -0,0 +1,41 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +resource "local_file" "target_endpoint_file" { + content = templatefile("${path.module}/templates/targets/default.xml.tpl", { + ip_address = module.apigee.endpoint_attachment_hosts["backend"] + }) + filename = "${path.module}/bundle/apiproxy/targets/default.xml" + file_permission = "0777" +} + +data "archive_file" "bundle" { + type = "zip" + source_dir = "${path.module}/bundle" + output_path = "${path.module}/bundle.zip" + depends_on = [ + local_file.target_endpoint_file + ] +} + +resource "local_file" "deploy_apiproxy_file" { + content = templatefile("${path.module}/templates/deploy-apiproxy.sh.tpl", { + organization = module.apigee.org_name + environment = local.environment + }) + filename = "${path.module}/deploy-apiproxy.sh" + file_permission = "0777" +} diff --git a/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/bundle/apiproxy/proxies/default.xml b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/bundle/apiproxy/proxies/default.xml new file mode 100644 index 000000000..a277b3cda --- /dev/null +++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/bundle/apiproxy/proxies/default.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + /test + + + default + + diff --git a/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/bundle/apiproxy/test.xml b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/bundle/apiproxy/test.xml new file mode 100644 index 000000000..93812d829 --- /dev/null +++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/bundle/apiproxy/test.xml @@ -0,0 +1,10 @@ + + + /test + + default + + + default + + diff --git a/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/diagram.png b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/diagram.png new file mode 100644 index 000000000..8667cd318 Binary files /dev/null and b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/diagram.png differ diff --git a/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/onprem.tf b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/onprem.tf new file mode 100644 index 000000000..07bedf8a8 --- /dev/null +++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/onprem.tf @@ -0,0 +1,152 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module "onprem_project" { + source = "../../../../modules/project" + billing_account = var.billing_account_id + parent = var.parent + name = var.onprem_project_id + services = [ + "compute.googleapis.com" + ] +} + +module "onprem_vpc" { + source = "../../../../modules/net-vpc" + project_id = module.onprem_project.project_id + name = "vpc" + subnets_proxy_only = [ + { + ip_cidr_range = var.onprem_proxy_only_subnet_ip_cidr_range + name = "regional-proxy" + region = var.region + active = true + } + ] + subnets = [ + { + ip_cidr_range = var.onprem_subnet_ip_cidr_range + name = "subnet" + region = var.region + } + ] +} + +module "firewall" { + source = "../../../../modules/net-vpc-firewall" + project_id = module.onprem_project.project_id + network = module.onprem_vpc.network.name + default_rules_config = { + disabled = true + } + ingress_rules = { + fw-allow-health-check = { + source_ranges = ["35.191.0.0/16", "130.211.0.0/22"] + targets = ["http-server"] + rules = [{ protocol = "tcp", ports = ["80"] }] + } + fw-allow-proxies = { + source_ranges = [var.onprem_proxy_only_subnet_ip_cidr_range] + targets = ["http-server"] + rules = [{ protocol = "tcp", ports = ["80"] }] + } + } +} + +module "cos-nginx" { + source = "../../../../modules/cloud-config-container/nginx" +} + +module "instance_template" { + source = "../../../../modules/compute-vm" + project_id = module.onprem_project.project_id + name = "nginx-template" + zone = var.zone + tags = ["http-server", "ssh"] + network_interfaces = [{ + network = module.onprem_vpc.self_link + subnetwork = module.onprem_vpc.subnet_self_links["${var.region}/subnet"] + nat = false + addresses = null + }] + boot_disk = { + image = "projects/cos-cloud/global/images/family/cos-stable" + type = "pd-ssd" + size = 10 + } + create_template = true + metadata = { + user-data = module.cos-nginx.cloud_config + } +} + +module "mig" { + source = "../../../../modules/compute-mig" + project_id = module.onprem_project.project_id + location = var.region + name = "mig" + target_size = 2 + instance_template = module.instance_template.template.self_link + named_ports = { + http = 80 + } + health_check_config = { + check_interval_sec = 1 + enable_logging = true + healthy_threshold = 1 + http = { + port_name = "http" + } + timeout_sec = 1 + unhealthy_threshold = 1 + } +} + +module "onprem_ilb_l7" { + source = "../../../../modules/net-ilb-l7" + name = "ilb" + project_id = module.onprem_project.project_id + region = var.region + backend_service_configs = { + default = { + port_name = "http" + backends = [{ + group = module.mig.group_manager.instance_group + }] + } + } + health_check_configs = { + default = { + check_interval_sec = 1 + enable_logging = true + healthy_threshold = 1 + http = { + port_name = "http" + port_specification = "USE_NAMED_PORT" + request_path = "/" + } + timeout_sec = 1 + unhealthy_threshold = 1 + } + } + vpc_config = { + network = module.onprem_vpc.self_link + subnetwork = module.onprem_vpc.subnet_self_links["${var.region}/subnet"] + } + depends_on = [ + module.onprem_vpc.subnets_proxy_only + ] +} diff --git a/tests/blueprints/factories/bigquery_factory/fixture/main.tf b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/outputs.tf similarity index 78% rename from tests/blueprints/factories/bigquery_factory/fixture/main.tf rename to blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/outputs.tf index 75f4fc1c5..3dffa2808 100644 --- a/tests/blueprints/factories/bigquery_factory/fixture/main.tf +++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/outputs.tf @@ -14,10 +14,7 @@ * limitations under the License. */ -module "bq" { - source = "../../../../../blueprints/factories/bigquery-factory/" - - project_id = "test-project" - views_dir = "./views" - tables_dir = "./tables" -} +output "ip_address" { + description = "GLB IP address." + value = module.glb.address +} \ No newline at end of file diff --git a/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/templates/deploy-apiproxy.sh.tpl b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/templates/deploy-apiproxy.sh.tpl new file mode 100644 index 000000000..21a0be14f --- /dev/null +++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/templates/deploy-apiproxy.sh.tpl @@ -0,0 +1,34 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#!/bin/bash + +ORGANIZATION=${organization} +ENVIRONMENT=${environment} + +export TOKEN=$(gcloud auth print-access-token) + +curl -v -X POST \ +-H "Authorization: Bearer $TOKEN" \ +-H "Content-Type:application/octet-stream" \ +-T 'bundle.zip' \ +"https://apigee.googleapis.com/v1/organizations/$ORGANIZATION/apis?name=test&action=import" + +curl -v -X POST \ +-H "Authorization: Bearer $TOKEN" \ +"https://apigee.googleapis.com/v1/organizations/$ORGANIZATION/environments/$ENVIRONMENT/apis/test/revisions/1/deployments" + +curl -v \ +-H "Authorization: Bearer $TOKEN" \ +"https://apigee.googleapis.com/v1/organizations/$ORGANIZATION/environments/$ENVIRONMENT/apis/test/revisions/1/deployments" diff --git a/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/templates/targets/default.xml.tpl b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/templates/targets/default.xml.tpl new file mode 100644 index 000000000..a2290cc4c --- /dev/null +++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/templates/targets/default.xml.tpl @@ -0,0 +1,15 @@ + + + + + + + + + + + + + http://${ip_address} + + \ No newline at end of file diff --git a/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/terraform.tfvars.sample b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/terraform.tfvars.sample new file mode 100644 index 000000000..8c3ff2970 --- /dev/null +++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/terraform.tfvars.sample @@ -0,0 +1,5 @@ +billing_account_id = "12345-12345-123456" +parent = "folders/123456789" +apigee_project_id = "my-apigee-project" +onprem_project_id = "my-onprem-project" +hostname = "test.myorg.org" \ No newline at end of file diff --git a/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/variables.tf b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/variables.tf new file mode 100644 index 000000000..86a720e70 --- /dev/null +++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/variables.tf @@ -0,0 +1,96 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "apigee_project_id" { + description = "Project ID." + type = string + nullable = false +} + +variable "apigee_proxy_only_subnet_ip_cidr_range" { + description = "Subnet IP CIDR range." + type = string + default = "10.2.1.0/24" +} + +variable "apigee_psc_subnet_ip_cidr_range" { + description = "Subnet IP CIDR range." + type = string + default = "10.2.2.0/24" +} + +variable "apigee_runtime_ip_cidr_range" { + description = "Apigee PSA IP CIDR range." + type = string + default = "10.0.4.0/22" +} + +variable "apigee_subnet_ip_cidr_range" { + description = "Subnet IP CIDR range." + type = string + default = "10.2.0.0/24" +} + +variable "apigee_troubleshooting_ip_cidr_range" { + description = "Apigee PSA IP CIDR range." + type = string + default = "10.1.0.0/28" +} + +variable "billing_account_id" { + description = "Parameters for the creation of the new project." + type = string +} + +variable "hostname" { + description = "Host name." + type = string +} + +variable "onprem_project_id" { + description = "Project ID." + type = string + nullable = false +} + +variable "onprem_proxy_only_subnet_ip_cidr_range" { + description = "Subnet IP CIDR range." + type = string + default = "10.1.1.0/24" +} + +variable "onprem_subnet_ip_cidr_range" { + description = "Subnet IP CIDR range." + type = string + default = "10.1.0.0/24" +} + +variable "parent" { + description = "Parent (organizations/organizationID or folders/folderID)." + type = string +} + +variable "region" { + description = "Region." + type = string + default = "europe-west1" +} + +variable "zone" { + description = "Zone." + type = string + default = "europe-west1-c" +} diff --git a/blueprints/cloud-operations/apigee/versions.tf b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/versions.tf similarity index 91% rename from blueprints/cloud-operations/apigee/versions.tf rename to blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/cloud-operations/apigee/versions.tf +++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/vpn.tf b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/vpn.tf new file mode 100644 index 000000000..c39878d19 --- /dev/null +++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/vpn.tf @@ -0,0 +1,117 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module "apigee_vpn" { + source = "../../../../modules/net-vpn-ha" + project_id = module.apigee_project.project_id + network = module.apigee_vpc.self_link + region = var.region + name = "vpn" + router_config = { + name = "router" + asn = 64513 + custom_advertise = { + all_subnets = true + ip_ranges = { + "35.191.0.0/16" = "health checks" + "130.211.0.0/22" = "load balancers" + } + mode = "CUSTOM" + } + } + peer_gateway = { + gcp = module.onprem_vpn.self_link + } + tunnels = { + 0 = { + bgp_peer = { + address = "169.254.2.2" + asn = 64514 + } + bgp_peer_options = null + bgp_session_range = "169.254.2.1/30" + ike_version = 2 + peer_external_gateway_interface = null + router = null + shared_secret = null + vpn_gateway_interface = 0 + } + 1 = { + bgp_peer = { + address = "169.254.2.6" + asn = 64514 + } + bgp_peer_options = null + bgp_session_range = "169.254.2.5/30" + ike_version = 2 + peer_external_gateway_interface = null + router = null + shared_secret = null + vpn_gateway_interface = 1 + } + } +} + +module "onprem_vpn" { + source = "../../../../modules/net-vpn-ha" + project_id = module.onprem_project.project_id + network = module.onprem_vpc.self_link + region = var.region + name = "vpn" + router_config = { + name = "router-${var.region}" + asn = 64514 + custom_advertise = { + all_subnets = false + ip_ranges = { + (var.onprem_subnet_ip_cidr_range) = "subnet range" + } + mode = "CUSTOM" + } + } + peer_gateway = { + gcp = module.apigee_vpn.self_link + } + tunnels = { + 0 = { + bgp_peer = { + address = "169.254.2.1" + asn = 64513 + } + bgp_peer_options = null + bgp_session_range = "169.254.2.2/30" + ike_version = 2 + peer_external_gateway_interface = null + router = null + shared_secret = module.apigee_vpn.random_secret + vpn_gateway_interface = 0 + } + 1 = { + bgp_peer = { + address = "169.254.2.5" + asn = 64513 + } + bgp_peer_options = null + bgp_session_range = "169.254.2.6/30" + ike_version = 2 + peer_external_gateway_interface = null + router = null + shared_secret = module.apigee_vpn.random_secret + vpn_gateway_interface = 1 + } + } +} + diff --git a/blueprints/cloud-operations/adfs/versions.tf b/blueprints/cloud-operations/adfs/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/cloud-operations/adfs/versions.tf +++ b/blueprints/cloud-operations/adfs/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/cloud-operations/apigee/bundle-export.zip b/blueprints/cloud-operations/apigee/bundle-export.zip new file mode 100644 index 000000000..8b090adfc Binary files /dev/null and b/blueprints/cloud-operations/apigee/bundle-export.zip differ diff --git a/blueprints/cloud-operations/apigee/bundle-gcs2bq.zip b/blueprints/cloud-operations/apigee/bundle-gcs2bq.zip new file mode 100644 index 000000000..19a037898 Binary files /dev/null and b/blueprints/cloud-operations/apigee/bundle-gcs2bq.zip differ diff --git a/blueprints/cloud-operations/apigee/functions/export/package-lock.json b/blueprints/cloud-operations/apigee/functions/export/package-lock.json deleted file mode 100644 index 737005bee..000000000 --- a/blueprints/cloud-operations/apigee/functions/export/package-lock.json +++ /dev/null @@ -1,5548 +0,0 @@ -{ - "name": "export", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "export", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@google-cloud/functions-framework": "^3.1.2", - "@google-cloud/logging-bunyan": "^4.2.0", - "bunyan": "^1.8.15", - "express": "^4.18.2", - "google-auth-library": "^8.6.0", - "superagent": "^8.0.3" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "dependencies": { - "@babel/highlight": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.2.tgz", - "integrity": "sha512-afk318kh2uKbo7BEj2QtEi8HVCGrwHUffrYDy7dgVcSa2j9lY3LDjPzcyGdpX7xgm35aWqvciZJ4WKmdF/SxYg==", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@google-cloud/common": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-4.0.3.tgz", - "integrity": "sha512-fUoMo5b8iAKbrYpneIRV3z95AlxVJPrjpevxs4SKoclngWZvTXBSGpNisF5+x5m+oNGve7jfB1e6vNBZBUs7Fw==", - "dependencies": { - "@google-cloud/projectify": "^3.0.0", - "@google-cloud/promisify": "^3.0.0", - "arrify": "^2.0.1", - "duplexify": "^4.1.1", - "ent": "^2.2.0", - "extend": "^3.0.2", - "google-auth-library": "^8.0.2", - "retry-request": "^5.0.0", - "teeny-request": "^8.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@google-cloud/functions-framework": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-3.1.2.tgz", - "integrity": "sha512-pYvEH65/Rqh1JNPdcBmorcV7Xoom2/iOSmbtYza8msro7Inl+qOYxbyMiQfySD2gwAyn38WyWPRqsDRcf/BFLg==", - "dependencies": { - "@types/express": "4.17.13", - "body-parser": "^1.18.3", - "cloudevents": "^6.0.0", - "express": "^4.16.4", - "minimist": "^1.2.5", - "on-finished": "^2.3.0", - "read-pkg-up": "^7.0.1", - "semver": "^7.3.5" - }, - "bin": { - "functions-framework": "build/src/main.js", - "functions-framework-nodejs": "build/src/main.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@google-cloud/logging": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@google-cloud/logging/-/logging-10.3.0.tgz", - "integrity": "sha512-3QNSMci/8mmvLs4Iyb6Z/pFT/lJCDPWGWSRx08+Yi254xpva32pOU0grzgbPYls8SFhDWUQgYr9DGZg+IH0kEQ==", - "dependencies": { - "@google-cloud/common": "^4.0.0", - "@google-cloud/paginator": "^4.0.0", - "@google-cloud/projectify": "^3.0.0", - "@google-cloud/promisify": "^3.0.0", - "arrify": "^2.0.1", - "dot-prop": "^6.0.0", - "eventid": "^2.0.0", - "extend": "^3.0.2", - "gcp-metadata": "^4.0.0", - "google-auth-library": "^8.0.2", - "google-gax": "^3.5.2", - "on-finished": "^2.3.0", - "pumpify": "^2.0.1", - "stream-events": "^1.0.5", - "uuid": "^9.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@google-cloud/logging-bunyan": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@google-cloud/logging-bunyan/-/logging-bunyan-4.2.0.tgz", - "integrity": "sha512-BbzbJguK0sIZedO/0p27N5FDRUkdH2KsiejkoXOTNItU2GI8LweM7dtxihV9m7TcYOXVIxPhirnn8Tu3miq/VA==", - "dependencies": { - "@google-cloud/logging": "^10.2.2", - "google-auth-library": "^8.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "bunyan": "*" - } - }, - "node_modules/@google-cloud/paginator": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-4.0.1.tgz", - "integrity": "sha512-6G1ui6bWhNyHjmbYwavdN7mpVPRBtyDg/bfqBTAlwr413On2TnFNfDxc9UhTJctkgoCDgQXEKiRPLPR9USlkbQ==", - "dependencies": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@google-cloud/projectify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", - "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@google-cloud/promisify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz", - "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/@grpc/grpc-js": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", - "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", - "dependencies": { - "@grpc/proto-loader": "^0.7.0", - "@types/node": ">=12.12.47" - }, - "engines": { - "node": "^8.13.0 || >=10.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.3.tgz", - "integrity": "sha512-5dAvoZwna2Py3Ef96Ux9jIkp3iZ62TUsV00p3wVBPNX5K178UbNi8Q7gQVqwXT1Yq9RejIGG9G2IPEo93T6RcA==", - "dependencies": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^7.0.0", - "yargs": "^16.2.0" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.31", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", - "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "node_modules/@types/linkify-it": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", - "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==" - }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" - }, - "node_modules/@types/markdown-it": { - "version": "12.2.3", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", - "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", - "dependencies": { - "@types/linkify-it": "*", - "@types/mdurl": "*" - } - }, - "node_modules/@types/mdurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", - "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==" - }, - "node_modules/@types/mime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" - }, - "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" - }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==" - }, - "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" - }, - "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" - }, - "node_modules/@types/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", - "dependencies": { - "@types/mime": "*", - "@types/node": "*" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agent-base/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/agent-base/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "engines": { - "node": ">=8" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bignumber.js": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.0.tgz", - "integrity": "sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A==", - "engines": { - "node": "*" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, - "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "node_modules/bunyan": { - "version": "1.8.15", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", - "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", - "engines": [ - "node >=0.10.0" - ], - "bin": { - "bunyan": "bin/bunyan" - }, - "optionalDependencies": { - "dtrace-provider": "~0.8", - "moment": "^2.19.3", - "mv": "~2", - "safe-json-stringify": "~1" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/catharsis": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", - "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", - "dependencies": { - "lodash": "^4.17.15" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/cloudevents": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/cloudevents/-/cloudevents-6.0.2.tgz", - "integrity": "sha512-mn/4EZnAbhfb/TghubK2jPnxYM15JRjf8LnWJtXidiVKi5ZCkd+p9jyBZbL57w7nRm6oFAzJhjxRLsXd/DNaBQ==", - "dependencies": { - "ajv": "^8.11.0", - "ajv-formats": "^2.1.1", - "util": "^0.12.4", - "uuid": "^8.3.2" - } - }, - "node_modules/cloudevents/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/cookiejar": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", - "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/dot-prop": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/dtrace-provider": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", - "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "nan": "^2.14.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/duplexify": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", - "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/ent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==" - }, - "node_modules/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=4.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", - "dependencies": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/eventid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/eventid/-/eventid-2.0.1.tgz", - "integrity": "sha512-sPNTqiMokAvV048P2c9+foqVJzk49o6d4e0D/sq5jog3pw+4kBgyR0gaM1FM7Mx6Kzd9dztesh9oYz1LWWOpzw==", - "dependencies": { - "uuid": "^8.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eventid/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" - }, - "node_modules/fast-text-encoding": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", - "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formidable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", - "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", - "dependencies": { - "dezalgo": "^1.0.4", - "hexoid": "^1.0.0", - "once": "^1.4.0", - "qs": "^6.11.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/gaxios": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", - "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", - "dependencies": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gcp-metadata": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", - "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", - "dependencies": { - "gaxios": "^4.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", - "optional": true, - "dependencies": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/google-auth-library": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.6.0.tgz", - "integrity": "sha512-y6bw1yTWMVgs1vGJwBZ3uu+uIClfgxQfsEVcTNKjQeNQOVwox69+ZUgTeTAzrh+74hBqrk1gWyb9RsQVDI7seg==", - "dependencies": { - "arrify": "^2.0.0", - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "fast-text-encoding": "^1.0.0", - "gaxios": "^5.0.0", - "gcp-metadata": "^5.0.0", - "gtoken": "^6.1.0", - "jws": "^4.0.0", - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/google-auth-library/node_modules/gaxios": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", - "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/google-auth-library/node_modules/gcp-metadata": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.1.tgz", - "integrity": "sha512-jiRJ+Fk7e8FH68Z6TLaqwea307OktJpDjmYnU7/li6ziwvVvU2RlrCyQo5vkdeP94chm0kcSCOOszvmuaioq3g==", - "dependencies": { - "gaxios": "^5.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/google-gax": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.5.2.tgz", - "integrity": "sha512-AyP53w0gHcWlzxm+jSgqCR3Xu4Ld7EpSjhtNBnNhzwwWaIUyphH9kBGNIEH+i4UGkTUXOY29K/Re8EiAvkBRGw==", - "dependencies": { - "@grpc/grpc-js": "~1.7.0", - "@grpc/proto-loader": "^0.7.0", - "@types/long": "^4.0.0", - "abort-controller": "^3.0.0", - "duplexify": "^4.0.0", - "fast-text-encoding": "^1.0.3", - "google-auth-library": "^8.0.2", - "is-stream-ended": "^0.1.4", - "node-fetch": "^2.6.1", - "object-hash": "^3.0.0", - "proto3-json-serializer": "^1.0.0", - "protobufjs": "7.1.2", - "protobufjs-cli": "1.0.2", - "retry-request": "^5.0.0" - }, - "bin": { - "compileProtos": "build/tools/compileProtos.js", - "minifyProtoJson": "build/tools/minify.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/google-p12-pem": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", - "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", - "dependencies": { - "node-forge": "^1.3.1" - }, - "bin": { - "gp12-pem": "build/src/bin/gp12-pem.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" - }, - "node_modules/gtoken": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", - "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", - "dependencies": { - "gaxios": "^5.0.1", - "google-p12-pem": "^4.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/gtoken/node_modules/gaxios": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", - "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hexoid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", - "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", - "engines": { - "node": ">=8" - } - }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http-proxy-agent/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/http-proxy-agent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-stream-ended": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", - "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" - }, - "node_modules/is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js2xmlparser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", - "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", - "dependencies": { - "xmlcreate": "^2.0.4" - } - }, - "node_modules/jsdoc": { - "version": "3.6.11", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.11.tgz", - "integrity": "sha512-8UCU0TYeIYD9KeLzEcAu2q8N/mx9O3phAGl32nmHlE0LpaJL71mMkP4d+QE5zWfNt50qheHtOZ0qoxVrsX5TUg==", - "dependencies": { - "@babel/parser": "^7.9.4", - "@types/markdown-it": "^12.2.3", - "bluebird": "^3.7.2", - "catharsis": "^0.9.0", - "escape-string-regexp": "^2.0.0", - "js2xmlparser": "^4.0.2", - "klaw": "^3.0.0", - "markdown-it": "^12.3.2", - "markdown-it-anchor": "^8.4.1", - "marked": "^4.0.10", - "mkdirp": "^1.0.4", - "requizzle": "^0.2.3", - "strip-json-comments": "^3.1.0", - "taffydb": "2.6.2", - "underscore": "~1.13.2" - }, - "bin": { - "jsdoc": "jsdoc.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/jsdoc/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "dependencies": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/klaw": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", - "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", - "dependencies": { - "graceful-fs": "^4.1.9" - } - }, - "node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "node_modules/linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", - "dependencies": { - "uc.micro": "^1.0.1" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" - }, - "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/markdown-it": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", - "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", - "dependencies": { - "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - }, - "bin": { - "markdown-it": "bin/markdown-it.js" - } - }, - "node_modules/markdown-it-anchor": { - "version": "8.6.5", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.5.tgz", - "integrity": "sha512-PI1qEHHkTNWT+X6Ip9w+paonfIQ+QZP9sCeMYi47oqhH+EsW8CrJ8J7CzV19QVOj6il8ATGbK2nTECj22ZHGvQ==", - "peerDependencies": { - "@types/markdown-it": "*", - "markdown-it": "*" - } - }, - "node_modules/marked": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.2.tgz", - "integrity": "sha512-JjBTFTAvuTgANXx82a5vzK9JLSMoV6V3LBVn4Uhdso6t7vXrGx7g1Cd2r6NYSsxrYbQGFCMqBDhFHyK5q2UvcQ==", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "optional": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", - "optional": true, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/mv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", - "optional": true, - "dependencies": { - "mkdirp": "~0.5.1", - "ncp": "~2.0.0", - "rimraf": "~2.4.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", - "optional": true - }, - "node_modules/ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", - "optional": true, - "bin": { - "ncp": "bin/ncp" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/proto3-json-serializer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", - "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", - "dependencies": { - "protobufjs": "^7.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/protobufjs": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", - "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/protobufjs-cli": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.0.2.tgz", - "integrity": "sha512-cz9Pq9p/Zs7okc6avH20W7QuyjTclwJPgqXG11jNaulfS3nbVisID8rC+prfgq0gbZE0w9LBFd1OKFF03kgFzg==", - "dependencies": { - "chalk": "^4.0.0", - "escodegen": "^1.13.0", - "espree": "^9.0.0", - "estraverse": "^5.1.0", - "glob": "^8.0.0", - "jsdoc": "^3.6.3", - "minimist": "^1.2.0", - "semver": "^7.1.2", - "tmp": "^0.2.1", - "uglify-js": "^3.7.7" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "protobufjs": "^7.0.0" - } - }, - "node_modules/protobufjs-cli/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/protobufjs-cli/node_modules/glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/protobufjs-cli/node_modules/minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/protobufjs/node_modules/long": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", - "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/pumpify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", - "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", - "dependencies": { - "duplexify": "^4.1.1", - "inherits": "^2.0.3", - "pump": "^3.0.0" - } - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requizzle": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz", - "integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ==", - "dependencies": { - "lodash": "^4.17.14" - } - }, - "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/retry-request": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", - "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", - "dependencies": { - "debug": "^4.1.1", - "extend": "^3.0.2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/retry-request/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/retry-request/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/rimraf": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", - "optional": true, - "dependencies": { - "glob": "^6.0.1" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safe-json-stringify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", - "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", - "optional": true - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", - "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==" - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stream-events": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", - "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", - "dependencies": { - "stubs": "^3.0.0" - } - }, - "node_modules/stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==" - }, - "node_modules/superagent": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.3.tgz", - "integrity": "sha512-oBC+aNsCjzzjmO5AOPBPFS+Z7HPzlx+DQr/aHwM08kI+R24gsDmAS1LMfza1fK+P+SKlTAoNZpOvooE/pRO1HA==", - "dependencies": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.3", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.0.1", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=6.4.0 <13 || >=14" - } - }, - "node_modules/superagent/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/superagent/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/superagent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/taffydb": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", - "integrity": "sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA==" - }, - "node_modules/teeny-request": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.2.tgz", - "integrity": "sha512-34pe0a4zASseXZCKdeTiIZqSKA8ETHb1EwItZr01PAR3CLPojeAKgSjzeNS4373gi59hNulyDrPKEbh2zO9sCg==", - "dependencies": { - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.1", - "stream-events": "^1.0.5", - "uuid": "^9.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, - "node_modules/tmp/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/tmp/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" - }, - "node_modules/uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/xmlcreate": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", - "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "engines": { - "node": ">=10" - } - } - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "requires": { - "@babel/highlight": "^7.18.6" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" - }, - "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/parser": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.2.tgz", - "integrity": "sha512-afk318kh2uKbo7BEj2QtEi8HVCGrwHUffrYDy7dgVcSa2j9lY3LDjPzcyGdpX7xgm35aWqvciZJ4WKmdF/SxYg==" - }, - "@google-cloud/common": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-4.0.3.tgz", - "integrity": "sha512-fUoMo5b8iAKbrYpneIRV3z95AlxVJPrjpevxs4SKoclngWZvTXBSGpNisF5+x5m+oNGve7jfB1e6vNBZBUs7Fw==", - "requires": { - "@google-cloud/projectify": "^3.0.0", - "@google-cloud/promisify": "^3.0.0", - "arrify": "^2.0.1", - "duplexify": "^4.1.1", - "ent": "^2.2.0", - "extend": "^3.0.2", - "google-auth-library": "^8.0.2", - "retry-request": "^5.0.0", - "teeny-request": "^8.0.0" - } - }, - "@google-cloud/functions-framework": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-3.1.2.tgz", - "integrity": "sha512-pYvEH65/Rqh1JNPdcBmorcV7Xoom2/iOSmbtYza8msro7Inl+qOYxbyMiQfySD2gwAyn38WyWPRqsDRcf/BFLg==", - "requires": { - "@types/express": "4.17.13", - "body-parser": "^1.18.3", - "cloudevents": "^6.0.0", - "express": "^4.16.4", - "minimist": "^1.2.5", - "on-finished": "^2.3.0", - "read-pkg-up": "^7.0.1", - "semver": "^7.3.5" - } - }, - "@google-cloud/logging": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@google-cloud/logging/-/logging-10.3.0.tgz", - "integrity": "sha512-3QNSMci/8mmvLs4Iyb6Z/pFT/lJCDPWGWSRx08+Yi254xpva32pOU0grzgbPYls8SFhDWUQgYr9DGZg+IH0kEQ==", - "requires": { - "@google-cloud/common": "^4.0.0", - "@google-cloud/paginator": "^4.0.0", - "@google-cloud/projectify": "^3.0.0", - "@google-cloud/promisify": "^3.0.0", - "arrify": "^2.0.1", - "dot-prop": "^6.0.0", - "eventid": "^2.0.0", - "extend": "^3.0.2", - "gcp-metadata": "^4.0.0", - "google-auth-library": "^8.0.2", - "google-gax": "^3.5.2", - "on-finished": "^2.3.0", - "pumpify": "^2.0.1", - "stream-events": "^1.0.5", - "uuid": "^9.0.0" - } - }, - "@google-cloud/logging-bunyan": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@google-cloud/logging-bunyan/-/logging-bunyan-4.2.0.tgz", - "integrity": "sha512-BbzbJguK0sIZedO/0p27N5FDRUkdH2KsiejkoXOTNItU2GI8LweM7dtxihV9m7TcYOXVIxPhirnn8Tu3miq/VA==", - "requires": { - "@google-cloud/logging": "^10.2.2", - "google-auth-library": "^8.0.2" - } - }, - "@google-cloud/paginator": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-4.0.1.tgz", - "integrity": "sha512-6G1ui6bWhNyHjmbYwavdN7mpVPRBtyDg/bfqBTAlwr413On2TnFNfDxc9UhTJctkgoCDgQXEKiRPLPR9USlkbQ==", - "requires": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - } - }, - "@google-cloud/projectify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", - "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==" - }, - "@google-cloud/promisify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz", - "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==" - }, - "@grpc/grpc-js": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", - "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", - "requires": { - "@grpc/proto-loader": "^0.7.0", - "@types/node": ">=12.12.47" - } - }, - "@grpc/proto-loader": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.3.tgz", - "integrity": "sha512-5dAvoZwna2Py3Ef96Ux9jIkp3iZ62TUsV00p3wVBPNX5K178UbNi8Q7gQVqwXT1Yq9RejIGG9G2IPEo93T6RcA==", - "requires": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^7.0.0", - "yargs": "^16.2.0" - } - }, - "@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" - }, - "@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" - }, - "@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "requires": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" - }, - "@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" - }, - "@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" - }, - "@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" - }, - "@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" - }, - "@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==" - }, - "@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "requires": { - "@types/node": "*" - } - }, - "@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.31", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", - "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "@types/linkify-it": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", - "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==" - }, - "@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" - }, - "@types/markdown-it": { - "version": "12.2.3", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", - "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", - "requires": { - "@types/linkify-it": "*", - "@types/mdurl": "*" - } - }, - "@types/mdurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", - "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==" - }, - "@types/mime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" - }, - "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" - }, - "@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==" - }, - "@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" - }, - "@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" - }, - "@types/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", - "requires": { - "@types/mime": "*", - "@types/node": "*" - } - }, - "abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "requires": { - "event-target-shim": "^5.0.0" - } - }, - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, - "acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==" - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "requires": {} - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "requires": { - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "requires": { - "ajv": "^8.0.0" - } - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "bignumber.js": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.0.tgz", - "integrity": "sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A==" - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, - "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "bunyan": { - "version": "1.8.15", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", - "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", - "requires": { - "dtrace-provider": "~0.8", - "moment": "^2.19.3", - "mv": "~2", - "safe-json-stringify": "~1" - } - }, - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "catharsis": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", - "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", - "requires": { - "lodash": "^4.17.15" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "cloudevents": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/cloudevents/-/cloudevents-6.0.2.tgz", - "integrity": "sha512-mn/4EZnAbhfb/TghubK2jPnxYM15JRjf8LnWJtXidiVKi5ZCkd+p9jyBZbL57w7nRm6oFAzJhjxRLsXd/DNaBQ==", - "requires": { - "ajv": "^8.11.0", - "ajv-formats": "^2.1.1", - "util": "^0.12.4", - "uuid": "^8.3.2" - }, - "dependencies": { - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - } - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "requires": { - "safe-buffer": "5.2.1" - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "cookiejar": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", - "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" - }, - "dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "requires": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "dot-prop": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", - "requires": { - "is-obj": "^2.0.0" - } - }, - "dtrace-provider": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", - "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", - "optional": true, - "requires": { - "nan": "^2.14.0" - } - }, - "duplexify": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", - "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", - "requires": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "ent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==" - }, - "entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==" - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" - }, - "escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "requires": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" - } - } - }, - "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==" - }, - "espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" - }, - "event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" - }, - "eventid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/eventid/-/eventid-2.0.1.tgz", - "integrity": "sha512-sPNTqiMokAvV048P2c9+foqVJzk49o6d4e0D/sq5jog3pw+4kBgyR0gaM1FM7Mx6Kzd9dztesh9oYz1LWWOpzw==", - "requires": { - "uuid": "^8.0.0" - }, - "dependencies": { - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - } - } - }, - "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "requires": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" - }, - "fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" - }, - "fast-text-encoding": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", - "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" - }, - "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "requires": { - "is-callable": "^1.1.3" - } - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "formidable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", - "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", - "requires": { - "dezalgo": "^1.0.4", - "hexoid": "^1.0.0", - "once": "^1.4.0", - "qs": "^6.11.0" - } - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "gaxios": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", - "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - } - }, - "gcp-metadata": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", - "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", - "requires": { - "gaxios": "^4.0.0", - "json-bigint": "^1.0.0" - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - } - }, - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", - "optional": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "google-auth-library": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.6.0.tgz", - "integrity": "sha512-y6bw1yTWMVgs1vGJwBZ3uu+uIClfgxQfsEVcTNKjQeNQOVwox69+ZUgTeTAzrh+74hBqrk1gWyb9RsQVDI7seg==", - "requires": { - "arrify": "^2.0.0", - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "fast-text-encoding": "^1.0.0", - "gaxios": "^5.0.0", - "gcp-metadata": "^5.0.0", - "gtoken": "^6.1.0", - "jws": "^4.0.0", - "lru-cache": "^6.0.0" - }, - "dependencies": { - "gaxios": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", - "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", - "requires": { - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - } - }, - "gcp-metadata": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.1.tgz", - "integrity": "sha512-jiRJ+Fk7e8FH68Z6TLaqwea307OktJpDjmYnU7/li6ziwvVvU2RlrCyQo5vkdeP94chm0kcSCOOszvmuaioq3g==", - "requires": { - "gaxios": "^5.0.0", - "json-bigint": "^1.0.0" - } - } - } - }, - "google-gax": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.5.2.tgz", - "integrity": "sha512-AyP53w0gHcWlzxm+jSgqCR3Xu4Ld7EpSjhtNBnNhzwwWaIUyphH9kBGNIEH+i4UGkTUXOY29K/Re8EiAvkBRGw==", - "requires": { - "@grpc/grpc-js": "~1.7.0", - "@grpc/proto-loader": "^0.7.0", - "@types/long": "^4.0.0", - "abort-controller": "^3.0.0", - "duplexify": "^4.0.0", - "fast-text-encoding": "^1.0.3", - "google-auth-library": "^8.0.2", - "is-stream-ended": "^0.1.4", - "node-fetch": "^2.6.1", - "object-hash": "^3.0.0", - "proto3-json-serializer": "^1.0.0", - "protobufjs": "7.1.2", - "protobufjs-cli": "1.0.2", - "retry-request": "^5.0.0" - } - }, - "google-p12-pem": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", - "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", - "requires": { - "node-forge": "^1.3.1" - } - }, - "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" - }, - "gtoken": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", - "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", - "requires": { - "gaxios": "^5.0.1", - "google-p12-pem": "^4.0.0", - "jws": "^4.0.0" - }, - "dependencies": { - "gaxios": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", - "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", - "requires": { - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - } - } - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "requires": { - "has-symbols": "^1.0.2" - } - }, - "hexoid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", - "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==" - }, - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "requires": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "requires": { - "agent-base": "6", - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, - "is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" - }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "requires": { - "has": "^1.0.3" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" - }, - "is-stream-ended": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", - "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" - }, - "is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "js2xmlparser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", - "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", - "requires": { - "xmlcreate": "^2.0.4" - } - }, - "jsdoc": { - "version": "3.6.11", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.11.tgz", - "integrity": "sha512-8UCU0TYeIYD9KeLzEcAu2q8N/mx9O3phAGl32nmHlE0LpaJL71mMkP4d+QE5zWfNt50qheHtOZ0qoxVrsX5TUg==", - "requires": { - "@babel/parser": "^7.9.4", - "@types/markdown-it": "^12.2.3", - "bluebird": "^3.7.2", - "catharsis": "^0.9.0", - "escape-string-regexp": "^2.0.0", - "js2xmlparser": "^4.0.2", - "klaw": "^3.0.0", - "markdown-it": "^12.3.2", - "markdown-it-anchor": "^8.4.1", - "marked": "^4.0.10", - "mkdirp": "^1.0.4", - "requizzle": "^0.2.3", - "strip-json-comments": "^3.1.0", - "taffydb": "2.6.2", - "underscore": "~1.13.2" - }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - } - } - }, - "json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "requires": { - "bignumber.js": "^9.0.0" - } - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "klaw": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", - "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", - "requires": { - "graceful-fs": "^4.1.9" - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", - "requires": { - "uc.micro": "^1.0.1" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" - }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "markdown-it": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", - "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", - "requires": { - "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - } - }, - "markdown-it-anchor": { - "version": "8.6.5", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.5.tgz", - "integrity": "sha512-PI1qEHHkTNWT+X6Ip9w+paonfIQ+QZP9sCeMYi47oqhH+EsW8CrJ8J7CzV19QVOj6il8ATGbK2nTECj22ZHGvQ==", - "requires": {} - }, - "marked": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.2.tgz", - "integrity": "sha512-JjBTFTAvuTgANXx82a5vzK9JLSMoV6V3LBVn4Uhdso6t7vXrGx7g1Cd2r6NYSsxrYbQGFCMqBDhFHyK5q2UvcQ==" - }, - "mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" - }, - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "optional": true, - "requires": { - "minimist": "^1.2.6" - } - }, - "moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", - "optional": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "mv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", - "optional": true, - "requires": { - "mkdirp": "~0.5.1", - "ncp": "~2.0.0", - "rimraf": "~2.4.0" - } - }, - "nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", - "optional": true - }, - "ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", - "optional": true - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" - }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - } - } - }, - "object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" - }, - "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" - }, - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "requires": { - "ee-first": "1.1.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==" - }, - "proto3-json-serializer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", - "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", - "requires": { - "protobufjs": "^7.0.0" - } - }, - "protobufjs": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", - "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", - "requires": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "dependencies": { - "long": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", - "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" - } - } - }, - "protobufjs-cli": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.0.2.tgz", - "integrity": "sha512-cz9Pq9p/Zs7okc6avH20W7QuyjTclwJPgqXG11jNaulfS3nbVisID8rC+prfgq0gbZE0w9LBFd1OKFF03kgFzg==", - "requires": { - "chalk": "^4.0.0", - "escodegen": "^1.13.0", - "espree": "^9.0.0", - "estraverse": "^5.1.0", - "glob": "^8.0.0", - "jsdoc": "^3.6.3", - "minimist": "^1.2.0", - "semver": "^7.1.2", - "tmp": "^0.2.1", - "uglify-js": "^3.7.7" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - }, - "glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - } - }, - "minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, - "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", - "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", - "requires": { - "duplexify": "^4.1.1", - "inherits": "^2.0.3", - "pump": "^3.0.0" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "requires": { - "side-channel": "^1.0.4" - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" - }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" - }, - "requizzle": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz", - "integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ==", - "requires": { - "lodash": "^4.17.14" - } - }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "retry-request": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", - "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", - "requires": { - "debug": "^4.1.1", - "extend": "^3.0.2" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "rimraf": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", - "optional": true, - "requires": { - "glob": "^6.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safe-json-stringify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", - "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "requires": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "dependencies": { - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, - "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - } - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true - }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", - "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==" - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - }, - "stream-events": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", - "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", - "requires": { - "stubs": "^3.0.0" - } - }, - "stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" - }, - "stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==" - }, - "superagent": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.3.tgz", - "integrity": "sha512-oBC+aNsCjzzjmO5AOPBPFS+Z7HPzlx+DQr/aHwM08kI+R24gsDmAS1LMfza1fK+P+SKlTAoNZpOvooE/pRO1HA==", - "requires": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.3", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.0.1", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" - }, - "taffydb": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", - "integrity": "sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA==" - }, - "teeny-request": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.2.tgz", - "integrity": "sha512-34pe0a4zASseXZCKdeTiIZqSKA8ETHb1EwItZr01PAR3CLPojeAKgSjzeNS4373gi59hNulyDrPKEbh2zO9sCg==", - "requires": { - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.1", - "stream-events": "^1.0.5", - "uuid": "^9.0.0" - } - }, - "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "requires": { - "rimraf": "^3.0.0" - }, - "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" - }, - "uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==" - }, - "underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "requires": { - "punycode": "^2.1.0" - } - }, - "util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "requires": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" - }, - "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "xmlcreate": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", - "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==" - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" - } - } -} diff --git a/blueprints/cloud-operations/apigee/functions/gcs2bq/package-lock.json b/blueprints/cloud-operations/apigee/functions/gcs2bq/package-lock.json deleted file mode 100644 index c5a7620b0..000000000 --- a/blueprints/cloud-operations/apigee/functions/gcs2bq/package-lock.json +++ /dev/null @@ -1,5675 +0,0 @@ -{ - "name": "gcs2bq", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "gcs2bq", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@google-cloud/bigquery": "^6.0.3", - "@google-cloud/functions-framework": "^3.1.2", - "@google-cloud/logging-bunyan": "^4.2.0", - "@google-cloud/storage": "^6.7.0", - "bunyan": "^1.8.15" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "dependencies": { - "@babel/highlight": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.2.tgz", - "integrity": "sha512-afk318kh2uKbo7BEj2QtEi8HVCGrwHUffrYDy7dgVcSa2j9lY3LDjPzcyGdpX7xgm35aWqvciZJ4WKmdF/SxYg==", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@google-cloud/bigquery": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@google-cloud/bigquery/-/bigquery-6.0.3.tgz", - "integrity": "sha512-BP464228S9dqDCb4dR99h9D8+N498YZi/AZvoOJUaieg2H6qbiYBE1xlYuaMvyV1WEQT/2/yZTCJnCo5WiaY0Q==", - "dependencies": { - "@google-cloud/common": "^4.0.0", - "@google-cloud/paginator": "^4.0.0", - "@google-cloud/promisify": "^3.0.0", - "arrify": "^2.0.1", - "big.js": "^6.0.0", - "duplexify": "^4.0.0", - "extend": "^3.0.2", - "is": "^3.3.0", - "p-event": "^4.1.0", - "readable-stream": "^4.0.0", - "stream-events": "^1.0.5", - "uuid": "^8.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@google-cloud/bigquery/node_modules/@google-cloud/paginator": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-4.0.1.tgz", - "integrity": "sha512-6G1ui6bWhNyHjmbYwavdN7mpVPRBtyDg/bfqBTAlwr413On2TnFNfDxc9UhTJctkgoCDgQXEKiRPLPR9USlkbQ==", - "dependencies": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@google-cloud/bigquery/node_modules/readable-stream": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.2.0.tgz", - "integrity": "sha512-gJrBHsaI3lgBoGMW/jHZsQ/o/TIWiu5ENCJG1BB7fuCKzpFM8GaS2UoBVt9NO+oI+3FcrBNbUkl3ilDe09aY4A==", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@google-cloud/common": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-4.0.3.tgz", - "integrity": "sha512-fUoMo5b8iAKbrYpneIRV3z95AlxVJPrjpevxs4SKoclngWZvTXBSGpNisF5+x5m+oNGve7jfB1e6vNBZBUs7Fw==", - "dependencies": { - "@google-cloud/projectify": "^3.0.0", - "@google-cloud/promisify": "^3.0.0", - "arrify": "^2.0.1", - "duplexify": "^4.1.1", - "ent": "^2.2.0", - "extend": "^3.0.2", - "google-auth-library": "^8.0.2", - "retry-request": "^5.0.0", - "teeny-request": "^8.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@google-cloud/functions-framework": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-3.1.2.tgz", - "integrity": "sha512-pYvEH65/Rqh1JNPdcBmorcV7Xoom2/iOSmbtYza8msro7Inl+qOYxbyMiQfySD2gwAyn38WyWPRqsDRcf/BFLg==", - "dependencies": { - "@types/express": "4.17.13", - "body-parser": "^1.18.3", - "cloudevents": "^6.0.0", - "express": "^4.16.4", - "minimist": "^1.2.5", - "on-finished": "^2.3.0", - "read-pkg-up": "^7.0.1", - "semver": "^7.3.5" - }, - "bin": { - "functions-framework": "build/src/main.js", - "functions-framework-nodejs": "build/src/main.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@google-cloud/logging": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@google-cloud/logging/-/logging-10.3.0.tgz", - "integrity": "sha512-3QNSMci/8mmvLs4Iyb6Z/pFT/lJCDPWGWSRx08+Yi254xpva32pOU0grzgbPYls8SFhDWUQgYr9DGZg+IH0kEQ==", - "dependencies": { - "@google-cloud/common": "^4.0.0", - "@google-cloud/paginator": "^4.0.0", - "@google-cloud/projectify": "^3.0.0", - "@google-cloud/promisify": "^3.0.0", - "arrify": "^2.0.1", - "dot-prop": "^6.0.0", - "eventid": "^2.0.0", - "extend": "^3.0.2", - "gcp-metadata": "^4.0.0", - "google-auth-library": "^8.0.2", - "google-gax": "^3.5.2", - "on-finished": "^2.3.0", - "pumpify": "^2.0.1", - "stream-events": "^1.0.5", - "uuid": "^9.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@google-cloud/logging-bunyan": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@google-cloud/logging-bunyan/-/logging-bunyan-4.2.0.tgz", - "integrity": "sha512-BbzbJguK0sIZedO/0p27N5FDRUkdH2KsiejkoXOTNItU2GI8LweM7dtxihV9m7TcYOXVIxPhirnn8Tu3miq/VA==", - "dependencies": { - "@google-cloud/logging": "^10.2.2", - "google-auth-library": "^8.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "bunyan": "*" - } - }, - "node_modules/@google-cloud/logging/node_modules/@google-cloud/paginator": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-4.0.1.tgz", - "integrity": "sha512-6G1ui6bWhNyHjmbYwavdN7mpVPRBtyDg/bfqBTAlwr413On2TnFNfDxc9UhTJctkgoCDgQXEKiRPLPR9USlkbQ==", - "dependencies": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@google-cloud/logging/node_modules/gaxios": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", - "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", - "dependencies": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@google-cloud/logging/node_modules/gcp-metadata": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", - "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", - "dependencies": { - "gaxios": "^4.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@google-cloud/logging/node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@google-cloud/paginator": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", - "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", - "dependencies": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@google-cloud/projectify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", - "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@google-cloud/promisify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz", - "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/@google-cloud/storage": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.7.0.tgz", - "integrity": "sha512-iEit3dvUhGQV3pPC8aci/Y+F6K2QJ/UvcXhymj8gnO8IYQfZSZvFf361yX4BWNUlbHzanUQVQdF9RvgEM8fwpw==", - "dependencies": { - "@google-cloud/paginator": "^3.0.7", - "@google-cloud/projectify": "^3.0.0", - "@google-cloud/promisify": "^3.0.0", - "abort-controller": "^3.0.0", - "async-retry": "^1.3.3", - "compressible": "^2.0.12", - "duplexify": "^4.0.0", - "ent": "^2.2.0", - "extend": "^3.0.2", - "gaxios": "^5.0.0", - "google-auth-library": "^8.0.1", - "mime": "^3.0.0", - "mime-types": "^2.0.8", - "p-limit": "^3.0.1", - "retry-request": "^5.0.0", - "teeny-request": "^8.0.0", - "uuid": "^8.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@grpc/grpc-js": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", - "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", - "dependencies": { - "@grpc/proto-loader": "^0.7.0", - "@types/node": ">=12.12.47" - }, - "engines": { - "node": "^8.13.0 || >=10.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.3.tgz", - "integrity": "sha512-5dAvoZwna2Py3Ef96Ux9jIkp3iZ62TUsV00p3wVBPNX5K178UbNi8Q7gQVqwXT1Yq9RejIGG9G2IPEo93T6RcA==", - "dependencies": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^7.0.0", - "yargs": "^16.2.0" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.31", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", - "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "node_modules/@types/linkify-it": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", - "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==" - }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" - }, - "node_modules/@types/markdown-it": { - "version": "12.2.3", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", - "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", - "dependencies": { - "@types/linkify-it": "*", - "@types/mdurl": "*" - } - }, - "node_modules/@types/mdurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", - "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==" - }, - "node_modules/@types/mime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" - }, - "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" - }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==" - }, - "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" - }, - "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" - }, - "node_modules/@types/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", - "dependencies": { - "@types/mime": "*", - "@types/node": "*" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ajv": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", - "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "engines": { - "node": ">=8" - } - }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", - "dependencies": { - "retry": "0.13.1" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/big.js": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.1.tgz", - "integrity": "sha512-bCtHMwL9LeDIozFn+oNhhFoq+yQ3BNdnsLSASUxLciOb1vgvpHsIO1dsENiGMgbb4SkP5TrzWzRiLddn8ahVOQ==", - "engines": { - "node": "*" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/bigjs" - } - }, - "node_modules/bignumber.js": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.0.tgz", - "integrity": "sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A==", - "engines": { - "node": "*" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, - "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "node_modules/bunyan": { - "version": "1.8.15", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", - "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", - "engines": [ - "node >=0.10.0" - ], - "bin": { - "bunyan": "bin/bunyan" - }, - "optionalDependencies": { - "dtrace-provider": "~0.8", - "moment": "^2.19.3", - "mv": "~2", - "safe-json-stringify": "~1" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/catharsis": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", - "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", - "dependencies": { - "lodash": "^4.17.15" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/cloudevents": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/cloudevents/-/cloudevents-6.0.3.tgz", - "integrity": "sha512-ADEHAv2KShH/cDIy2GP+npFz3R6Fu/UCsUO/j4kYA9VqN4yhGdF+Zg6wmjeq6qlUvlaKdrVBwgZuH/w57IsyGQ==", - "dependencies": { - "ajv": "^8.11.0", - "ajv-formats": "^2.1.1", - "util": "^0.12.4", - "uuid": "^8.3.2" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dot-prop": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/dtrace-provider": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", - "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "nan": "^2.14.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/duplexify": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", - "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/ent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==" - }, - "node_modules/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=4.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", - "dependencies": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/eventid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/eventid/-/eventid-2.0.1.tgz", - "integrity": "sha512-sPNTqiMokAvV048P2c9+foqVJzk49o6d4e0D/sq5jog3pw+4kBgyR0gaM1FM7Mx6Kzd9dztesh9oYz1LWWOpzw==", - "dependencies": { - "uuid": "^8.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" - }, - "node_modules/fast-text-encoding": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", - "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/gaxios": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", - "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/gcp-metadata": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.1.tgz", - "integrity": "sha512-jiRJ+Fk7e8FH68Z6TLaqwea307OktJpDjmYnU7/li6ziwvVvU2RlrCyQo5vkdeP94chm0kcSCOOszvmuaioq3g==", - "dependencies": { - "gaxios": "^5.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", - "optional": true, - "dependencies": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/google-auth-library": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.6.0.tgz", - "integrity": "sha512-y6bw1yTWMVgs1vGJwBZ3uu+uIClfgxQfsEVcTNKjQeNQOVwox69+ZUgTeTAzrh+74hBqrk1gWyb9RsQVDI7seg==", - "dependencies": { - "arrify": "^2.0.0", - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "fast-text-encoding": "^1.0.0", - "gaxios": "^5.0.0", - "gcp-metadata": "^5.0.0", - "gtoken": "^6.1.0", - "jws": "^4.0.0", - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/google-gax": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.5.2.tgz", - "integrity": "sha512-AyP53w0gHcWlzxm+jSgqCR3Xu4Ld7EpSjhtNBnNhzwwWaIUyphH9kBGNIEH+i4UGkTUXOY29K/Re8EiAvkBRGw==", - "dependencies": { - "@grpc/grpc-js": "~1.7.0", - "@grpc/proto-loader": "^0.7.0", - "@types/long": "^4.0.0", - "abort-controller": "^3.0.0", - "duplexify": "^4.0.0", - "fast-text-encoding": "^1.0.3", - "google-auth-library": "^8.0.2", - "is-stream-ended": "^0.1.4", - "node-fetch": "^2.6.1", - "object-hash": "^3.0.0", - "proto3-json-serializer": "^1.0.0", - "protobufjs": "7.1.2", - "protobufjs-cli": "1.0.2", - "retry-request": "^5.0.0" - }, - "bin": { - "compileProtos": "build/tools/compileProtos.js", - "minifyProtoJson": "build/tools/minify.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/google-p12-pem": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", - "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", - "dependencies": { - "node-forge": "^1.3.1" - }, - "bin": { - "gp12-pem": "build/src/bin/gp12-pem.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" - }, - "node_modules/gtoken": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", - "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", - "dependencies": { - "gaxios": "^5.0.1", - "google-p12-pem": "^4.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", - "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==", - "engines": { - "node": "*" - } - }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-stream-ended": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", - "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" - }, - "node_modules/is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js2xmlparser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", - "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", - "dependencies": { - "xmlcreate": "^2.0.4" - } - }, - "node_modules/jsdoc": { - "version": "3.6.11", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.11.tgz", - "integrity": "sha512-8UCU0TYeIYD9KeLzEcAu2q8N/mx9O3phAGl32nmHlE0LpaJL71mMkP4d+QE5zWfNt50qheHtOZ0qoxVrsX5TUg==", - "dependencies": { - "@babel/parser": "^7.9.4", - "@types/markdown-it": "^12.2.3", - "bluebird": "^3.7.2", - "catharsis": "^0.9.0", - "escape-string-regexp": "^2.0.0", - "js2xmlparser": "^4.0.2", - "klaw": "^3.0.0", - "markdown-it": "^12.3.2", - "markdown-it-anchor": "^8.4.1", - "marked": "^4.0.10", - "mkdirp": "^1.0.4", - "requizzle": "^0.2.3", - "strip-json-comments": "^3.1.0", - "taffydb": "2.6.2", - "underscore": "~1.13.2" - }, - "bin": { - "jsdoc": "jsdoc.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/jsdoc/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "dependencies": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/klaw": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", - "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", - "dependencies": { - "graceful-fs": "^4.1.9" - } - }, - "node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "node_modules/linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", - "dependencies": { - "uc.micro": "^1.0.1" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" - }, - "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/markdown-it": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", - "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", - "dependencies": { - "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - }, - "bin": { - "markdown-it": "bin/markdown-it.js" - } - }, - "node_modules/markdown-it-anchor": { - "version": "8.6.5", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.5.tgz", - "integrity": "sha512-PI1qEHHkTNWT+X6Ip9w+paonfIQ+QZP9sCeMYi47oqhH+EsW8CrJ8J7CzV19QVOj6il8ATGbK2nTECj22ZHGvQ==", - "peerDependencies": { - "@types/markdown-it": "*", - "markdown-it": "*" - } - }, - "node_modules/marked": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.2.tgz", - "integrity": "sha512-JjBTFTAvuTgANXx82a5vzK9JLSMoV6V3LBVn4Uhdso6t7vXrGx7g1Cd2r6NYSsxrYbQGFCMqBDhFHyK5q2UvcQ==", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "optional": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", - "optional": true, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/mv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", - "optional": true, - "dependencies": { - "mkdirp": "~0.5.1", - "ncp": "~2.0.0", - "rimraf": "~2.4.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", - "optional": true - }, - "node_modules/ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", - "optional": true, - "bin": { - "ncp": "bin/ncp" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-event": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", - "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", - "dependencies": { - "p-timeout": "^3.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "engines": { - "node": ">=4" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/proto3-json-serializer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", - "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", - "dependencies": { - "protobufjs": "^7.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/protobufjs": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", - "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/protobufjs-cli": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.0.2.tgz", - "integrity": "sha512-cz9Pq9p/Zs7okc6avH20W7QuyjTclwJPgqXG11jNaulfS3nbVisID8rC+prfgq0gbZE0w9LBFd1OKFF03kgFzg==", - "dependencies": { - "chalk": "^4.0.0", - "escodegen": "^1.13.0", - "espree": "^9.0.0", - "estraverse": "^5.1.0", - "glob": "^8.0.0", - "jsdoc": "^3.6.3", - "minimist": "^1.2.0", - "semver": "^7.1.2", - "tmp": "^0.2.1", - "uglify-js": "^3.7.7" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "protobufjs": "^7.0.0" - } - }, - "node_modules/protobufjs-cli/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/protobufjs-cli/node_modules/glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/protobufjs-cli/node_modules/minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/protobufjs/node_modules/long": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", - "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/pumpify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", - "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", - "dependencies": { - "duplexify": "^4.1.1", - "inherits": "^2.0.3", - "pump": "^3.0.0" - } - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requizzle": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz", - "integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ==", - "dependencies": { - "lodash": "^4.17.14" - } - }, - "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/retry-request": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", - "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", - "dependencies": { - "debug": "^4.1.1", - "extend": "^3.0.2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/rimraf": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", - "optional": true, - "dependencies": { - "glob": "^6.0.1" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safe-json-stringify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", - "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", - "optional": true - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/send/node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", - "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==" - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stream-events": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", - "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", - "dependencies": { - "stubs": "^3.0.0" - } - }, - "node_modules/stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==" - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/taffydb": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", - "integrity": "sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA==" - }, - "node_modules/teeny-request": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.2.tgz", - "integrity": "sha512-34pe0a4zASseXZCKdeTiIZqSKA8ETHb1EwItZr01PAR3CLPojeAKgSjzeNS4373gi59hNulyDrPKEbh2zO9sCg==", - "dependencies": { - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.1", - "stream-events": "^1.0.5", - "uuid": "^9.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/teeny-request/node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, - "node_modules/tmp/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/tmp/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" - }, - "node_modules/uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/xmlcreate": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", - "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "requires": { - "@babel/highlight": "^7.18.6" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" - }, - "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/parser": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.2.tgz", - "integrity": "sha512-afk318kh2uKbo7BEj2QtEi8HVCGrwHUffrYDy7dgVcSa2j9lY3LDjPzcyGdpX7xgm35aWqvciZJ4WKmdF/SxYg==" - }, - "@google-cloud/bigquery": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@google-cloud/bigquery/-/bigquery-6.0.3.tgz", - "integrity": "sha512-BP464228S9dqDCb4dR99h9D8+N498YZi/AZvoOJUaieg2H6qbiYBE1xlYuaMvyV1WEQT/2/yZTCJnCo5WiaY0Q==", - "requires": { - "@google-cloud/common": "^4.0.0", - "@google-cloud/paginator": "^4.0.0", - "@google-cloud/promisify": "^3.0.0", - "arrify": "^2.0.1", - "big.js": "^6.0.0", - "duplexify": "^4.0.0", - "extend": "^3.0.2", - "is": "^3.3.0", - "p-event": "^4.1.0", - "readable-stream": "^4.0.0", - "stream-events": "^1.0.5", - "uuid": "^8.0.0" - }, - "dependencies": { - "@google-cloud/paginator": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-4.0.1.tgz", - "integrity": "sha512-6G1ui6bWhNyHjmbYwavdN7mpVPRBtyDg/bfqBTAlwr413On2TnFNfDxc9UhTJctkgoCDgQXEKiRPLPR9USlkbQ==", - "requires": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - } - }, - "readable-stream": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.2.0.tgz", - "integrity": "sha512-gJrBHsaI3lgBoGMW/jHZsQ/o/TIWiu5ENCJG1BB7fuCKzpFM8GaS2UoBVt9NO+oI+3FcrBNbUkl3ilDe09aY4A==", - "requires": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10" - } - } - } - }, - "@google-cloud/common": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-4.0.3.tgz", - "integrity": "sha512-fUoMo5b8iAKbrYpneIRV3z95AlxVJPrjpevxs4SKoclngWZvTXBSGpNisF5+x5m+oNGve7jfB1e6vNBZBUs7Fw==", - "requires": { - "@google-cloud/projectify": "^3.0.0", - "@google-cloud/promisify": "^3.0.0", - "arrify": "^2.0.1", - "duplexify": "^4.1.1", - "ent": "^2.2.0", - "extend": "^3.0.2", - "google-auth-library": "^8.0.2", - "retry-request": "^5.0.0", - "teeny-request": "^8.0.0" - } - }, - "@google-cloud/functions-framework": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-3.1.2.tgz", - "integrity": "sha512-pYvEH65/Rqh1JNPdcBmorcV7Xoom2/iOSmbtYza8msro7Inl+qOYxbyMiQfySD2gwAyn38WyWPRqsDRcf/BFLg==", - "requires": { - "@types/express": "4.17.13", - "body-parser": "^1.18.3", - "cloudevents": "^6.0.0", - "express": "^4.16.4", - "minimist": "^1.2.5", - "on-finished": "^2.3.0", - "read-pkg-up": "^7.0.1", - "semver": "^7.3.5" - } - }, - "@google-cloud/logging": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@google-cloud/logging/-/logging-10.3.0.tgz", - "integrity": "sha512-3QNSMci/8mmvLs4Iyb6Z/pFT/lJCDPWGWSRx08+Yi254xpva32pOU0grzgbPYls8SFhDWUQgYr9DGZg+IH0kEQ==", - "requires": { - "@google-cloud/common": "^4.0.0", - "@google-cloud/paginator": "^4.0.0", - "@google-cloud/projectify": "^3.0.0", - "@google-cloud/promisify": "^3.0.0", - "arrify": "^2.0.1", - "dot-prop": "^6.0.0", - "eventid": "^2.0.0", - "extend": "^3.0.2", - "gcp-metadata": "^4.0.0", - "google-auth-library": "^8.0.2", - "google-gax": "^3.5.2", - "on-finished": "^2.3.0", - "pumpify": "^2.0.1", - "stream-events": "^1.0.5", - "uuid": "^9.0.0" - }, - "dependencies": { - "@google-cloud/paginator": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-4.0.1.tgz", - "integrity": "sha512-6G1ui6bWhNyHjmbYwavdN7mpVPRBtyDg/bfqBTAlwr413On2TnFNfDxc9UhTJctkgoCDgQXEKiRPLPR9USlkbQ==", - "requires": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - } - }, - "gaxios": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", - "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - } - }, - "gcp-metadata": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", - "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", - "requires": { - "gaxios": "^4.0.0", - "json-bigint": "^1.0.0" - } - }, - "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" - } - } - }, - "@google-cloud/logging-bunyan": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@google-cloud/logging-bunyan/-/logging-bunyan-4.2.0.tgz", - "integrity": "sha512-BbzbJguK0sIZedO/0p27N5FDRUkdH2KsiejkoXOTNItU2GI8LweM7dtxihV9m7TcYOXVIxPhirnn8Tu3miq/VA==", - "requires": { - "@google-cloud/logging": "^10.2.2", - "google-auth-library": "^8.0.2" - } - }, - "@google-cloud/paginator": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", - "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", - "requires": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - } - }, - "@google-cloud/projectify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", - "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==" - }, - "@google-cloud/promisify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz", - "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==" - }, - "@google-cloud/storage": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.7.0.tgz", - "integrity": "sha512-iEit3dvUhGQV3pPC8aci/Y+F6K2QJ/UvcXhymj8gnO8IYQfZSZvFf361yX4BWNUlbHzanUQVQdF9RvgEM8fwpw==", - "requires": { - "@google-cloud/paginator": "^3.0.7", - "@google-cloud/projectify": "^3.0.0", - "@google-cloud/promisify": "^3.0.0", - "abort-controller": "^3.0.0", - "async-retry": "^1.3.3", - "compressible": "^2.0.12", - "duplexify": "^4.0.0", - "ent": "^2.2.0", - "extend": "^3.0.2", - "gaxios": "^5.0.0", - "google-auth-library": "^8.0.1", - "mime": "^3.0.0", - "mime-types": "^2.0.8", - "p-limit": "^3.0.1", - "retry-request": "^5.0.0", - "teeny-request": "^8.0.0", - "uuid": "^8.0.0" - } - }, - "@grpc/grpc-js": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", - "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", - "requires": { - "@grpc/proto-loader": "^0.7.0", - "@types/node": ">=12.12.47" - } - }, - "@grpc/proto-loader": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.3.tgz", - "integrity": "sha512-5dAvoZwna2Py3Ef96Ux9jIkp3iZ62TUsV00p3wVBPNX5K178UbNi8Q7gQVqwXT1Yq9RejIGG9G2IPEo93T6RcA==", - "requires": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^7.0.0", - "yargs": "^16.2.0" - } - }, - "@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" - }, - "@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" - }, - "@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "requires": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" - }, - "@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" - }, - "@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" - }, - "@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" - }, - "@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" - }, - "@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==" - }, - "@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "requires": { - "@types/node": "*" - } - }, - "@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.31", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", - "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "@types/linkify-it": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", - "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==" - }, - "@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" - }, - "@types/markdown-it": { - "version": "12.2.3", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", - "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", - "requires": { - "@types/linkify-it": "*", - "@types/mdurl": "*" - } - }, - "@types/mdurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", - "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==" - }, - "@types/mime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" - }, - "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" - }, - "@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==" - }, - "@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" - }, - "@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" - }, - "@types/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", - "requires": { - "@types/mime": "*", - "@types/node": "*" - } - }, - "abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "requires": { - "event-target-shim": "^5.0.0" - } - }, - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, - "acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==" - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "requires": {} - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "requires": { - "debug": "4" - } - }, - "ajv": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", - "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "requires": { - "ajv": "^8.0.0" - } - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" - }, - "async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", - "requires": { - "retry": "0.13.1" - } - }, - "available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "big.js": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.1.tgz", - "integrity": "sha512-bCtHMwL9LeDIozFn+oNhhFoq+yQ3BNdnsLSASUxLciOb1vgvpHsIO1dsENiGMgbb4SkP5TrzWzRiLddn8ahVOQ==" - }, - "bignumber.js": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.0.tgz", - "integrity": "sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A==" - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, - "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "bunyan": { - "version": "1.8.15", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", - "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", - "requires": { - "dtrace-provider": "~0.8", - "moment": "^2.19.3", - "mv": "~2", - "safe-json-stringify": "~1" - } - }, - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "catharsis": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", - "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", - "requires": { - "lodash": "^4.17.15" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "cloudevents": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/cloudevents/-/cloudevents-6.0.3.tgz", - "integrity": "sha512-ADEHAv2KShH/cDIy2GP+npFz3R6Fu/UCsUO/j4kYA9VqN4yhGdF+Zg6wmjeq6qlUvlaKdrVBwgZuH/w57IsyGQ==", - "requires": { - "ajv": "^8.11.0", - "ajv-formats": "^2.1.1", - "util": "^0.12.4", - "uuid": "^8.3.2" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "requires": { - "mime-db": ">= 1.43.0 < 2" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "requires": { - "safe-buffer": "5.2.1" - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" - }, - "dot-prop": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", - "requires": { - "is-obj": "^2.0.0" - } - }, - "dtrace-provider": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", - "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", - "optional": true, - "requires": { - "nan": "^2.14.0" - } - }, - "duplexify": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", - "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", - "requires": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "ent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==" - }, - "entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==" - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" - }, - "escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "requires": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" - } - } - }, - "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==" - }, - "espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" - }, - "event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" - }, - "eventid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/eventid/-/eventid-2.0.1.tgz", - "integrity": "sha512-sPNTqiMokAvV048P2c9+foqVJzk49o6d4e0D/sq5jog3pw+4kBgyR0gaM1FM7Mx6Kzd9dztesh9oYz1LWWOpzw==", - "requires": { - "uuid": "^8.0.0" - } - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" - }, - "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "requires": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" - }, - "fast-text-encoding": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", - "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" - }, - "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "requires": { - "is-callable": "^1.1.3" - } - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "gaxios": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", - "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", - "requires": { - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - } - }, - "gcp-metadata": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.1.tgz", - "integrity": "sha512-jiRJ+Fk7e8FH68Z6TLaqwea307OktJpDjmYnU7/li6ziwvVvU2RlrCyQo5vkdeP94chm0kcSCOOszvmuaioq3g==", - "requires": { - "gaxios": "^5.0.0", - "json-bigint": "^1.0.0" - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - } - }, - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", - "optional": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "google-auth-library": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.6.0.tgz", - "integrity": "sha512-y6bw1yTWMVgs1vGJwBZ3uu+uIClfgxQfsEVcTNKjQeNQOVwox69+ZUgTeTAzrh+74hBqrk1gWyb9RsQVDI7seg==", - "requires": { - "arrify": "^2.0.0", - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "fast-text-encoding": "^1.0.0", - "gaxios": "^5.0.0", - "gcp-metadata": "^5.0.0", - "gtoken": "^6.1.0", - "jws": "^4.0.0", - "lru-cache": "^6.0.0" - } - }, - "google-gax": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.5.2.tgz", - "integrity": "sha512-AyP53w0gHcWlzxm+jSgqCR3Xu4Ld7EpSjhtNBnNhzwwWaIUyphH9kBGNIEH+i4UGkTUXOY29K/Re8EiAvkBRGw==", - "requires": { - "@grpc/grpc-js": "~1.7.0", - "@grpc/proto-loader": "^0.7.0", - "@types/long": "^4.0.0", - "abort-controller": "^3.0.0", - "duplexify": "^4.0.0", - "fast-text-encoding": "^1.0.3", - "google-auth-library": "^8.0.2", - "is-stream-ended": "^0.1.4", - "node-fetch": "^2.6.1", - "object-hash": "^3.0.0", - "proto3-json-serializer": "^1.0.0", - "protobufjs": "7.1.2", - "protobufjs-cli": "1.0.2", - "retry-request": "^5.0.0" - } - }, - "google-p12-pem": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", - "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", - "requires": { - "node-forge": "^1.3.1" - } - }, - "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" - }, - "gtoken": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", - "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", - "requires": { - "gaxios": "^5.0.1", - "google-p12-pem": "^4.0.0", - "jws": "^4.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "requires": { - "has-symbols": "^1.0.2" - } - }, - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "requires": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - } - }, - "https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "is": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", - "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==" - }, - "is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, - "is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" - }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "requires": { - "has": "^1.0.3" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" - }, - "is-stream-ended": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", - "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" - }, - "is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "js2xmlparser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", - "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", - "requires": { - "xmlcreate": "^2.0.4" - } - }, - "jsdoc": { - "version": "3.6.11", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.11.tgz", - "integrity": "sha512-8UCU0TYeIYD9KeLzEcAu2q8N/mx9O3phAGl32nmHlE0LpaJL71mMkP4d+QE5zWfNt50qheHtOZ0qoxVrsX5TUg==", - "requires": { - "@babel/parser": "^7.9.4", - "@types/markdown-it": "^12.2.3", - "bluebird": "^3.7.2", - "catharsis": "^0.9.0", - "escape-string-regexp": "^2.0.0", - "js2xmlparser": "^4.0.2", - "klaw": "^3.0.0", - "markdown-it": "^12.3.2", - "markdown-it-anchor": "^8.4.1", - "marked": "^4.0.10", - "mkdirp": "^1.0.4", - "requizzle": "^0.2.3", - "strip-json-comments": "^3.1.0", - "taffydb": "2.6.2", - "underscore": "~1.13.2" - }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - } - } - }, - "json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "requires": { - "bignumber.js": "^9.0.0" - } - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "klaw": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", - "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", - "requires": { - "graceful-fs": "^4.1.9" - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", - "requires": { - "uc.micro": "^1.0.1" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" - }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "markdown-it": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", - "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", - "requires": { - "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - } - }, - "markdown-it-anchor": { - "version": "8.6.5", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.5.tgz", - "integrity": "sha512-PI1qEHHkTNWT+X6Ip9w+paonfIQ+QZP9sCeMYi47oqhH+EsW8CrJ8J7CzV19QVOj6il8ATGbK2nTECj22ZHGvQ==", - "requires": {} - }, - "marked": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.2.tgz", - "integrity": "sha512-JjBTFTAvuTgANXx82a5vzK9JLSMoV6V3LBVn4Uhdso6t7vXrGx7g1Cd2r6NYSsxrYbQGFCMqBDhFHyK5q2UvcQ==" - }, - "mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" - }, - "mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" - }, - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "optional": true, - "requires": { - "minimist": "^1.2.6" - } - }, - "moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", - "optional": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "mv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", - "optional": true, - "requires": { - "mkdirp": "~0.5.1", - "ncp": "~2.0.0", - "rimraf": "~2.4.0" - } - }, - "nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", - "optional": true - }, - "ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", - "optional": true - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" - }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - } - } - }, - "object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" - }, - "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" - }, - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "requires": { - "ee-first": "1.1.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "p-event": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", - "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", - "requires": { - "p-timeout": "^3.1.0" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==" - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" - }, - "dependencies": { - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - } - } - }, - "p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "requires": { - "p-finally": "^1.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==" - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" - }, - "proto3-json-serializer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", - "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", - "requires": { - "protobufjs": "^7.0.0" - } - }, - "protobufjs": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", - "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", - "requires": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "dependencies": { - "long": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", - "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" - } - } - }, - "protobufjs-cli": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.0.2.tgz", - "integrity": "sha512-cz9Pq9p/Zs7okc6avH20W7QuyjTclwJPgqXG11jNaulfS3nbVisID8rC+prfgq0gbZE0w9LBFd1OKFF03kgFzg==", - "requires": { - "chalk": "^4.0.0", - "escodegen": "^1.13.0", - "espree": "^9.0.0", - "estraverse": "^5.1.0", - "glob": "^8.0.0", - "jsdoc": "^3.6.3", - "minimist": "^1.2.0", - "semver": "^7.1.2", - "tmp": "^0.2.1", - "uglify-js": "^3.7.7" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - }, - "glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - } - }, - "minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, - "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", - "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", - "requires": { - "duplexify": "^4.1.1", - "inherits": "^2.0.3", - "pump": "^3.0.0" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "requires": { - "side-channel": "^1.0.4" - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" - }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" - }, - "requizzle": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz", - "integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ==", - "requires": { - "lodash": "^4.17.14" - } - }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==" - }, - "retry-request": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", - "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", - "requires": { - "debug": "^4.1.1", - "extend": "^3.0.2" - } - }, - "rimraf": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", - "optional": true, - "requires": { - "glob": "^6.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safe-json-stringify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", - "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "requires": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, - "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - } - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true - }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", - "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==" - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - }, - "stream-events": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", - "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", - "requires": { - "stubs": "^3.0.0" - } - }, - "stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" - }, - "stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==" - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" - }, - "taffydb": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", - "integrity": "sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA==" - }, - "teeny-request": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.2.tgz", - "integrity": "sha512-34pe0a4zASseXZCKdeTiIZqSKA8ETHb1EwItZr01PAR3CLPojeAKgSjzeNS4373gi59hNulyDrPKEbh2zO9sCg==", - "requires": { - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.1", - "stream-events": "^1.0.5", - "uuid": "^9.0.0" - }, - "dependencies": { - "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" - } - } - }, - "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "requires": { - "rimraf": "^3.0.0" - }, - "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" - }, - "uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==" - }, - "underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "requires": { - "punycode": "^2.1.0" - } - }, - "util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "requires": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "xmlcreate": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", - "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==" - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" - } - } -} diff --git a/blueprints/cloud-operations/apigee/package-lock.json b/blueprints/cloud-operations/apigee/package-lock.json deleted file mode 100644 index 97ea79948..000000000 --- a/blueprints/cloud-operations/apigee/package-lock.json +++ /dev/null @@ -1,251 +0,0 @@ -{ - "name": "apigee", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "dependencies": { - "superagent-debugger": "^1.2.9" - } - }, - "node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==", - "dependencies": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/superagent-debugger": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/superagent-debugger/-/superagent-debugger-1.2.9.tgz", - "integrity": "sha512-iH4NvJl1utorgRbrsYoOM8yoeTbS7YWLoDkAwRy2rgB6aP5Lr36XxmpE8GbgvmUY6R4QmYr+4R4IdAGMPmwR9g==", - "dependencies": { - "chalk": "^1.1.3", - "debug": "^2.6.0", - "lodash": "^4.17.4", - "moment": "^2.17.1", - "query-string": "^4.3.1" - } - }, - "node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "engines": { - "node": ">=0.8.0" - } - } - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" - }, - "query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==", - "requires": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - } - }, - "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "superagent-debugger": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/superagent-debugger/-/superagent-debugger-1.2.9.tgz", - "integrity": "sha512-iH4NvJl1utorgRbrsYoOM8yoeTbS7YWLoDkAwRy2rgB6aP5Lr36XxmpE8GbgvmUY6R4QmYr+4R4IdAGMPmwR9g==", - "requires": { - "chalk": "^1.1.3", - "debug": "^2.6.0", - "lodash": "^4.17.4", - "moment": "^2.17.1", - "query-string": "^4.3.1" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==" - } - } -} diff --git a/blueprints/cloud-operations/apigee/terraform.tfvars.sample b/blueprints/cloud-operations/apigee/terraform.tfvars.sample deleted file mode 100644 index c8c38cafb..000000000 --- a/blueprints/cloud-operations/apigee/terraform.tfvars.sample +++ /dev/null @@ -1,23 +0,0 @@ -project_create = { - billing_account_id = "011D94-9C86C1-ADD197" - parent = "folders/586929298360" -} -project_id = "g-prj-cd-sb-apigee-bq-10" -envgroups = { - test = ["test.cool-demos.space"] -} -environments = { - apis-test = { - envgroups = ["test"] - } -} -instances = { - instance-ew1 = { - region = "europe-west1" - environments = ["apis-test"] - psa_ip_cidr_range = "10.0.4.0/22" - } -} -psc_config = { - europe-west1 = "10.0.0.0/28" -} \ No newline at end of file diff --git a/blueprints/cloud-operations/asset-inventory-feed-remediation/versions.tf b/blueprints/cloud-operations/asset-inventory-feed-remediation/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/cloud-operations/asset-inventory-feed-remediation/versions.tf +++ b/blueprints/cloud-operations/asset-inventory-feed-remediation/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.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 286536a65..08492c6f9 100644 --- a/blueprints/cloud-operations/dns-fine-grained-iam/versions.tf +++ b/blueprints/cloud-operations/dns-fine-grained-iam/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/cloud-operations/dns-shared-vpc/versions.tf b/blueprints/cloud-operations/dns-shared-vpc/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/cloud-operations/dns-shared-vpc/versions.tf +++ b/blueprints/cloud-operations/dns-shared-vpc/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.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 286536a65..08492c6f9 100644 --- a/blueprints/cloud-operations/iam-delegated-role-grants/versions.tf +++ b/blueprints/cloud-operations/iam-delegated-role-grants/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/cloud-operations/network-dashboard/README.md b/blueprints/cloud-operations/network-dashboard/README.md index 768e0f12d..1fe0960f7 100644 --- a/blueprints/cloud-operations/network-dashboard/README.md +++ b/blueprints/cloud-operations/network-dashboard/README.md @@ -1,103 +1,89 @@ -# Networking Dashboard +# Network Dashboard and Discovery Tool -This repository provides an end-to-end solution to gather some GCP Networking quotas and limits (that cannot be seen in the GCP console today) and display them in a dashboard. -The goal is to allow for better visibility of these limits, facilitating capacity planning and avoiding hitting these limits. +This repository provides an end-to-end solution to gather some GCP networking quotas, limits, and their corresponding usage, store them in Cloud Operations timeseries which can displayed in one or more dashboards or wired to alerts. -Here is an example of dashboard you can get with this solution: +The goal is to allow for better visibility of these limits, some of which cannot be seen in the GCP console today, facilitating capacity planning and being notified when actual usage approaches them. + +The tool tracks several distinct usage types across a variety of resources: projects, policies, networks, subnetworks, peering groups, etc. For each usage type three distinct metrics are created tracking usage count, limit and utilization ratio. + +The screenshot below is an example of a simple dashboard provided with this blueprint, showing utilization for a specific metric (number of instances per VPC) for multiple VPCs and projects: -Here you see utilization (usage compared to the limit) for a specific metric (number of instances per VPC) for multiple VPCs and projects. +One other example is the IP utilization information per subnet, allowing you to monitor the percentage of used IP addresses in your GCP subnets. -Three metric descriptors are created for each monitored resource: usage, limit and utilization. You can follow each of these and create alerting policies if a threshold is reached. +More complex scenarios are possible by leveraging and combining the 50 different timeseries created by this tool, and connecting them to Cloud Operations dashboards and alerts. -## Usage +Refer to the [Cloud Function deployment instructions](./deploy-cloud-function/) for a high level overview and an end-to-end deployment example, and to the[discovery tool documentation](./src/) to try it as a standalone program or to package it in alternative ways. -Clone this repository, then go through the following steps to create resources: -- Create a terraform.tfvars file with the following content: - ```tfvars - organization_id = "" - billing_account = "" - monitoring_project_id = "" - # Monitoring project where the dashboard will be created and the solution deployed, a project named "mon-network-dahshboard" will be created if left blank - monitored_projects_list = ["project-1", "project2"] - # Projects to be monitored by the solution - monitored_folders_list = ["folder_id"] - # Folders to be monitored by the solution - prefix = "" - # Monitoring project name prefix, monitoring project name is -network-dashboard, ignored if monitoring_project_id variable is provided - cf_version = V1|V2 - # Set to V2 to use V2 Cloud Functions environment - ``` -- `terraform init` -- `terraform apply` +## Metrics created -Note: Org level viewing permission is required for some metrics such as firewall policies. - -Once the resources are deployed, go to the following page to see the dashboard: https://console.cloud.google.com/monitoring/dashboards?project= a dashboard called "quotas-utilization" should be created. - -The Cloud Function runs every 10 minutes by default so you should start getting some data points after a few minutes. -You can use the metric explorer to view the data points for the different custom metrics created: https://console.cloud.google.com/monitoring/metrics-explorer?project=. -You can change this frequency by modifying the "schedule_cron" variable in variables.tf. - -Note that some charts in the dashboard align values over 1h so you might need to wait 1h to see charts on the dashboard views. - -Once done testing, you can clean up resources by running `terraform destroy`. - -## Supported limits and quotas -The Cloud Function currently tracks usage, limit and utilization of: -- active VPC peerings per VPC -- VPC peerings per VPC -- instances per VPC -- instances per VPC peering group -- Subnet IP ranges per VPC peering group -- internal forwarding rules for internal L4 load balancers per VPC -- internal forwarding rules for internal L7 load balancers per VPC -- internal forwarding rules for internal L4 load balancers per VPC peering group -- internal forwarding rules for internal L7 load balancers per VPC peering group -- Dynamic routes per VPC -- Dynamic routes per VPC peering group -- Static routes per project (VPC drill down is available for usage) -- Static routes per VPC peering group -- IP utilization per subnet (% of IP addresses used in a subnet) -- VPC firewall rules per project (VPC drill down is available for usage) -- Tuples per Firewall Policy - -It writes this values to custom metrics in Cloud Monitoring and creates a dashboard to visualize the current utilization of these metrics in Cloud Monitoring. - -Note that metrics are created in the cloud-function/metrics.yaml file. You can also edit default limits for a specific network in that file. See the example for `vpc_peering_per_network`. +- `firewall_policy/tuples_available` +- `firewall_policy/tuples_used` +- `firewall_policy/tuples_used_ratio` +- `network/firewall_rules_used` +- `network/forwarding_rules_l4_available` +- `network/forwarding_rules_l4_used` +- `network/forwarding_rules_l4_used_ratio` +- `network/forwarding_rules_l7_available` +- `network/forwarding_rules_l7_used` +- `network/forwarding_rules_l7_used_ratio` +- `network/instances_available` +- `network/instances_used` +- `network/instances_used_ratio` +- `network/peerings_active_available` +- `network/peerings_active_used` +- `network/peerings_active_used_ratio` +- `network/peerings_total_available` +- `network/peerings_total_used` +- `network/peerings_total_used_ratio` +- `network/routes_dynamic_available` +- `network/routes_dynamic_used` +- `network/routes_dynamic_used_ratio` +- `network/routes_static_used` +- `network/subnets_available` +- `network/subnets_used` +- `network/subnets_used_ratio` +- `peering_group/forwarding_rules_l4_available` +- `peering_group/forwarding_rules_l4_used` +- `peering_group/forwarding_rules_l4_used_ratio` +- `peering_group/forwarding_rules_l7_available` +- `peering_group/forwarding_rules_l7_used` +- `peering_group/forwarding_rules_l7_used_ratio` +- `peering_group/instances_available` +- `peering_group/instances_used` +- `peering_group/instances_used_ratio` +- `peering_group/routes_dynamic_available` +- `peering_group/routes_dynamic_used` +- `peering_group/routes_dynamic_used_ratio` +- `peering_group/routes_static_available` +- `peering_group/routes_static_used` +- `peering_group/routes_static_used_ratio` +- `project/firewall_rules_available` +- `project/firewall_rules_used` +- `project/firewall_rules_used_ratio` +- `project/routes_static_available` +- `project/routes_static_used` +- `project/routes_static_used_ratio` +- `subnetwork/addresses_available` +- `subnetwork/addresses_used` +- `subnetwork/addresses_used_ratio` ## Assumptions and limitations -- The CF assumes that all VPCs in peering groups are within the same organization, except for PSA peerings -- The CF will only fetch subnet utilization data from the PSA peerings (not the VMs, ILB or routes usage) -- The CF assumes global routing is ON, this impacts dynamic routes usage calculation -- The CF assumes custom routes importing/exporting is ON, this impacts static and dynamic routes usage calculation -- The CF assumes all networks in peering groups have the same global routing and custom routes sharing configuration -## Next steps and ideas -In a future release, we could support: -- Google managed VPCs that are peered with PSA (such as Cloud SQL or Memorystore) -- Dynamic routes calculation for VPCs/PPGs with "global routing" set to OFF -- Static routes calculation for projects/PPGs with "custom routes importing/exporting" set to OFF -- Calculations for cross Organization peering groups -- Support different scopes (reduced and fine-grained) +- The tool assumes all VPCs in peering groups are within the same organization, except for PSA peerings. +- The tool will only fetch subnet utilization data from the PSA peerings (not the VMs, ILB or routes usage). +- The tool assumes global routing is ON, this impacts dynamic routes usage calculation. +- The tool assumes custom routes importing/exporting is ON, this impacts static and dynamic routes usage calculation. +- The tool assumes all networks in peering groups have the same global routing and custom routes sharing configuration. -If you are interested in this and/or would like to contribute, please contact legranda@google.com. - +## TODO -## Variables +These are some of our ideas for additional features: -| name | description | type | required | default | -|---|---|:---:|:---:|:---:| -| [billing_account](variables.tf#L17) | The ID of the billing account to associate this project with. | | ✓ | | -| [monitored_projects_list](variables.tf#L36) | ID of the projects to be monitored (where limits and quotas data will be pulled). | list(string) | ✓ | | -| [organization_id](variables.tf#L46) | The organization id for the associated services. | | ✓ | | -| [prefix](variables.tf#L50) | Prefix used for resource names. | string | ✓ | | -| [cf_version](variables.tf#L21) | Cloud Function version 2nd Gen or 1st Gen. Possible options: 'V1' or 'V2'.Use CFv2 if your Cloud Function timeouts after 9 minutes. By default it is using CFv1. | | | V1 | -| [monitored_folders_list](variables.tf#L30) | ID of the projects to be monitored (where limits and quotas data will be pulled). | list(string) | | [] | -| [monitoring_project_id](variables.tf#L41) | Monitoring project where the dashboard will be created and the solution deployed; a project will be created if set to empty string. | | | | -| [project_monitoring_services](variables.tf#L59) | Service APIs enabled in the monitoring project if it will be created. | | | […] | -| [region](variables.tf#L81) | Region used to deploy the cloud functions and scheduler. | | | europe-west1 | -| [schedule_cron](variables.tf#L86) | Cron format schedule to run the Cloud Function. Default is every 10 minutes. | | | */10 * * * * | +- support PSA-peered Google VPCs (Cloud SQL, Memorystore, etc.) +- dynamic routes for VPCs/peering groups with "global routing" turned off +- static routes calculation for projects/peering groups with custom routes import/export turned off +- cross-organization peering groups - +If you are interested in this and/or would like to contribute, please open an issue in this repository or send us a PR. diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/main.py b/blueprints/cloud-operations/network-dashboard/cloud-function/main.py deleted file mode 100644 index 8e7640dd4..000000000 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/main.py +++ /dev/null @@ -1,242 +0,0 @@ -# -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# CFv2 define whether to use Cloud function 2nd generation or 1st generation - -import re -from distutils.command.config import config -import os -import time -from google.cloud import monitoring_v3, asset_v1 -from google.protobuf import field_mask_pb2 -from googleapiclient import discovery -from metrics import ilb_fwrules, firewall_policies, instances, networks, metrics, limits, peerings, routes, subnets, vpc_firewalls, secondarys - -CF_VERSION = os.environ.get("CF_VERSION") - - -def get_monitored_projects_list(config): - ''' - Gets the projects to be monitored from the MONITORED_FOLDERS_LIST environment variable. - - Parameters: - config (dict): The dict containing config like clients and limits - Returns: - monitored_projects (List of strings): Full list of projects to be monitored - ''' - monitored_projects = config["monitored_projects"] - monitored_folders = os.environ.get("MONITORED_FOLDERS_LIST").split(",") - - # Handling empty monitored folders list - if monitored_folders == ['']: - monitored_folders = [] - - # Gets all projects under each monitored folder (and even in sub folders) - for folder in monitored_folders: - read_mask = field_mask_pb2.FieldMask() - read_mask.FromJsonString('name,versionedResources') - - response = config["clients"]["asset_client"].search_all_resources( - request={ - "scope": f"folders/{folder}", - "asset_types": ["cloudresourcemanager.googleapis.com/Project"], - "read_mask": read_mask - }) - - for resource in response: - for versioned in resource.versioned_resources: - for field_name, field_value in versioned.resource.items(): - if field_name == "projectId": - project_id = field_value - # Avoid duplicate - if project_id not in monitored_projects: - monitored_projects.append(project_id) - - print("List of projects to be monitored:") - print(monitored_projects) - - return monitored_projects - - -def monitoring_interval(): - ''' - Creates the monitoring interval of 24 hours - Returns: - monitoring_v3.TimeInterval: Monitoring time interval of 24h - ''' - now = time.time() - seconds = int(now) - nanos = int((now - seconds) * 10**9) - return monitoring_v3.TimeInterval({ - "end_time": { - "seconds": seconds, - "nanos": nanos - }, - "start_time": { - "seconds": (seconds - 24 * 60 * 60), - "nanos": nanos - }, - }) - - -config = { - # Organization ID containing the projects to be monitored - "organization": - os.environ.get("ORGANIZATION_ID"), - # list of projects from which function will get quotas information - "monitored_projects": - os.environ.get("MONITORED_PROJECTS_LIST").split(","), - "monitoring_project": - os.environ.get('MONITORING_PROJECT_ID'), - "monitoring_project_link": - f"projects/{os.environ.get('MONITORING_PROJECT_ID')}", - "monitoring_interval": - monitoring_interval(), - "limit_names": { - "GCE_INSTANCES": - "compute.googleapis.com/quota/instances_per_vpc_network/limit", - "L4": - "compute.googleapis.com/quota/internal_lb_forwarding_rules_per_vpc_network/limit", - "L7": - "compute.googleapis.com/quota/internal_managed_forwarding_rules_per_vpc_network/limit", - "SUBNET_RANGES": - "compute.googleapis.com/quota/subnet_ranges_per_vpc_network/limit" - }, - "lb_scheme": { - "L7": "INTERNAL_MANAGED", - "L4": "INTERNAL" - }, - "clients": { - "discovery_client": discovery.build('compute', 'v1'), - "asset_client": asset_v1.AssetServiceClient(), - "monitoring_client": monitoring_v3.MetricServiceClient() - }, - # Improve performance for Asset Inventory queries on large environments - "page_size": - 500, - "series_buffer": [], -} - - -def main(event, context=None): - ''' - Cloud Function Entry point, called by the scheduler. - Parameters: - event: Not used for now (Pubsub trigger) - context: Not used for now (Pubsub trigger) - Returns: - 'Function executed successfully' - ''' - # Handling empty monitored projects list - if config["monitored_projects"] == ['']: - config["monitored_projects"] = [] - - # Gets projects and folders to be monitored - config["monitored_projects"] = get_monitored_projects_list(config) - - # Keep the monitoring interval up2date during each run - config["monitoring_interval"] = monitoring_interval() - - metrics_dict, limits_dict = metrics.create_metrics( - config["monitoring_project_link"], config) - project_quotas_dict = limits.get_quota_project_limit(config) - - firewalls_dict = vpc_firewalls.get_firewalls_dict(config) - firewall_policies_dict = firewall_policies.get_firewall_policies_dict(config) - - # IP utilization subnet level metrics - subnets.get_subnets(config, metrics_dict) - - # IP utilization secondary range metrics - secondarys.get_secondaries(config, metrics_dict) - - # Asset inventory queries - gce_instance_dict = instances.get_gce_instance_dict(config) - l4_forwarding_rules_dict = ilb_fwrules.get_forwarding_rules_dict(config, "L4") - l7_forwarding_rules_dict = ilb_fwrules.get_forwarding_rules_dict(config, "L7") - subnet_range_dict = networks.get_subnet_ranges_dict(config) - static_routes_dict = routes.get_static_routes_dict(config) - dynamic_routes_dict = routes.get_dynamic_routes( - config, metrics_dict, limits_dict['dynamic_routes_per_network_limit']) - - try: - - # Per Project metrics - vpc_firewalls.get_firewalls_data(config, metrics_dict, project_quotas_dict, - firewalls_dict) - # Per Firewall Policy metrics - firewall_policies.get_firewal_policies_data(config, metrics_dict, - firewall_policies_dict) - # Per Network metrics - instances.get_gce_instances_data(config, metrics_dict, gce_instance_dict, - limits_dict['number_of_instances_limit']) - ilb_fwrules.get_forwarding_rules_data( - config, metrics_dict, l4_forwarding_rules_dict, - limits_dict['internal_forwarding_rules_l4_limit'], "L4") - ilb_fwrules.get_forwarding_rules_data( - config, metrics_dict, l7_forwarding_rules_dict, - limits_dict['internal_forwarding_rules_l7_limit'], "L7") - - routes.get_static_routes_data(config, metrics_dict, static_routes_dict, - project_quotas_dict) - - peerings.get_vpc_peering_data(config, metrics_dict, - limits_dict['number_of_vpc_peerings_limit']) - - # Per VPC peering group metrics - metrics.get_pgg_data( - config, - metrics_dict["metrics_per_peering_group"]["instance_per_peering_group"], - gce_instance_dict, config["limit_names"]["GCE_INSTANCES"], - limits_dict['number_of_instances_ppg_limit']) - metrics.get_pgg_data( - config, metrics_dict["metrics_per_peering_group"] - ["l4_forwarding_rules_per_peering_group"], l4_forwarding_rules_dict, - config["limit_names"]["L4"], - limits_dict['internal_forwarding_rules_l4_ppg_limit']) - metrics.get_pgg_data( - config, metrics_dict["metrics_per_peering_group"] - ["l7_forwarding_rules_per_peering_group"], l7_forwarding_rules_dict, - config["limit_names"]["L7"], - limits_dict['internal_forwarding_rules_l7_ppg_limit']) - metrics.get_pgg_data( - config, metrics_dict["metrics_per_peering_group"] - ["subnet_ranges_per_peering_group"], subnet_range_dict, - config["limit_names"]["SUBNET_RANGES"], - limits_dict['number_of_subnet_IP_ranges_ppg_limit']) - #static - routes.get_routes_ppg( - config, metrics_dict["metrics_per_peering_group"] - ["static_routes_per_peering_group"], static_routes_dict, - limits_dict['static_routes_per_peering_group_limit']) - #dynamic - routes.get_routes_ppg( - config, metrics_dict["metrics_per_peering_group"] - ["dynamic_routes_per_peering_group"], dynamic_routes_dict, - limits_dict['dynamic_routes_per_peering_group_limit']) - except Exception as e: - print("Error writing metrics") - print(e) - finally: - metrics.flush_series_buffer(config) - - return 'Function execution completed' - - -if CF_VERSION == "V2": - import functions_framework - main_http = functions_framework.http(main) - -if __name__ == "__main__": - main(None, None) \ No newline at end of file diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics.yaml b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics.yaml deleted file mode 100644 index 217599634..000000000 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics.yaml +++ /dev/null @@ -1,223 +0,0 @@ -# -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ---- -metrics_per_subnet: - ip_usage_per_subnet: - usage: - name: number_of_ip_used - description: Number of used IP addresses in the subnet. - utilization: - name: ip_addresses_per_subnet_utilization - description: Percentage of IP used in the subnet. - limit: - name: number_of_max_ip - description: Number of available IP addresses in the subnet. - ip_usage_per_secondaryRange: - usage: - name: number_of_sr_ip_used - description: Number of used IP addresses in the secondary range. - utilization: - name: ip_addresses_per_sr_utilization - description: Percentage of IP used in the secondary range. - limit: - name: number_of_max_sr_ip - description: Number of available IP addresses in the secondary range. -metrics_per_network: - instance_per_network: - usage: - name: number_of_instances_usage - description: Number of instances per VPC network - usage. - limit: - name: number_of_instances_limit - description: Number of instances per VPC network - limit. - values: - default_value: 15000 - utilization: - name: number_of_instances_utilization - description: Number of instances per VPC network - utilization. - vpc_peering_active_per_network: - usage: - name: number_of_active_vpc_peerings_usage - description: Number of active VPC Peerings per VPC - usage. - limit: - name: number_of_active_vpc_peerings_limit - description: Number of active VPC Peerings per VPC - limit. - values: - default_value: 25 - utilization: - name: number_of_active_vpc_peerings_utilization - description: Number of active VPC Peerings per VPC - utilization. - vpc_peering_per_network: - usage: - name: number_of_vpc_peerings_usage - description: Number of VPC Peerings per VPC - usage. - limit: - name: number_of_vpc_peerings_limit - description: Number of VPC Peerings per VPC - limit. - values: - default_value: 25 - https://www.googleapis.com/compute/v1/projects/net-dash-test-host-prod/global/networks/vpc-prod: 40 - utilization: - name: number_of_vpc_peerings_utilization - description: Number of VPC Peerings per VPC - utilization. - l4_forwarding_rules_per_network: - usage: - name: internal_forwarding_rules_l4_usage - description: Number of Internal Forwarding Rules for Internal L4 Load Balancers - usage. - limit: - name: internal_forwarding_rules_l4_limit - description: Number of Internal Forwarding Rules for Internal L4 Load Balancers - limit. - values: - default_value: 500 - utilization: - name: internal_forwarding_rules_l4_utilization - description: Number of Internal Forwarding Rules for Internal L4 Load Balancers - utilization. - l7_forwarding_rules_per_network: - usage: - name: internal_forwarding_rules_l7_usage - description: Number of Internal Forwarding Rules for Internal L7 Load Balancers per network - usage. - limit: - name: internal_forwarding_rules_l7_limit - description: Number of Internal Forwarding Rules for Internal L7 Load Balancers per network - effective limit. - values: - default_value: 75 - utilization: - name: internal_forwarding_rules_l7_utilization - description: Number of Internal Forwarding Rules for Internal L7 Load Balancers per Vnetwork - utilization. - dynamic_routes_per_network: - usage: - name: dynamic_routes_per_network_usage - description: Number of Dynamic routes per network - usage. - limit: - name: dynamic_routes_per_network_limit - description: Number of Dynamic routes per network - limit. - values: - default_value: 100 - utilization: - name: dynamic_routes_per_network_utilization - description: Number of Dynamic routes per network - utilization. - #static routes limit is per project, but usage is per network - static_routes_per_project: - usage: - name: static_routes_per_project_vpc_usage - description: Number of Static routes per project and network - usage. - limit: - name: static_routes_per_project_limit - description: Number of Static routes per project - limit. - values: - default_value: 250 - utilization: - name: static_routes_per_project_utilization - description: Number of Static routes per project - utilization. -metrics_per_peering_group: - l4_forwarding_rules_per_peering_group: - usage: - name: internal_forwarding_rules_l4_ppg_usage - description: Number of Internal Forwarding Rules for Internal L4 Load Balancers per VPC peering group - usage. - limit: - name: internal_forwarding_rules_l4_ppg_limit - description: Number of Internal Forwarding Rules for Internal L4 Load Balancers per VPC peering group - effective limit. - values: - default_value: 500 - utilization: - name: internal_forwarding_rules_l4_ppg_utilization - description: Number of Internal Forwarding Rules for Internal L4 Load Balancers per VPC peering group - utilization. - l7_forwarding_rules_per_peering_group: - usage: - name: internal_forwarding_rules_l7_ppg_usage - description: Number of Internal Forwarding Rules for Internal L7 Load Balancers per VPC peering group - usage. - limit: - name: internal_forwarding_rules_l7_ppg_limit - description: Number of Internal Forwarding Rules for Internal L7 Load Balancers per VPC peering group - effective limit. - values: - default_value: 175 - utilization: - name: internal_forwarding_rules_l7_ppg_utilization - description: Number of Internal Forwarding Rules for Internal L7 Load Balancers per VPC peering group - utilization. - subnet_ranges_per_peering_group: - usage: - name: number_of_subnet_IP_ranges_ppg_usage - description: Number of Subnet Ranges per peering group - usage. - limit: - name: number_of_subnet_IP_ranges_ppg_limit - description: Number of Subnet Ranges per peering group - effective limit. - values: - default_value: 400 - utilization: - name: number_of_subnet_IP_ranges_ppg_utilization - description: Number of Subnet Ranges per peering group - utilization. - instance_per_peering_group: - usage: - name: number_of_instances_ppg_usage - description: Number of instances per peering group - usage. - limit: - name: number_of_instances_ppg_limit - description: Number of instances per peering group - limit. - values: - default_value: 15500 - utilization: - name: number_of_instances_ppg_utilization - description: Number of instances per peering group - utilization. - dynamic_routes_per_peering_group: - usage: - name: dynamic_routes_per_peering_group_usage - description: Number of Dynamic routes per peering group - usage. - limit: - name: dynamic_routes_per_peering_group_limit - description: Number of Dynamic routes per peering group - limit. - values: - default_value: 300 - utilization: - name: dynamic_routes_per_peering_group_utilization - description: Number of Dynamic routes per peering group - utilization. - static_routes_per_peering_group: - usage: - name: static_routes_per_peering_group_usage - description: Number of Static routes per peering group - usage. - limit: - name: static_routes_per_peering_group_limit - description: Number of Static routes per peering group - limit. - values: - default_value: 300 - utilization: - name: static_routes_per_peering_group_utilization - description: Number of Static routes per peering group - utilization. -metrics_per_project: - firewalls: - usage: - name: firewalls_per_project_vpc_usage - description: Number of VPC firewall rules in a project - usage. - limit: - # Firewalls limit is per project and we get the limit for the GCP quota API in vpc_firewalls.py - name: firewalls_per_project_limit - description: Number of VPC firewall rules in a project - limit. - utilization: - name: firewalls_per_project_utilization - description: Number of VPC firewall rules in a project - utilization. -metrics_per_firewall_policy: - firewall_policy_tuples: - usage: - name: firewall_policy_tuples_per_policy_usage - description: Number of tuples in a firewall policy - usage. - limit: - # This limit is not visibile through Google APIs, set default_value - name: firewall_policy_tuples_per_policy_limit - description: Number of tuples in a firewall policy - limit. - values: - default_value: 2000 - utilization: - name: firewall_policy_tuples_per_policy_utilization - description: Number of tuples in a firewall policy - utilization. diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/firewall_policies.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/firewall_policies.py deleted file mode 100644 index 95a26db38..000000000 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/firewall_policies.py +++ /dev/null @@ -1,118 +0,0 @@ -# -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import re -import time - -from collections import defaultdict -from pydoc import doc -from collections import defaultdict -from google.protobuf import field_mask_pb2 -from . import metrics, networks, limits - - -def get_firewall_policies_dict(config: dict): - ''' - Calls the Asset Inventory API to get all Firewall Policies under the GCP organization, including children - Ignores monitored projects list: returns all policies regardless of their parent resource - Parameters: - config (dict): The dict containing config like clients and limits - Returns: - firewal_policies_dict (dictionary of dictionary): Keys are policy ids, subkeys are policy field values - ''' - - firewall_policies_dict = defaultdict(int) - read_mask = field_mask_pb2.FieldMask() - read_mask.FromJsonString('name,versionedResources') - - response = config["clients"]["asset_client"].search_all_resources( - request={ - "scope": f"organizations/{config['organization']}", - "asset_types": ["compute.googleapis.com/FirewallPolicy"], - "read_mask": read_mask, - }) - for resource in response: - for versioned in resource.versioned_resources: - firewall_policy = dict() - for field_name, field_value in versioned.resource.items(): - firewall_policy[field_name] = field_value - firewall_policies_dict[firewall_policy['id']] = firewall_policy - return firewall_policies_dict - - -def get_firewal_policies_data(config, metrics_dict, firewall_policies_dict): - ''' - Gets the data for VPC Firewall Policies in an organization, including children. All folders are considered, - only projects in the monitored projects list are considered. - Parameters: - config (dict): The dict containing config like clients and limits - metrics_dict (dictionary of dictionary of string: string): metrics names and descriptions. - firewall_policies_dict (dictionary of of dictionary of string: string): Keys are policies ids, subkeys are policies values - Returns: - None - ''' - - current_tuples_limit = None - try: - current_tuples_limit = metrics_dict["metrics_per_firewall_policy"][ - "firewall_policy_tuples"]["limit"]["values"]["default_value"] - except Exception: - print( - f"Could not determine number of tuples metric limit due to missing default value" - ) - if current_tuples_limit < 0: - print( - f"Could not determine number of tuples metric limit as default value is <= 0" - ) - - timestamp = time.time() - for firewall_policy_key in firewall_policies_dict: - firewall_policy = firewall_policies_dict[firewall_policy_key] - - # may either be a org, a folder, or a project - # folder and org require to split {folder,organization}\/\w+ - parent = re.search("(\w+$)", firewall_policy["parent"]).group( - 1) if "parent" in firewall_policy else re.search( - "([\d,a-z,-]+)(\/[\d,a-z,-]+\/firewallPolicies/[\d,a-z,-]*$)", - firewall_policy["selfLink"]).group(1) - parent_type = re.search("(^\w+)", firewall_policy["parent"]).group( - 1) if "parent" in firewall_policy else "projects" - - if parent_type == "projects" and parent not in config["monitored_projects"]: - continue - - metric_labels = {'parent': parent, 'parent_type': parent_type} - - metric_labels["name"] = firewall_policy[ - "displayName"] if "displayName" in firewall_policy else firewall_policy[ - "name"] - - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_firewall_policy"] - [f"firewall_policy_tuples"]["usage"]["name"], - firewall_policy['ruleTupleCount'], metric_labels, timestamp=timestamp) - if not current_tuples_limit == None and current_tuples_limit > 0: - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_firewall_policy"] - [f"firewall_policy_tuples"]["limit"]["name"], current_tuples_limit, - metric_labels, timestamp=timestamp) - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_firewall_policy"] - [f"firewall_policy_tuples"]["utilization"]["name"], - firewall_policy['ruleTupleCount'] / current_tuples_limit, - metric_labels, timestamp=timestamp) - - print(f"Buffered number tuples per Firewall Policy") diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/ilb_fwrules.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/ilb_fwrules.py deleted file mode 100644 index de8274d97..000000000 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/ilb_fwrules.py +++ /dev/null @@ -1,122 +0,0 @@ -# -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import time - -from collections import defaultdict -from google.protobuf import field_mask_pb2 -from . import metrics, networks, limits - - -def get_forwarding_rules_dict(config, layer: str): - ''' - Calls the Asset Inventory API to get all L4 Forwarding Rules under the GCP organization. - - Parameters: - config (dict): The dict containing config like clients and limits - layer (string): the Layer to get Forwarding rules (L4/L7) - Returns: - forwarding_rules_dict (dictionary of string: int): Keys are the network links and values are the number of Forwarding Rules per network. - ''' - - read_mask = field_mask_pb2.FieldMask() - read_mask.FromJsonString('name,versionedResources') - - forwarding_rules_dict = defaultdict(int) - - response = config["clients"]["asset_client"].search_all_resources( - request={ - "scope": f"organizations/{config['organization']}", - "asset_types": ["compute.googleapis.com/ForwardingRule"], - "read_mask": read_mask, - "page_size": config["page_size"], - }) - - for resource in response: - internal = False - network_link = "" - for versioned in resource.versioned_resources: - for field_name, field_value in versioned.resource.items(): - if field_name == "loadBalancingScheme": - internal = (field_value == config["lb_scheme"][layer]) - if field_name == "network": - network_link = field_value - if internal: - if network_link in forwarding_rules_dict: - forwarding_rules_dict[network_link] += 1 - else: - forwarding_rules_dict[network_link] = 1 - - return forwarding_rules_dict - - -def get_forwarding_rules_data(config, metrics_dict, forwarding_rules_dict, - limit_dict, layer): - ''' - Gets the data for L4 Internal Forwarding Rules per VPC Network and writes it to the metric defined in forwarding_rules_metric. - - Parameters: - config (dict): The dict containing config like clients and limits - metrics_dict (dictionary of dictionary of string: string): metrics names and descriptions. - forwarding_rules_dict (dictionary of string: int): Keys are the network links and values are the number of Forwarding Rules per network. - limit_dict (dictionary of string:int): Dictionary with the network link as key and the limit as value. - layer (string): the Layer to get Forwarding rules (L4/L7) - Returns: - None - ''' - - timestamp = time.time() - for project_id in config["monitored_projects"]: - network_dict = networks.get_networks(config, project_id) - - current_quota_limit = limits.get_quota_current_limit( - config, f"projects/{project_id}", config["limit_names"][layer]) - - if current_quota_limit is None: - print( - f"Could not determine {layer} forwarding rules to metric for projects/{project_id} due to missing quotas" - ) - continue - - current_quota_limit_view = metrics.customize_quota_view(current_quota_limit) - - for net in network_dict: - limits.set_limits(net, current_quota_limit_view, limit_dict) - - usage = 0 - if net['self_link'] in forwarding_rules_dict: - usage = forwarding_rules_dict[net['self_link']] - - metric_labels = { - 'project': project_id, - 'network_name': net['network_name'] - } - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"] - [f"{layer.lower()}_forwarding_rules_per_network"]["usage"]["name"], - usage, metric_labels, timestamp=timestamp) - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"] - [f"{layer.lower()}_forwarding_rules_per_network"]["limit"]["name"], - net['limit'], metric_labels, timestamp=timestamp) - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"] - [f"{layer.lower()}_forwarding_rules_per_network"]["utilization"] - ["name"], usage / net['limit'], metric_labels, timestamp=timestamp) - - print( - f"Buffered number of {layer} forwarding rules to metric for projects/{project_id}" - ) \ No newline at end of file diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/instances.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/instances.py deleted file mode 100644 index d3b72e678..000000000 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/instances.py +++ /dev/null @@ -1,103 +0,0 @@ -# -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import time - -from code import interact -from collections import defaultdict -from . import metrics, networks, limits - - -def get_gce_instance_dict(config: dict): - ''' - Calls the Asset Inventory API to get all GCE instances under the GCP organization. - - Parameters: - config (dict): The dict containing config like clients and limits - - Returns: - gce_instance_dict (dictionary of string: int): Keys are the network links and values are the number of GCE Instances per network. - ''' - - gce_instance_dict = defaultdict(int) - - response = config["clients"]["asset_client"].search_all_resources( - request={ - "scope": f"organizations/{config['organization']}", - "asset_types": ["compute.googleapis.com/Instance"], - "page_size": config["page_size"], - }) - for resource in response: - for field_name, field_value in resource.additional_attributes.items(): - if field_name == "networkInterfaceNetworks": - for network in field_value: - if network in gce_instance_dict: - gce_instance_dict[network] += 1 - else: - gce_instance_dict[network] = 1 - - return gce_instance_dict - - -def get_gce_instances_data(config, metrics_dict, gce_instance_dict, limit_dict): - ''' - Gets the data for GCE instances per VPC Network and writes it to the metric defined in instance_metric. - - Parameters: - config (dict): The dict containing config like clients and limits - metrics_dict (dictionary of dictionary of string: string): metrics names and descriptions - gce_instance_dict (dictionary of string: int): Keys are the network links and values are the number of GCE Instances per network. - limit_dict (dictionary of string:int): Dictionary with the network link as key and the limit as value - Returns: - gce_instance_dict - ''' - timestamp = time.time() - for project_id in config["monitored_projects"]: - network_dict = networks.get_networks(config, project_id) - - current_quota_limit = limits.get_quota_current_limit( - config, f"projects/{project_id}", - config["limit_names"]["GCE_INSTANCES"]) - if current_quota_limit is None: - print( - f"Could not determine number of instances for projects/{project_id} due to missing quotas" - ) - - current_quota_limit_view = metrics.customize_quota_view(current_quota_limit) - - for net in network_dict: - limits.set_limits(net, current_quota_limit_view, limit_dict) - - usage = 0 - if net['self_link'] in gce_instance_dict: - usage = gce_instance_dict[net['self_link']] - - metric_labels = { - 'project': project_id, - 'network_name': net['network_name'] - } - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"]["instance_per_network"] - ["usage"]["name"], usage, metric_labels, timestamp=timestamp) - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"]["instance_per_network"] - ["limit"]["name"], net['limit'], metric_labels, timestamp=timestamp) - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"]["instance_per_network"] - ["utilization"]["name"], usage / net['limit'], metric_labels, - timestamp=timestamp) - - print(f"Buffered number of instances to metric for projects/{project_id}") diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/limits.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/limits.py deleted file mode 100644 index edd4a50b3..000000000 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/limits.py +++ /dev/null @@ -1,236 +0,0 @@ -# -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import time - -from google.api_core import exceptions -from google.cloud import monitoring_v3 -from . import metrics - - -def get_quotas_dict(quotas_list): - ''' - Creates a dictionary of quotas from a list, with lower case quota name as keys - Parameters: - quotas_array (array): array of quotas - Returns: - quotas_dict (dict): dictionary of quotas - ''' - quota_keys = [q['metric'] for q in quotas_list] - quotas_dict = dict() - i = 0 - for key in quota_keys: - if ("metric" in quotas_list[i]): - del (quotas_list[i]["metric"]) - quotas_dict[key.lower()] = quotas_list[i] - i += 1 - return quotas_dict - - -def get_quota_project_limit(config, regions=["global"]): - ''' - Retrieves quotas for all monitored project in selected regions, default 'global' - Parameters: - project_link (string): Project link. - Returns: - quotas (dict): quotas for all selected regions, default 'global' - ''' - try: - request = {} - quotas = dict() - for project in config["monitored_projects"]: - quotas[project] = dict() - if regions != ["global"]: - for region in regions: - request = config["clients"]["discovery_client"].compute.regions().get( - region=region, project=project) - response = request.execute() - quotas[project][region] = get_quotas_dict(response['quotas']) - else: - region = "global" - request = config["clients"]["discovery_client"].projects().get( - project=project, fields="quotas") - response = request.execute() - quotas[project][region] = get_quotas_dict(response['quotas']) - - return quotas - except exceptions.PermissionDenied as err: - print( - f"Warning: error reading quotas for {project}. " + - f"This can happen if you don't have permissions on the project, for example if the project is in another organization or a Google managed project" - ) - return None - - -def get_ppg(network_link, limit_dict): - ''' - Checks if this network has a specific limit for a metric, if so, returns that limit, if not, returns the default limit. - - Parameters: - network_link (string): VPC network link. - limit_list (list of string): Used to get the limit per VPC or the default limit. - Returns: - limit_dict (dictionary of string:int): Dictionary with the network link as key and the limit as value - ''' - if network_link in limit_dict: - return limit_dict[network_link] - else: - if 'default_value' in limit_dict: - return limit_dict['default_value'] - else: - print(f"Error: limit not found for {network_link}") - return 0 - - -def set_limits(network_dict, quota_limit, limit_dict): - ''' - Updates the network dictionary with quota limit values. - - Parameters: - network_dict (dictionary of string: string): Contains network information. - quota_limit (list of dictionaries of string: string): Current quota limit. - limit_dict (dictionary of string:int): Dictionary with the network link as key and the limit as value - Returns: - None - ''' - - network_dict['limit'] = None - - if quota_limit: - for net in quota_limit: - if net['network_id'] == network_dict['network_id']: - network_dict['limit'] = net['value'] - return - - network_link = f"https://www.googleapis.com/compute/v1/projects/{network_dict['project_id']}/global/networks/{network_dict['network_name']}" - - if network_link in limit_dict: - network_dict['limit'] = limit_dict[network_link] - else: - if 'default_value' in limit_dict: - network_dict['limit'] = limit_dict['default_value'] - else: - print(f"Error: Couldn't find limit for {network_link}") - network_dict['limit'] = 0 - - -def get_quota_current_limit(config, project_link, metric_name): - ''' - Retrieves limit for a specific metric. - - Parameters: - project_link (string): Project link. - metric_name (string): Name of the metric. - Returns: - results_list (list of string): Current limit. - ''' - - try: - results = config["clients"]["monitoring_client"].list_time_series( - request={ - "name": project_link, - "filter": f'metric.type = "{metric_name}"', - "interval": config["monitoring_interval"], - "view": monitoring_v3.ListTimeSeriesRequest.TimeSeriesView.FULL - }) - results_list = list(results) - return results_list - except exceptions.PermissionDenied as err: - print( - f"Warning: error reading quotas for {project_link}. " + - f"This can happen if you don't have permissions on the project, for example if the project is in another organization or a Google managed project" - ) - return None - - -def count_effective_limit(config, project_id, network_dict, usage_metric_name, - limit_metric_name, utilization_metric_name, - limit_dict, timestamp=None): - ''' - Calculates the effective limits (using algorithm in the link below) for peering groups and writes data (usage, limit, utilization) to the custom metrics. - Source: https://cloud.google.com/vpc/docs/quota#vpc-peering-effective-limit - - Parameters: - config (dict): The dict containing config like clients and limits - project_id (string): Project ID for the project to be analyzed. - network_dict (dictionary of string: string): Contains all required information about the network to get the usage, limit and utilization. - usage_metric_name (string): Name of the custom metric to be populated for usage per VPC peering group. - limit_metric_name (string): Name of the custom metric to be populated for limit per VPC peering group. - utilization_metric_name (string): Name of the custom metric to be populated for utilization per VPC peering group. - limit_dict (dictionary of string:int): Dictionary containing the limit per peering group (either VPC specific or default limit). - timestamp (time): timestamp to be recorded for all points - Returns: - None - ''' - - if timestamp == None: - timestamp = time.time() - - if network_dict['peerings'] == []: - return - - # Get usage: Sums usage for current network + all peered networks - peering_group_usage = network_dict['usage'] - for peered_network in network_dict['peerings']: - if 'usage' not in peered_network: - print( - f"Cannot add metrics for peered network in projects/{project_id} as no usage metrics exist due to missing permissions" - ) - continue - peering_group_usage += peered_network['usage'] - - network_link = f"https://www.googleapis.com/compute/v1/projects/{project_id}/global/networks/{network_dict['network_name']}" - - # Calculates effective limit: Step 1: max(per network limit, per network_peering_group limit) - limit_step1 = max(network_dict['limit'], get_ppg(network_link, limit_dict)) - - # Calculates effective limit: Step 2: List of max(per network limit, per network_peering_group limit) for each peered network - limit_step2 = [] - for peered_network in network_dict['peerings']: - peered_network_link = f"https://www.googleapis.com/compute/v1/projects/{peered_network['project_id']}/global/networks/{peered_network['network_name']}" - - if 'limit' in peered_network: - limit_step2.append( - max(peered_network['limit'], get_ppg(peered_network_link, - limit_dict))) - else: - print( - f"Ignoring projects/{peered_network['project_id']} for limits in peering group of project {project_id} as no limits are available." - + - "This can happen if you don't have permissions on the project, for example if the project is in another organization or a Google managed project" - ) - - # Calculates effective limit: Step 3: Find minimum from the list created by Step 2 - limit_step3 = 0 - if len(limit_step2) > 0: - limit_step3 = min(limit_step2) - - # Calculates effective limit: Step 4: Find maximum from step 1 and step 3 - effective_limit = max(limit_step1, limit_step3) - utilization = peering_group_usage / effective_limit - metric_labels = { - 'project': project_id, - 'network_name': network_dict['network_name'] - } - metrics.append_data_to_series_buffer(config, usage_metric_name, - peering_group_usage, metric_labels, - timestamp=timestamp) - metrics.append_data_to_series_buffer(config, limit_metric_name, - effective_limit, metric_labels, - timestamp=timestamp) - metrics.append_data_to_series_buffer(config, utilization_metric_name, - utilization, metric_labels, - timestamp=timestamp) diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/metrics.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/metrics.py deleted file mode 100644 index 8e0c4082b..000000000 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/metrics.py +++ /dev/null @@ -1,267 +0,0 @@ -# -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -from curses import KEY_MARK -import re -import time -import yaml -from google.api import metric_pb2 as ga_metric -from google.cloud import monitoring_v3 -from . import peerings, limits, networks - - -def create_metrics(monitoring_project, config): - ''' - Creates all Cloud Monitoring custom metrics based on the metric.yaml file - Parameters: - monitoring_project (string): the project where the metrics are written to - config (dict): The dict containing config like clients and limits - Returns: - metrics_dict (dictionary of dictionary of string: string): metrics names and descriptions - limits_dict (dictionary of dictionary of string: int): limits_dict[metric_name]: dict[network_name] = limit_value - ''' - client = config["clients"]["monitoring_client"] - existing_metrics = [] - for desc in client.list_metric_descriptors(name=monitoring_project): - existing_metrics.append(desc.type) - limits_dict = {} - - with open("./metrics.yaml", 'r') as stream: - try: - metrics_dict = yaml.safe_load(stream) - - for metric_list in metrics_dict.values(): - for metric_name, metric in metric_list.items(): - for sub_metric_key, sub_metric in metric.items(): - metric_link = f"custom.googleapis.com/{sub_metric['name']}" - # If the metric doesn't exist yet, then we create it - if metric_link not in existing_metrics: - create_metric(sub_metric["name"], sub_metric["description"], - monitoring_project, config) - # Parse limits for network and peering group metrics - # Subnet level metrics have a different limit: the subnet IP range size - if sub_metric_key == "limit" and ( - metric_name != "ip_usage_per_subnet" and - metric_name != "ip_usage_per_secondaryRange"): - limits_dict_for_metric = {} - if "values" in sub_metric: - for network_link, limit_value in sub_metric["values"].items(): - limits_dict_for_metric[network_link] = limit_value - limits_dict[sub_metric["name"]] = limits_dict_for_metric - - return metrics_dict, limits_dict - except yaml.YAMLError as exc: - print(exc) - - -def create_metric(metric_name, description, monitoring_project, config): - ''' - Creates a Cloud Monitoring metric based on the parameter given if the metric is not already existing - Parameters: - metric_name (string): Name of the metric to be created - description (string): Description of the metric to be created - monitoring_project (string): the project where the metrics are written to - config (dict): The dict containing config like clients and limits - Returns: - None - ''' - client = config["clients"]["monitoring_client"] - - descriptor = ga_metric.MetricDescriptor() - descriptor.type = f"custom.googleapis.com/{metric_name}" - descriptor.metric_kind = ga_metric.MetricDescriptor.MetricKind.GAUGE - descriptor.value_type = ga_metric.MetricDescriptor.ValueType.DOUBLE - descriptor.description = description - descriptor = client.create_metric_descriptor(name=monitoring_project, - metric_descriptor=descriptor) - print("Created {}.".format(descriptor.name)) - - -def append_data_to_series_buffer(config, metric_name, metric_value, - metric_labels, timestamp=None): - ''' - Appends data to Cloud Monitoring custom metrics, using a buffer. buffer is flushed every BUFFER_LEN elements, - any unflushed series is discarded upon function closure - Parameters: - config (dict): The dict containing config like clients and limits - metric_name (string): Name of the metric - metric_value (int): Value for the data point of the metric. - matric_labels (dictionary of dictionary of string: string): metric labels names and values - timestamp (float): seconds since the epoch, in UTC - Returns: - usage (int): Current usage for that network. - limit (int): Current usage for that network. - ''' - - # Configurable buffer size to improve performance when writing datapoints to metrics - buffer_len = 10 - - series = monitoring_v3.TimeSeries() - series.metric.type = f"custom.googleapis.com/{metric_name}" - series.resource.type = "global" - - for label_name in metric_labels: - if (metric_labels[label_name] != None): - series.metric.labels[label_name] = metric_labels[label_name] - - timestamp = timestamp if timestamp != None else time.time() - seconds = int(timestamp) - nanos = int((timestamp - seconds) * 10**9) - interval = monitoring_v3.TimeInterval( - {"end_time": { - "seconds": seconds, - "nanos": nanos - }}) - point = monitoring_v3.Point({ - "interval": interval, - "value": { - "double_value": metric_value - } - }) - series.points = [point] - - # TODO: sometimes this cashes with 'DeadlineExceeded: 504 Deadline expired before operation could complete' error - # Implement exponential backoff retries? - config["series_buffer"].append(series) - if len(config["series_buffer"]) >= buffer_len: - flush_series_buffer(config) - - -def flush_series_buffer(config): - ''' - writes buffered metrics to Google Cloud Monitoring, empties buffer upon both failure/success - config (dict): The dict containing config like clients and limits - ''' - try: - if config["series_buffer"] and len(config["series_buffer"]) > 0: - client = config["clients"]["monitoring_client"] - client.create_time_series(name=config["monitoring_project_link"], - time_series=config["series_buffer"]) - series_names = [ - re.search("\/(.+$)", series.metric.type).group(1) - for series in config["series_buffer"] - ] - print("Wrote time series: ", series_names) - except Exception as e: - print("Error while flushing series buffer") - print(e) - - config["series_buffer"] = [] - - -def get_pgg_data(config, metric_dict, usage_dict, limit_metric, limit_dict): - ''' - This function gets the usage, limit and utilization per VPC peering group for a specific metric for all projects to be monitored. - Parameters: - config (dict): The dict containing config like clients and limits - metric_dict (dictionary of string: string): Dictionary with the metric names and description, that will be used to populate the metrics - usage_dict (dictionnary of string:int): Dictionary with the network link as key and the number of resources as value - limit_metric (string): Name of the existing GCP metric for limit per VPC network - limit_dict (dictionary of string:int): Dictionary with the network link as key and the limit as value - Returns: - None - ''' - for project_id in config["monitored_projects"]: - network_dict_list = peerings.gather_peering_data(config, project_id) - # Network dict list is a list of dictionary (one for each network) - # For each network, this dictionary contains: - # project_id, network_name, network_id, usage, limit, peerings (list of peered networks) - # peerings is a list of dictionary (one for each peered network) and contains: - # project_id, network_name, network_id - current_quota_limit = limits.get_quota_current_limit( - config, f"projects/{project_id}", limit_metric) - if current_quota_limit is None: - print( - f"Could not determine number of L7 forwarding rules to metric for projects/{project_id} due to missing quotas" - ) - continue - - current_quota_limit_view = customize_quota_view(current_quota_limit) - - timestamp = time.time() - # For each network in this GCP project - for network_dict in network_dict_list: - if network_dict['network_id'] == 0: - print( - f"Could not determine {metric_dict['usage']['name']} for peering group {network_dict['network_name']} in {project_id} due to missing permissions." - ) - continue - network_link = f"https://www.googleapis.com/compute/v1/projects/{project_id}/global/networks/{network_dict['network_name']}" - - limit = networks.get_limit_network(network_dict, network_link, - current_quota_limit_view, limit_dict) - - usage = 0 - if network_link in usage_dict: - usage = usage_dict[network_link] - - # Here we add usage and limit to the network dictionary - network_dict["usage"] = usage - network_dict["limit"] = limit - - # For every peered network, get usage and limits - for peered_network_dict in network_dict['peerings']: - peered_network_link = f"https://www.googleapis.com/compute/v1/projects/{peered_network_dict['project_id']}/global/networks/{peered_network_dict['network_name']}" - peered_usage = 0 - if peered_network_link in usage_dict: - peered_usage = usage_dict[peered_network_link] - - current_peered_quota_limit = limits.get_quota_current_limit( - config, f"projects/{peered_network_dict['project_id']}", - limit_metric) - if current_peered_quota_limit is None: - print( - f"Could not determine metrics for peering to projects/{peered_network_dict['project_id']} due to missing quotas" - ) - continue - - peering_project_limit = customize_quota_view(current_peered_quota_limit) - - peered_limit = networks.get_limit_network(peered_network_dict, - peered_network_link, - peering_project_limit, - limit_dict) - # Here we add usage and limit to the peered network dictionary - peered_network_dict["usage"] = peered_usage - peered_network_dict["limit"] = peered_limit - - limits.count_effective_limit(config, project_id, network_dict, - metric_dict["usage"]["name"], - metric_dict["limit"]["name"], - metric_dict["utilization"]["name"], - limit_dict, timestamp) - print( - f"Buffered {metric_dict['usage']['name']} for peering group {network_dict['network_name']} in {project_id}" - ) - - -def customize_quota_view(quota_results): - ''' - Customize the quota output for an easier parsable output. - Parameters: - quota_results (string): Input from get_quota_current_usage or get_quota_current_limit. Contains the Current usage or limit for all networks in that project. - Returns: - quotaViewList (list of dictionaries of string: string): Current quota usage or limit. - ''' - quotaViewList = [] - for result in quota_results: - quotaViewJson = {} - quotaViewJson.update(dict(result.resource.labels)) - quotaViewJson.update(dict(result.metric.labels)) - for val in result.points: - quotaViewJson.update({'value': val.value.int64_value}) - quotaViewList.append(quotaViewJson) - return quotaViewList diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/networks.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/networks.py deleted file mode 100644 index 094f374ed..000000000 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/networks.py +++ /dev/null @@ -1,160 +0,0 @@ -# -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -from code import interact -from collections import defaultdict -from google.protobuf import field_mask_pb2 -from googleapiclient import errors -import http - - -def get_subnet_ranges_dict(config: dict): - ''' - Calls the Asset Inventory API to get all Subnet ranges under the GCP organization. - - Parameters: - config (dict): The dict containing config like clients and limits - Returns: - subnet_range_dict (dictionary of string: int): Keys are the network links and values are the number of subnet ranges per network. - ''' - - subnet_range_dict = defaultdict(int) - read_mask = field_mask_pb2.FieldMask() - read_mask.FromJsonString('name,versionedResources') - - response = config["clients"]["asset_client"].search_all_resources( - request={ - "scope": f"organizations/{config['organization']}", - "asset_types": ["compute.googleapis.com/Subnetwork"], - "read_mask": read_mask, - "page_size": config["page_size"], - }) - for resource in response: - ranges = 0 - network_link = None - - for versioned in resource.versioned_resources: - for field_name, field_value in versioned.resource.items(): - if field_name == "network": - network_link = field_value - ranges += 1 - if field_name == "secondaryIpRanges": - for range in field_value: - ranges += 1 - - if network_link in subnet_range_dict: - subnet_range_dict[network_link] += ranges - else: - subnet_range_dict[network_link] = ranges - - return subnet_range_dict - - -def get_networks(config, project_id): - ''' - Returns a dictionary of all networks in a project. - - Parameters: - config (dict): The dict containing config like clients and limits - project_id (string): Project ID for the project containing the networks. - Returns: - network_dict (dictionary of string: string): Contains the project_id, network_name(s) and network_id(s) - ''' - request = config["clients"]["discovery_client"].networks().list( - project=project_id) - response = request.execute() - network_dict = [] - if 'items' in response: - for network in response['items']: - network_name = network['name'] - network_id = network['id'] - self_link = network['selfLink'] - d = { - 'project_id': project_id, - 'network_name': network_name, - 'network_id': network_id, - 'self_link': self_link - } - network_dict.append(d) - return network_dict - - -def get_network_id(config, project_id, network_name): - ''' - Returns the network_id for a specific project / network name. - - Parameters: - config (dict): The dict containing config like clients and limits - project_id (string): Project ID for the project containing the networks. - network_name (string): Name of the network - Returns: - network_id (int): Network ID. - ''' - request = config["clients"]["discovery_client"].networks().list( - project=project_id) - try: - response = request.execute() - except errors.HttpError as err: - # TODO: log proper warning - if err.resp.status == http.HTTPStatus.FORBIDDEN: - print( - f"Warning: error reading networks for {project_id}. " + - f"This can happen if you don't have permissions on the project, for example if the project is in another organization or a Google managed project" - ) - else: - print(f"Warning: error reading networks for {project_id}: {err}") - return 0 - - network_id = 0 - - if 'items' in response: - for network in response['items']: - if network['name'] == network_name: - network_id = network['id'] - break - - if network_id == 0: - print(f"Error: network_id not found for {network_name} in {project_id}") - - return network_id - - -def get_limit_network(network_dict, network_link, quota_limit, limit_dict): - ''' - Returns limit for a specific network and metric, using the GCP quota metrics or the values in the yaml file if not found. - - Parameters: - network_dict (dictionary of string: string): Contains network information. - network_link (string): Contains network link - quota_limit (list of dictionaries of string: string): Current quota limit for all networks in that project. - limit_dict (dictionary of string:int): Dictionary with the network link as key and the limit as value - Returns: - limit (int): Current limit for that network. - ''' - if quota_limit: - for net in quota_limit: - if net['network_id'] == network_dict['network_id']: - return net['value'] - - if network_link in limit_dict: - return limit_dict[network_link] - else: - if 'default_value' in limit_dict: - return limit_dict['default_value'] - else: - print(f"Error: Couldn't find limit for {network_link}") - - return 0 diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/peerings.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/peerings.py deleted file mode 100644 index 616c7f663..000000000 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/peerings.py +++ /dev/null @@ -1,179 +0,0 @@ -# -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import time - -from . import metrics, networks, limits - - -def get_vpc_peering_data(config, metrics_dict, limit_dict): - ''' - Gets the data for VPC peerings (active or not) and writes it to the metric defined (vpc_peering_active_metric and vpc_peering_metric). - - Parameters: - config (dict): The dict containing config like clients and limits - metrics_dict (dictionary of dictionary of string: string): metrics names and descriptions - limit_dict (dictionary of string:int): Dictionary with the network link as key and the limit as value - Returns: - None - ''' - timestamp = time.time() - for project in config["monitored_projects"]: - active_vpc_peerings, vpc_peerings = gather_vpc_peerings_data( - config, project, limit_dict) - - for peering in active_vpc_peerings: - metric_labels = { - 'project': project, - 'network_name': peering['network_name'] - } - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"] - ["vpc_peering_active_per_network"]["usage"]["name"], - peering['active_peerings'], metric_labels, timestamp=timestamp) - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"] - ["vpc_peering_active_per_network"]["limit"]["name"], - peering['network_limit'], metric_labels, timestamp=timestamp) - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"] - ["vpc_peering_active_per_network"]["utilization"]["name"], - peering['active_peerings'] / peering['network_limit'], metric_labels, - timestamp=timestamp) - print( - "Buffered number of active VPC peerings to custom metric for project:", - project) - - for peering in vpc_peerings: - metric_labels = { - 'project': project, - 'network_name': peering['network_name'] - } - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"]["vpc_peering_per_network"] - ["usage"]["name"], peering['peerings'], metric_labels, - timestamp=timestamp) - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"]["vpc_peering_per_network"] - ["limit"]["name"], peering['network_limit'], metric_labels, - timestamp=timestamp) - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"]["vpc_peering_per_network"] - ["utilization"]["name"], - peering['peerings'] / peering['network_limit'], metric_labels, - timestamp=timestamp) - print("Buffered number of VPC peerings to custom metric for project:", - project) - - -def gather_peering_data(config, project_id): - ''' - Returns a dictionary of all peerings for all networks in a project. - - Parameters: - config (dict): The dict containing config like clients and limits - project_id (string): Project ID for the project containing the networks. - Returns: - network_list (dictionary of string: string): Contains the project_id, network_name(s) and network_id(s) of peered networks. - ''' - request = config["clients"]["discovery_client"].networks().list( - project=project_id) - response = request.execute() - - network_list = [] - if 'items' in response: - for network in response['items']: - net = { - 'project_id': project_id, - 'network_name': network['name'], - 'network_id': network['id'], - 'peerings': [] - } - if 'peerings' in network: - STATE = network['peerings'][0]['state'] - if STATE == "ACTIVE": - for peered_network in network[ - 'peerings']: # "projects/{project_name}/global/networks/{network_name}" - start = peered_network['network'].find("projects/") + len( - 'projects/') - end = peered_network['network'].find("/global") - peered_project = peered_network['network'][start:end] - peered_network_name = peered_network['network'].split( - "networks/")[1] - peered_net = { - 'project_id': - peered_project, - 'network_name': - peered_network_name, - 'network_id': - networks.get_network_id(config, peered_project, - peered_network_name) - } - net["peerings"].append(peered_net) - network_list.append(net) - return network_list - - -def gather_vpc_peerings_data(config, project_id, limit_dict): - ''' - Gets the data for all VPC peerings (active or not) in project_id and writes it to the metric defined in vpc_peering_active_metric and vpc_peering_metric. - - Parameters: - config (dict): The dict containing config like clients and limits - project_id (string): We will take all VPCs in that project_id and look for all peerings to these VPCs. - limit_dict (dictionary of string:int): Dictionary with the network link as key and the limit as value - Returns: - active_peerings_dict (dictionary of string: string): Contains project_id, network_name, network_limit for each active VPC peering. - peerings_dict (dictionary of string: string): Contains project_id, network_name, network_limit for each VPC peering. - ''' - active_peerings_dict = [] - peerings_dict = [] - request = config["clients"]["discovery_client"].networks().list( - project=project_id) - response = request.execute() - if 'items' in response: - for network in response['items']: - if 'peerings' in network: - STATE = network['peerings'][0]['state'] - if STATE == "ACTIVE": - active_peerings_count = len(network['peerings']) - else: - active_peerings_count = 0 - - peerings_count = len(network['peerings']) - else: - peerings_count = 0 - active_peerings_count = 0 - - network_link = f"https://www.googleapis.com/compute/v1/projects/{project_id}/global/networks/{network['name']}" - network_limit = limits.get_ppg(network_link, limit_dict) - - active_d = { - 'project_id': project_id, - 'network_name': network['name'], - 'active_peerings': active_peerings_count, - 'network_limit': network_limit - } - active_peerings_dict.append(active_d) - d = { - 'project_id': project_id, - 'network_name': network['name'], - 'peerings': peerings_count, - 'network_limit': network_limit - } - peerings_dict.append(d) - - return active_peerings_dict, peerings_dict diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/routers.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/routers.py deleted file mode 100644 index 064354e7f..000000000 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/routers.py +++ /dev/null @@ -1,57 +0,0 @@ -# -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -from google.protobuf import field_mask_pb2 - - -def get_routers(config): - ''' - Returns a dictionary of all Cloud Routers in the GCP organization. - - Parameters: - config (dict): The dict containing config like clients and limits - Returns: - routers_dict (dictionary of string: list of string): Key is the network link and value is a list of router links. - ''' - - read_mask = field_mask_pb2.FieldMask() - read_mask.FromJsonString('name,versionedResources') - - routers_dict = {} - - response = config["clients"]["asset_client"].search_all_resources( - request={ - "scope": f"organizations/{config['organization']}", - "asset_types": ["compute.googleapis.com/Router"], - "read_mask": read_mask, - "page_size": config["page_size"], - }) - for resource in response: - network_link = None - router_link = None - for versioned in resource.versioned_resources: - for field_name, field_value in versioned.resource.items(): - if field_name == "network": - network_link = field_value - if field_name == "selfLink": - router_link = field_value - - if network_link in routers_dict: - routers_dict[network_link].append(router_link) - else: - routers_dict[network_link] = [router_link] - - return routers_dict diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/routes.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/routes.py deleted file mode 100644 index a16145454..000000000 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/routes.py +++ /dev/null @@ -1,289 +0,0 @@ -# -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import time - -from collections import defaultdict -from google.protobuf import field_mask_pb2 -from . import metrics, networks, limits, peerings, routers - - -def get_routes_for_router(config, project_id, router_region, router_name): - ''' - Returns the same of dynamic routes learned by a specific Cloud Router instance - - Parameters: - config (dict): The dict containing config like clients and limits - project_id (string): Project ID for the project containing the Cloud Router. - router_region (string): GCP region for the Cloud Router. - router_name (string): Cloud Router name. - Returns: - sum_routes (int): Number of dynamic routes learned by the Cloud Router. - ''' - request = config["clients"]["discovery_client"].routers().getRouterStatus( - project=project_id, region=router_region, router=router_name) - response = request.execute() - - sum_routes = 0 - - if 'result' in response: - if 'bgpPeerStatus' in response['result']: - for peer in response['result']['bgpPeerStatus']: - sum_routes += peer['numLearnedRoutes'] - - return sum_routes - - -def get_routes_for_network(config, network_link, project_id, routers_dict): - ''' - Returns a the number of dynamic routes for a given network - - Parameters: - config (dict): The dict containing config like clients and limits - network_link (string): Network self link. - project_id (string): Project ID containing the network. - routers_dict (dictionary of string: list of string): Dictionary with key as network link and value as list of router links. - Returns: - sum_routes (int): Number of routes in that network. - ''' - sum_routes = 0 - - if network_link in routers_dict: - for router_link in routers_dict[network_link]: - # Router link is using the following format: - # 'https://www.googleapis.com/compute/v1/projects/PROJECT_ID/regions/REGION/routers/ROUTER_NAME' - start = router_link.find("/regions/") + len("/regions/") - end = router_link.find("/routers/") - router_region = router_link[start:end] - router_name = router_link.split('/routers/')[1] - routes = get_routes_for_router(config, project_id, router_region, - router_name) - - sum_routes += routes - - return sum_routes - - -def get_dynamic_routes(config, metrics_dict, limits_dict): - ''' - This function gets the usage, limit and utilization for the dynamic routes per VPC - note: assumes global routing is ON for all VPCs - Parameters: - config (dict): The dict containing config like clients and limits - metrics_dict (dictionary of dictionary of string: string): metrics names and descriptions. - limits_dict (dictionary of string: int): key is network link (or 'default_value') and value is the limit for that network - Returns: - dynamic_routes_dict (dictionary of string: int): key is network link and value is the number of dynamic routes for that network - ''' - routers_dict = routers.get_routers(config) - dynamic_routes_dict = defaultdict(int) - - timestamp = time.time() - for project in config["monitored_projects"]: - network_dict = networks.get_networks(config, project) - - for net in network_dict: - sum_routes = get_routes_for_network(config, net['self_link'], project, - routers_dict) - dynamic_routes_dict[net['self_link']] = sum_routes - - if net['self_link'] in limits_dict: - limit = limits_dict[net['self_link']] - else: - if 'default_value' in limits_dict: - limit = limits_dict['default_value'] - else: - print("Error: couldn't find limit for dynamic routes.") - break - - utilization = sum_routes / limit - metric_labels = {'project': project, 'network_name': net['network_name']} - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"] - ["dynamic_routes_per_network"]["usage"]["name"], sum_routes, - metric_labels, timestamp=timestamp) - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"] - ["dynamic_routes_per_network"]["limit"]["name"], limit, metric_labels, - timestamp=timestamp) - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"] - ["dynamic_routes_per_network"]["utilization"]["name"], utilization, - metric_labels, timestamp=timestamp) - - print("Buffered metrics for dynamic routes for VPCs in project", project) - - return dynamic_routes_dict - - -def get_routes_ppg(config, metric_dict, usage_dict, limit_dict): - ''' - This function gets the usage, limit and utilization for the static or dynamic routes per VPC peering group. - note: assumes global routing is ON for all VPCs for dynamic routes, assumes share custom routes is on for all peered networks - Parameters: - config (dict): The dict containing config like clients and limits - metric_dict (dictionary of string: string): Dictionary with the metric names and description, that will be used to populate the metrics - usage_dict (dictionnary of string:int): Dictionary with the network link as key and the number of resources as value - limit_dict (dictionary of string:int): Dictionary with the network link as key and the limit as value - Returns: - None - ''' - timestamp = time.time() - for project_id in config["monitored_projects"]: - network_dict_list = peerings.gather_peering_data(config, project_id) - - for network_dict in network_dict_list: - network_link = f"https://www.googleapis.com/compute/v1/projects/{project_id}/global/networks/{network_dict['network_name']}" - - limit = limits.get_ppg(network_link, limit_dict) - - usage = 0 - if network_link in usage_dict: - usage = usage_dict[network_link] - - # Here we add usage and limit to the network dictionary - network_dict["usage"] = usage - network_dict["limit"] = limit - - # For every peered network, get usage and limits - for peered_network_dict in network_dict['peerings']: - peered_network_link = f"https://www.googleapis.com/compute/v1/projects/{peered_network_dict['project_id']}/global/networks/{peered_network_dict['network_name']}" - peered_usage = 0 - if peered_network_link in usage_dict: - peered_usage = usage_dict[peered_network_link] - - peered_limit = limits.get_ppg(peered_network_link, limit_dict) - - # Here we add usage and limit to the peered network dictionary - peered_network_dict["usage"] = peered_usage - peered_network_dict["limit"] = peered_limit - - limits.count_effective_limit(config, project_id, network_dict, - metric_dict["usage"]["name"], - metric_dict["limit"]["name"], - metric_dict["utilization"]["name"], - limit_dict, timestamp) - print( - f"Buffered {metric_dict['usage']['name']} for peering group {network_dict['network_name']} in {project_id}" - ) - - -def get_static_routes_dict(config): - ''' - Calls the Asset Inventory API to get all static custom routes under the GCP organization. - Parameters: - config (dict): The dict containing config like clients and limits - Returns: - routes_per_vpc_dict (dictionary of string: int): Keys are the network links and values are the number of custom static routes per network. - ''' - routes_per_vpc_dict = defaultdict() - usage_dict = defaultdict() - - read_mask = field_mask_pb2.FieldMask() - read_mask.FromJsonString('name,versionedResources') - - response = config["clients"]["asset_client"].search_all_resources( - request={ - "scope": f"organizations/{config['organization']}", - "asset_types": ["compute.googleapis.com/Route"], - "read_mask": read_mask - }) - - for resource in response: - for versioned in resource.versioned_resources: - static_route = dict() - for field_name, field_value in versioned.resource.items(): - static_route[field_name] = field_value - static_route["project_id"] = static_route["network"].split('/')[6] - static_route["network_name"] = static_route["network"].split('/')[-1] - network_link = f"https://www.googleapis.com/compute/v1/projects/{static_route['project_id']}/global/networks/{static_route['network_name']}" - #exclude default vpc and peering routes, dynamic routes are not in Cloud Asset Inventory - if "nextHopPeering" not in static_route and "nextHopNetwork" not in static_route: - if network_link not in routes_per_vpc_dict: - routes_per_vpc_dict[network_link] = dict() - routes_per_vpc_dict[network_link]["project_id"] = static_route[ - "project_id"] - routes_per_vpc_dict[network_link]["network_name"] = static_route[ - "network_name"] - if static_route["destRange"] not in routes_per_vpc_dict[network_link]: - routes_per_vpc_dict[network_link][static_route["destRange"]] = {} - if "usage" not in routes_per_vpc_dict[network_link]: - routes_per_vpc_dict[network_link]["usage"] = 0 - routes_per_vpc_dict[network_link][ - "usage"] = routes_per_vpc_dict[network_link]["usage"] + 1 - - #output a dict with network links and usage only - return { - network_link_out: routes_per_vpc_dict[network_link_out]["usage"] - for network_link_out in routes_per_vpc_dict - } - - -def get_static_routes_data(config, metrics_dict, static_routes_dict, - project_quotas_dict): - ''' - Determines and writes the number of static routes for each VPC in monitored projects, the per project limit and the per project utilization - note: assumes custom routes sharing is ON for all VPCs - Parameters: - config (dict): The dict containing config like clients and limits - metric_dict (dictionary of string: string): Dictionary with the metric names and description, that will be used to populate the metrics - static_routes_dict (dictionary of dictionary: int): Keys are the network links and values are the number of custom static routes per network. - project_quotas_dict (dictionary of string:int): Dictionary with the network link as key and the limit as value. - Returns: - None - ''' - timestamp = time.time() - project_usage = {project: 0 for project in config["monitored_projects"]} - - #usage is drilled down by network - for network_link in static_routes_dict: - - project_id = network_link.split('/')[6] - if (project_id not in config["monitored_projects"]): - continue - network_name = network_link.split('/')[-1] - - project_usage[project_id] = project_usage[project_id] + static_routes_dict[ - network_link] - - metric_labels = {"project": project_id, "network_name": network_name} - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"]["static_routes_per_project"] - ["usage"]["name"], static_routes_dict[network_link], metric_labels, - timestamp=timestamp) - - #limit and utilization are calculated by project - for project_id in project_usage: - current_quota_limit = project_quotas_dict[project_id]['global']["routes"][ - "limit"] - if current_quota_limit is None: - print( - f"Could not determine static routes metric for projects/{project_id} due to missing quotas" - ) - continue - # limit and utilization are calculted by project - metric_labels = {"project": project_id} - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"]["static_routes_per_project"] - ["limit"]["name"], current_quota_limit, metric_labels, - timestamp=timestamp) - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_network"]["static_routes_per_project"] - ["utilization"]["name"], - project_usage[project_id] / current_quota_limit, metric_labels, - timestamp=timestamp) - - return diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/secondarys.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/secondarys.py deleted file mode 100644 index 6030ddafd..000000000 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/secondarys.py +++ /dev/null @@ -1,266 +0,0 @@ -# -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import time - -from . import metrics -from google.protobuf import field_mask_pb2 -from google.protobuf.json_format import MessageToDict -import ipaddress - - -def get_all_secondaryRange(config): - ''' - Returns a dictionary with secondary range informations - Parameters: - config (dict): The dict containing config like clients and limits - Returns: - secondary_dict (dictionary of String: dictionary): Key is the project_id, - value is a nested dictionary with subnet_name/secondary_range_name as the key. - ''' - secondary_dict = {} - read_mask = field_mask_pb2.FieldMask() - read_mask.FromJsonString('name,versionedResources') - - response = config["clients"]["asset_client"].search_all_resources( - request={ - "scope": f"organizations/{config['organization']}", - "asset_types": ['compute.googleapis.com/Subnetwork'], - "read_mask": read_mask, - "page_size": config["page_size"], - }) - - for asset in response: - for versioned in asset.versioned_resources: - subnet_name = versioned.resource.get('name') - # Network self link format: - # "https://www.googleapis.com/compute/v1/projects//global/networks/" - project_id = versioned.resource.get('network').split('/')[6] - network_name = versioned.resource.get('network').split('/')[-1] - subnet_region = versioned.resource.get('region').split('/')[-1] - - # Check first if the subnet has any secondary ranges to begin with - if versioned.resource.get('secondaryIpRanges'): - for items in versioned.resource.get('secondaryIpRanges'): - # Each subnet can have multiple secondary ranges - secondaryRange_name = items.get('rangeName') - secondaryCidrBlock = items.get('ipCidrRange') - - net = ipaddress.ip_network(secondaryCidrBlock) - total_ip_addresses = int(net.num_addresses) - - if project_id not in secondary_dict: - secondary_dict[project_id] = {} - secondary_dict[project_id][f"{subnet_name}/{secondaryRange_name}"] = { - 'name': secondaryRange_name, - 'region': subnet_region, - 'subnetName': subnet_name, - 'ip_cidr_range': secondaryCidrBlock, - 'total_ip_addresses': total_ip_addresses, - 'used_ip_addresses': 0, - 'network_name': network_name - } - return secondary_dict - - -def compute_GKE_secondaryIP_utilization(config, read_mask, all_secondary_dict): - ''' - Counts the IP Addresses used by GKE (Pods and Services) - Parameters: - config (dict): The dict containing config like clients and limits - read_mask (FieldMask): read_mask to get additional metadata from Cloud Asset Inventory - all_secondary_dict (dict): Dict containing the secondary IP Range information for each subnets in the GCP organization - Returns: - all_secondary_dict (dict): Same dict but populated with GKE IP utilization information - ''' - cluster_secondary_dict = {} - node_secondary_dict = {} - - # Creating cluster dict - # Cluster dict has subnet information - response_cluster = config["clients"]["asset_client"].list_assets( - request={ - "parent": f"organizations/{config['organization']}", - "asset_types": ['container.googleapis.com/Cluster'], - "content_type": 'RESOURCE', - "page_size": config["page_size"], - }) - - for asset in response_cluster: - cluster_project = asset.resource.data['selfLink'].split('/')[5] - cluster_parent = "/".join(asset.resource.data['selfLink'].split('/')[5:10]) - cluster_subnetwork = asset.resource.data['subnetwork'] - cluster_service_rangeName = asset.resource.data['ipAllocationPolicy'][ - 'servicesSecondaryRangeName'] - - cluster_secondary_dict[f"{cluster_parent}/Service"] = { - "project": cluster_project, - "subnet": cluster_subnetwork, - "secondaryRange_name": cluster_service_rangeName, - 'used_ip_addresses': 0, - } - - for node_pool in asset.resource.data['nodePools']: - nodepool_name = node_pool['name'] - node_IPrange = node_pool['networkConfig']['podRange'] - cluster_secondary_dict[f"{cluster_parent}/{nodepool_name}"] = { - "project": cluster_project, - "subnet": cluster_subnetwork, - "secondaryRange_name": node_IPrange, - 'used_ip_addresses': 0, - } - - # Creating node dict - # Node dict allows 1:1 mapping of pod IP utilization, and which secondary Range it is using - response_node = config["clients"]["asset_client"].search_all_resources( - request={ - "scope": f"organizations/{config['organization']}", - "asset_types": ['k8s.io/Node'], - "read_mask": read_mask, - "page_size": config["page_size"], - }) - - for asset in response_node: - # Node name link format: - # "//container.googleapis.com/projects////clusters//k8s/nodes/" - node_parent = "/".join(asset.name.split('/')[4:9]) - node_name = asset.name.split('/')[-1] - node_full_name = f"{node_parent}/{node_name}" - - for versioned in asset.versioned_resources: - node_secondary_dict[node_full_name] = { - 'node_parent': - node_parent, - 'this_node_pool': - versioned.resource['metadata']['labels'] - ['cloud.google.com/gke-nodepool'], - 'used_ip_addresses': - 0 - } - - # Counting IP addresses used by pods in GKE - response_pods = config["clients"]["asset_client"].search_all_resources( - request={ - "scope": f"organizations/{config['organization']}", - "asset_types": ['k8s.io/Pod'], - "read_mask": read_mask, - "page_size": config["page_size"], - }) - - for asset in response_pods: - # Pod name link format: - # "//container.googleapis.com/projects////clusters//k8s/namespaces//pods/" - pod_parent = "/".join(asset.name.split('/')[4:9]) - - for versioned in asset.versioned_resources: - cur_PodIP = versioned.resource['status']['podIP'] - cur_HostIP = versioned.resource['status']['hostIP'] - host_node_name = versioned.resource['spec']['nodeName'] - pod_full_path = f"{pod_parent}/{host_node_name}" - - # A check to make sure pod is not using node IP - if cur_PodIP != cur_HostIP: - node_secondary_dict[pod_full_path]['used_ip_addresses'] += 1 - - # Counting IP addresses used by Service in GKE - response_service = config["clients"]["asset_client"].search_all_resources( - request={ - "scope": f"organizations/{config['organization']}", - "asset_types": ['k8s.io/Service'], - "read_mask": read_mask, - "page_size": config["page_size"], - }) - - for asset in response_service: - service_parent = "/".join(asset.name.split('/')[4:9]) - service_fullpath = f"{service_parent}/Service" - cluster_secondary_dict[service_fullpath]['used_ip_addresses'] += 1 - - for item in node_secondary_dict.values(): - itemKey = f"{item['node_parent']}/{item['this_node_pool']}" - cluster_secondary_dict[itemKey]['used_ip_addresses'] += item['used_ip_addresses'] - - for item in cluster_secondary_dict.values(): - itemKey = f"{item['subnet']}/{item['secondaryRange_name']}" - all_secondary_dict[item['project']][itemKey]['used_ip_addresses'] += item[ - 'used_ip_addresses'] - - -def compute_secondary_utilization(config, all_secondary_dict): - ''' - Counts resources (GKE, GCE) using IPs in secondary ranges. - Parameters: - config (dict): Dict containing config like clients and limits - all_secondary_dict (dict): Dict containing the secondary IP Range information for each subnets in the GCP organization - Returns: - None - ''' - read_mask = field_mask_pb2.FieldMask() - read_mask.FromJsonString('name,versionedResources') - - compute_GKE_secondaryIP_utilization(config, read_mask, all_secondary_dict) - # TODO: Other Secondary IP like GCE VM using alias IPs - - -def get_secondaries(config, metrics_dict): - ''' - Writes all secondary rang IP address usage metrics to custom metrics. - Parameters: - config (dict): The dict containing config like clients and limits - Returns: - None - ''' - - secondaryRange_dict = get_all_secondaryRange(config) - # Updates all_subnets_dict with the IP utilization info - compute_secondary_utilization(config, secondaryRange_dict) - - timestamp = time.time() - for project_id in config["monitored_projects"]: - if project_id not in secondaryRange_dict: - continue - for secondary_dict in secondaryRange_dict[project_id].values(): - ip_utilization = 0 - if secondary_dict['used_ip_addresses'] > 0: - ip_utilization = secondary_dict['used_ip_addresses'] / secondary_dict[ - 'total_ip_addresses'] - - # Building unique identifier with subnet region/name - subnet_id = f"{secondary_dict['region']}/{secondary_dict['name']}" - metric_labels = { - 'project': project_id, - 'network_name': secondary_dict['network_name'], - 'region' : secondary_dict['region'], - 'subnet' : secondary_dict['subnetName'], - 'secondary_range' : secondary_dict['name'] - } - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_subnet"] - ["ip_usage_per_secondaryRange"]["usage"]["name"], - secondary_dict['used_ip_addresses'], metric_labels, - timestamp=timestamp) - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_subnet"] - ["ip_usage_per_secondaryRange"]["limit"]["name"], - secondary_dict['total_ip_addresses'], metric_labels, - timestamp=timestamp) - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_subnet"] - ["ip_usage_per_secondaryRange"]["utilization"]["name"], - ip_utilization, metric_labels, timestamp=timestamp) - - print("Buffered metrics for secondary ip utilization for VPCs in project", - project_id) diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/subnets.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/subnets.py deleted file mode 100644 index 46fbc7564..000000000 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/subnets.py +++ /dev/null @@ -1,373 +0,0 @@ -# -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import time - -from . import metrics -from google.protobuf import field_mask_pb2 -from google.protobuf.json_format import MessageToDict -import ipaddress - - -def get_all_subnets(config): - ''' - Returns a dictionary with subnet level informations (such as IP utilization) - Parameters: - config (dict): The dict containing config like clients and limits - Returns: - subnet_dict (dictionary of String: dictionary): Key is the project_id, value is a nested dictionary with subnet_region/subnet_name as the key. - ''' - subnet_dict = {} - read_mask = field_mask_pb2.FieldMask() - read_mask.FromJsonString('name,versionedResources') - - response = config["clients"]["asset_client"].search_all_resources( - request={ - "scope": f"organizations/{config['organization']}", - "asset_types": ['compute.googleapis.com/Subnetwork'], - "read_mask": read_mask, - "page_size": config["page_size"], - }) - - for asset in response: - for versioned in asset.versioned_resources: - subnet_name = "" - network_name = "" - project_id = "" - ip_cidr_range = "" - subnet_region = "" - - for field_name, field_value in versioned.resource.items(): - if field_name == 'name': - subnet_name = field_value - elif field_name == 'network': - # Network self link format: - # "https://www.googleapis.com/compute/v1/projects//global/networks/" - project_id = field_value.split('/')[6] - network_name = field_value.split('/')[-1] - elif field_name == 'ipCidrRange': - ip_cidr_range = field_value - elif field_name == 'region': - subnet_region = field_value.split('/')[-1] - - net = ipaddress.ip_network(ip_cidr_range) - # Note that 4 IP addresses are reserved by GCP in all subnets - # Source: https://cloud.google.com/vpc/docs/subnets#reserved_ip_addresses_in_every_subnet - total_ip_addresses = int(net.num_addresses) - 4 - - if project_id not in subnet_dict: - subnet_dict[project_id] = {} - subnet_dict[project_id][f"{subnet_region}/{subnet_name}"] = { - 'name': subnet_name, - 'region': subnet_region, - 'ip_cidr_range': ip_cidr_range, - 'total_ip_addresses': total_ip_addresses, - 'used_ip_addresses': 0, - 'network_name': network_name - } - - return subnet_dict - - -def compute_subnet_utilization_vms(config, read_mask, all_subnets_dict): - ''' - Counts VMs using private IPs in the different subnets. - Parameters: - config (dict): Dict containing config like clients and limits - read_mask (FieldMask): read_mask to get additional metadata from Cloud Asset Inventory - all_subnets_dict (dict): Dict containing the information for each subnets in the GCP organization - Returns: - None - ''' - response_vm = config["clients"]["asset_client"].search_all_resources( - request={ - "scope": f"organizations/{config['organization']}", - "asset_types": ["compute.googleapis.com/Instance"], - "read_mask": read_mask, - "page_size": config["page_size"], - }) - - # Counting IP addresses for GCE instances (VMs) - for asset in response_vm: - for versioned in asset.versioned_resources: - for field_name, field_value in versioned.resource.items(): - # TODO: Handle multi-NIC - if field_name == 'networkInterfaces': - response_dict = MessageToDict(list(field_value._pb)[0]) - # Subnet self link: - # https://www.googleapis.com/compute/v1/projects//regions//subnetworks/ - subnet_region = response_dict['subnetwork'].split('/')[-3] - subnet_name = response_dict['subnetwork'].split('/')[-1] - # Network self link: - # https://www.googleapis.com/compute/v1/projects//global/networks/ - project_id = response_dict['network'].split('/')[6] - network_name = response_dict['network'].split('/')[-1] - - all_subnets_dict[project_id][f"{subnet_region}/{subnet_name}"][ - 'used_ip_addresses'] += 1 - - -def compute_subnet_utilization_ilbs(config, read_mask, all_subnets_dict): - ''' - Counts ILBs using private IPs in the different subnets. - Parameters: - config (dict): Dict containing config like clients and limits - read_mask (FieldMask): read_mask to get additional metadata from Cloud Asset Inventory - all_subnets_dict (dict): Dict containing the information for each subnets in the GCP organization - Returns: - None - ''' - response_ilb = config["clients"]["asset_client"].search_all_resources( - request={ - "scope": f"organizations/{config['organization']}", - "asset_types": ["compute.googleapis.com/ForwardingRule"], - "read_mask": read_mask, - "page_size": config["page_size"], - }) - - for asset in response_ilb: - internal = False - psc = False - project_id = '' - subnet_name = '' - subnet_region = '' - address = '' - network = '' - for versioned in asset.versioned_resources: - for field_name, field_value in versioned.resource.items(): - if 'loadBalancingScheme' in field_name and field_value in [ - 'INTERNAL', 'INTERNAL_MANAGED' - ]: - internal = True - # We want to count only accepted PSC endpoint Forwarding Rule - # If the PSC endpoint Forwarding Rule is pending, we will count it in the reserved IP addresses - elif field_name == 'pscConnectionStatus' and field_value == 'ACCEPTED': - psc = True - elif field_name == 'IPAddress': - address = field_value - elif field_name == 'network': - project_id = field_value.split('/')[6] - network = field_value.split('/')[-1] - elif 'subnetwork' in field_name: - subnet_name = field_value.split('/')[-1] - subnet_region = field_value.split('/')[-3] - - if internal: - all_subnets_dict[project_id][f"{subnet_region}/{subnet_name}"][ - 'used_ip_addresses'] += 1 - elif psc: - # PSC endpoint asset doesn't contain the subnet information in Asset Inventory - # We need to find the correct subnet with IP address matching - ip_address = ipaddress.ip_address(address) - for subnet_key, subnet_dict in all_subnets_dict[project_id].items(): - if subnet_dict["network_name"] == network: - if ip_address in ipaddress.ip_network(subnet_dict['ip_cidr_range']): - all_subnets_dict[project_id][subnet_key]['used_ip_addresses'] += 1 - - -def compute_subnet_utilization_addresses(config, read_mask, all_subnets_dict): - ''' - Counts reserved IP addresses in the different subnets. - Parameters: - config (dict): Dict containing config like clients and limits - read_mask (FieldMask): read_mask to get additional metadata from Cloud Asset Inventory - all_subnets_dict (dict): Dict containing the information for each subnets in the GCP organization - Returns: - None - ''' - response_reserved_ips = config["clients"][ - "asset_client"].search_all_resources( - request={ - "scope": f"organizations/{config['organization']}", - "asset_types": ["compute.googleapis.com/Address"], - "read_mask": read_mask, - "page_size": config["page_size"], - }) - - # Counting IP addresses for GCE Reserved IPs (ex: PSC, Cloud DNS Inbound policies, reserved GCE IPs) - for asset in response_reserved_ips: - purpose = "" - status = "" - project_id = "" - network_name = "" - subnet_name = "" - subnet_region = "" - address = "" - prefixLength = "" - address_name = "" - for versioned in asset.versioned_resources: - for field_name, field_value in versioned.resource.items(): - if field_name == 'name': - address_name = field_value - if field_name == 'purpose': - purpose = field_value - elif field_name == 'region': - subnet_region = field_value.split('/')[-1] - elif field_name == 'status': - status = field_value - elif field_name == 'address': - address = field_value - elif field_name == 'network': - network_name = field_value.split('/')[-1] - project_id = field_value.split('/')[6] - elif field_name == 'subnetwork': - subnet_name = field_value.split('/')[-1] - project_id = field_value.split('/')[6] - elif field_name == 'prefixLength': - prefixLength = field_value - - # Rserved IP addresses for GCE instances or PSC Forwarding Rule PENDING state - if purpose == "GCE_ENDPOINT" and status == "RESERVED": - all_subnets_dict[project_id][f"{subnet_region}/{subnet_name}"][ - 'used_ip_addresses'] += 1 - # Cloud DNS inbound policy - elif purpose == "DNS_RESOLVER": - all_subnets_dict[project_id][f"{subnet_region}/{subnet_name}"][ - 'used_ip_addresses'] += 1 - # PSA Range for Cloud SQL, MemoryStore, etc. - elif purpose == "VPC_PEERING": - ip_range = f"{address}/{int(prefixLength)}" - net = ipaddress.ip_network(ip_range) - # Note that 4 IP addresses are reserved by GCP in all subnets - # Source: https://cloud.google.com/vpc/docs/subnets#reserved_ip_addresses_in_every_subnet - total_ip_addresses = int(net.num_addresses) - 4 - all_subnets_dict[project_id][f"psa/{address_name}"] = { - 'name': f"psa/{address_name}", - 'region': subnet_region, - 'ip_cidr_range': ip_range, - 'total_ip_addresses': total_ip_addresses, - 'used_ip_addresses': 0, - 'network_name': network_name - } - - -def compute_subnet_utilization_redis(config, read_mask, all_subnets_dict): - ''' - Counts Redis (Memorystore) instances using private IPs in the different subnets. - Parameters: - config (dict): Dict containing config like clients and limits - read_mask (FieldMask): read_mask to get additional metadata from Cloud Asset Inventory - all_subnets_dict (dict): Dict containing the information for each subnets in the GCP organization - Returns: - None - ''' - response_redis = config["clients"]["asset_client"].search_all_resources( - request={ - "scope": f"organizations/{config['organization']}", - "asset_types": ["redis.googleapis.com/Instance"], - "read_mask": read_mask, - "page_size": config["page_size"], - }) - - for asset in response_redis: - ip_range = "" - connect_mode = "" - network_name = "" - project_id = "" - region = "" - for versioned in asset.versioned_resources: - for field_name, field_value in versioned.resource.items(): - if field_name == 'locationId': - region = field_value[0:-2] - if field_name == 'authorizedNetwork': - network_name = field_value.split('/')[-1] - project_id = field_value.split('/')[1] - if field_name == 'reservedIpRange': - ip_range = field_value - if field_name == 'connectMode': - connect_mode = field_value - - # Only handling PSA for Redis for now - if connect_mode == "PRIVATE_SERVICE_ACCESS": - redis_ip_range = ipaddress.ip_network(ip_range) - for subnet_key, subnet_dict in all_subnets_dict[project_id].items(): - if subnet_dict["network_name"] == network_name: - # Reddis instance asset doesn't contain the subnet information in Asset Inventory - # We need to find the correct subnet range with IP address matching to compute the utilization - if redis_ip_range.overlaps( - ipaddress.ip_network(subnet_dict['ip_cidr_range'])): - all_subnets_dict[project_id][subnet_key][ - 'used_ip_addresses'] += redis_ip_range.num_addresses - all_subnets_dict[project_id][subnet_key]['region'] = region - - -def compute_subnet_utilization(config, all_subnets_dict): - ''' - Counts resources (VMs, ILBs, reserved IPs) using private IPs in the different subnets. - Parameters: - config (dict): Dict containing config like clients and limits - all_subnets_dict (dict): Dict containing the information for each subnets in the GCP organization - Returns: - None - ''' - read_mask = field_mask_pb2.FieldMask() - read_mask.FromJsonString('name,versionedResources') - - compute_subnet_utilization_vms(config, read_mask, all_subnets_dict) - compute_subnet_utilization_ilbs(config, read_mask, all_subnets_dict) - compute_subnet_utilization_addresses(config, read_mask, all_subnets_dict) - # TODO: Other PSA services such as FileStore, Cloud SQL - compute_subnet_utilization_redis(config, read_mask, all_subnets_dict) - - # TODO: Handle secondary ranges and count GKE pods - - -def get_subnets(config, metrics_dict): - ''' - Writes all subnet metrics to custom metrics. - - Parameters: - config (dict): The dict containing config like clients and limits - Returns: - None - ''' - - all_subnets_dict = get_all_subnets(config) - # Updates all_subnets_dict with the IP utilization info - compute_subnet_utilization(config, all_subnets_dict) - - timestamp = time.time() - for project_id in config["monitored_projects"]: - if project_id not in all_subnets_dict: - continue - for subnet_dict in all_subnets_dict[project_id].values(): - ip_utilization = 0 - if subnet_dict['used_ip_addresses'] > 0: - ip_utilization = subnet_dict['used_ip_addresses'] / subnet_dict[ - 'total_ip_addresses'] - - # Building unique identifier with subnet region/name - subnet_id = f"{subnet_dict['region']}/{subnet_dict['name']}" - metric_labels = { - 'project': project_id, - 'network_name': subnet_dict['network_name'], - 'subnet_id': subnet_id - } - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_subnet"]["ip_usage_per_subnet"] - ["usage"]["name"], subnet_dict['used_ip_addresses'], metric_labels, - timestamp=timestamp) - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_subnet"]["ip_usage_per_subnet"] - ["limit"]["name"], subnet_dict['total_ip_addresses'], metric_labels, - timestamp=timestamp) - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_subnet"]["ip_usage_per_subnet"] - ["utilization"]["name"], ip_utilization, metric_labels, - timestamp=timestamp) - - print("Buffered metrics for subnet ip utilization for VPCs in project", - project_id) diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/vpc_firewalls.py b/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/vpc_firewalls.py deleted file mode 100644 index f9fec79a7..000000000 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/metrics/vpc_firewalls.py +++ /dev/null @@ -1,122 +0,0 @@ -# -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import re -import time - -from collections import defaultdict -from pydoc import doc -from collections import defaultdict -from google.protobuf import field_mask_pb2 -from . import metrics, networks, limits - - -def get_firewalls_dict(config: dict): - ''' - Calls the Asset Inventory API to get all VPC Firewall Rules under the GCP organization. - - Parameters: - config (dict): The dict containing config like clients and limits - Returns: - firewalls_dict (dictionary of dictionary: int): Keys are projects, subkeys are networks, values count #of VPC Firewall Rules - ''' - - firewalls_dict = defaultdict(int) - read_mask = field_mask_pb2.FieldMask() - read_mask.FromJsonString('name,versionedResources') - - response = config["clients"]["asset_client"].search_all_resources( - request={ - "scope": f"organizations/{config['organization']}", - "asset_types": ["compute.googleapis.com/Firewall"], - "read_mask": read_mask, - "page_size": config["page_size"], - }) - for resource in response: - project_id = re.search("(compute.googleapis.com/projects/)([\w\-\d]+)", - resource.name).group(2) - network_name = "" - for versioned in resource.versioned_resources: - for field_name, field_value in versioned.resource.items(): - if field_name == "network": - network_name = re.search("[a-z0-9\-]*$", field_value).group(0) - firewalls_dict[project_id] = defaultdict( - int - ) if not project_id in firewalls_dict else firewalls_dict[project_id] - firewalls_dict[project_id][ - network_name] = 1 if not network_name in firewalls_dict[ - project_id] else firewalls_dict[project_id][network_name] + 1 - break - break - return firewalls_dict - - -def get_firewalls_data(config, metrics_dict, project_quotas_dict, - firewalls_dict): - ''' - Gets the data for VPC Firewall Rules per VPC Network and writes it to the metric defined in vpc_firewalls_metric. - - Parameters: - config (dict): The dict containing config like clients and limits - metrics_dict (dictionary of dictionary of string: string): metrics names and descriptions. - project_quotas_dict (dictionary of string:int): Dictionary with the network link as key and the limit as value. - firewalls_dict (dictionary of of dictionary of string: string): Keys are projects, subkeys are networks, values count #of VPC Firewall Rules - Returns: - None - ''' - - timestamp = time.time() - for project_id in config["monitored_projects"]: - - current_quota_limit = project_quotas_dict[project_id]['global']["firewalls"] - if current_quota_limit is None: - print( - f"Could not determine VPC firewal rules metric for projects/{project_id} due to missing quotas" - ) - continue - - network_dict = networks.get_networks(config, project_id) - - project_usage = 0 - for net in network_dict: - usage = 0 - if project_id in firewalls_dict and net['network_name'] in firewalls_dict[ - project_id]: - usage = firewalls_dict[project_id][net['network_name']] - project_usage += usage - metric_labels = { - 'project': project_id, - 'network_name': net['network_name'] - } - metrics.append_data_to_series_buffer( - config, - metrics_dict["metrics_per_project"][f"firewalls"]["usage"]["name"], - usage, metric_labels, timestamp=timestamp) - - metric_labels = {'project': project_id} - # firewall quotas are per project, not per single VPC - metrics.append_data_to_series_buffer( - config, - metrics_dict["metrics_per_project"][f"firewalls"]["limit"]["name"], - current_quota_limit['limit'], metric_labels, timestamp=timestamp) - metrics.append_data_to_series_buffer( - config, metrics_dict["metrics_per_project"][f"firewalls"]["utilization"] - ["name"], project_usage / current_quota_limit['limit'] - if current_quota_limit['limit'] != 0 else 0, metric_labels, - timestamp=timestamp) - print( - f"Buffered number of VPC Firewall Rules to metric for projects/{project_id}" - ) diff --git a/blueprints/cloud-operations/network-dashboard/cloud-function/requirements.txt b/blueprints/cloud-operations/network-dashboard/cloud-function/requirements.txt deleted file mode 100644 index d56134822..000000000 --- a/blueprints/cloud-operations/network-dashboard/cloud-function/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -regex==2022.3.2 -google-api-python-client==2.39.0 -google-auth==2.6.0 -google-auth-httplib2==0.1.0 -google-cloud-logging==3.0.0 -google-cloud-monitoring==2.9.1 -oauth2client==4.1.3 -google-api-core==2.7.0 -PyYAML==6.0 -google-cloud-asset==3.8.1 -functions-framework==3.* \ No newline at end of file diff --git a/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json b/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json index e26d69264..361eb8214 100644 --- a/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json +++ b/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json @@ -1,5 +1,4 @@ { - "category": "CUSTOM", "displayName": "quotas_utilization", "mosaicLayout": { "columns": 12, @@ -7,7 +6,7 @@ { "height": 4, "widget": { - "title": "internal_forwarding_rules_l4_utilization", + "title": "Internal L4 forwarding rules utilization", "xyChart": { "chartOptions": { "mode": "COLOR" @@ -18,13 +17,12 @@ "plotType": "LINE", "targetAxis": "Y1", "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, - "filter": "metric.type=\"custom.googleapis.com/internal_forwarding_rules_l4_utilization\" resource.type=\"global\"", + "filter": "metric.type=\"custom.googleapis.com/netmon/network/forwarding_rules_l4_used_ratio\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "1800s", "perSeriesAligner": "ALIGN_MEAN" @@ -40,14 +38,12 @@ } } }, - "width": 6, - "xPos": 0, - "yPos": 0 + "width": 6 }, { "height": 4, "widget": { - "title": "internal_forwarding_rules_l7_utilization", + "title": "Internal L7 forwarding rules utilization", "xyChart": { "chartOptions": { "mode": "COLOR" @@ -58,13 +54,12 @@ "plotType": "LINE", "targetAxis": "Y1", "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, - "filter": "metric.type=\"custom.googleapis.com/internal_forwarding_rules_l7_utilization\" resource.type=\"global\"", + "filter": "metric.type=\"custom.googleapis.com/netmon/network/forwarding_rules_l4_used_ratio\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "60s", "perSeriesAligner": "ALIGN_MEAN" @@ -81,13 +76,12 @@ } }, "width": 6, - "xPos": 0, "yPos": 12 }, { "height": 4, "widget": { - "title": "number_of_instances_utilization", + "title": "Instance utilization", "xyChart": { "chartOptions": { "mode": "COLOR" @@ -98,13 +92,12 @@ "plotType": "LINE", "targetAxis": "Y1", "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, - "filter": "metric.type=\"custom.googleapis.com/number_of_instances_utilization\" resource.type=\"global\"", + "filter": "metric.type=\"custom.googleapis.com/netmon/network/instances_used_ratio\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "60s", "perSeriesAligner": "ALIGN_MEAN" @@ -121,13 +114,12 @@ } }, "width": 6, - "xPos": 0, "yPos": 8 }, { "height": 4, "widget": { - "title": "number_of_vpc_peerings_utilization", + "title": "Peering utilization", "xyChart": { "chartOptions": { "mode": "COLOR" @@ -138,13 +130,12 @@ "plotType": "LINE", "targetAxis": "Y1", "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, - "filter": "metric.type=\"custom.googleapis.com/number_of_vpc_peerings_utilization\" resource.type=\"global\"", + "filter": "metric.type=\"custom.googleapis.com/netmon/network/peerings_total_used_ratio\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "60s", "perSeriesAligner": "ALIGN_MEAN" @@ -167,7 +158,7 @@ { "height": 4, "widget": { - "title": "number_of_active_vpc_peerings_utilization", + "title": "Active peering utilization", "xyChart": { "chartOptions": { "mode": "COLOR" @@ -178,13 +169,12 @@ "plotType": "LINE", "targetAxis": "Y1", "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, - "filter": "metric.type=\"custom.googleapis.com/number_of_active_vpc_peerings_utilization\" resource.type=\"global\"", + "filter": "metric.type=\"custom.googleapis.com/netmon/network/peerings_active_used_ratio\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "60s", "perSeriesAligner": "ALIGN_INTERPOLATE" @@ -201,13 +191,12 @@ } }, "width": 6, - "xPos": 0, "yPos": 4 }, { "height": 4, "widget": { - "title": "subnet_IP_ranges_ppg_utilization", + "title": "Peering group internal L4 forwarding rules utilization", "xyChart": { "chartOptions": { "mode": "COLOR" @@ -218,13 +207,12 @@ "plotType": "LINE", "targetAxis": "Y1", "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, - "filter": "metric.type=\"custom.googleapis.com/number_of_subnet_IP_ranges_ppg_utilization\" resource.type=\"global\"", + "filter": "metric.type=\"custom.googleapis.com/netmon/peering_group/forwarding_rules_l4_used_ratio\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_MEAN" @@ -241,13 +229,12 @@ } }, "width": 6, - "xPos": 0, - "yPos": 16 + "xPos": 6 }, { "height": 4, "widget": { - "title": "internal_forwarding_rules_l4_ppg_utilization", + "title": "Peering group internal L7 forwarding rules utilization", "xyChart": { "chartOptions": { "mode": "COLOR" @@ -258,53 +245,12 @@ "plotType": "LINE", "targetAxis": "Y1", "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, - "filter": "metric.type=\"custom.googleapis.com/internal_forwarding_rules_l4_ppg_utilization\" resource.type=\"global\"", - "secondaryAggregation": { - "alignmentPeriod": "3600s", - "perSeriesAligner": "ALIGN_MEAN" - } - } - } - } - ], - "timeshiftDuration": "0s", - "yAxis": { - "label": "y1Axis", - "scale": "LINEAR" - } - } - }, - "width": 6, - "xPos": 6, - "yPos": 0 - }, - { - "height": 4, - "widget": { - "title": "internal_forwarding_rules_l7_ppg_utilization", - "xyChart": { - "chartOptions": { - "mode": "COLOR" - }, - "dataSets": [ - { - "minAlignmentPeriod": "3600s", - "plotType": "LINE", - "targetAxis": "Y1", - "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", - "timeSeriesFilter": { - "aggregation": { - "alignmentPeriod": "3600s", - "perSeriesAligner": "ALIGN_NEXT_OLDER" - }, - "filter": "metric.type=\"custom.googleapis.com/internal_forwarding_rules_l7_ppg_utilization\" resource.type=\"global\"", + "filter": "metric.type=\"custom.googleapis.com/netmon/peering_group/forwarding_rules_l7_used_ratio\" resource.type=\"global\"", "secondaryAggregation": { "alignmentPeriod": "60s", "perSeriesAligner": "ALIGN_MEAN" @@ -327,7 +273,7 @@ { "height": 4, "widget": { - "title": "number_of_instances_ppg_utilization", + "title": "Peering group instance utilization", "xyChart": { "chartOptions": { "mode": "COLOR" @@ -338,13 +284,12 @@ "plotType": "LINE", "targetAxis": "Y1", "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", "timeSeriesFilter": { "aggregation": { "alignmentPeriod": "3600s", "perSeriesAligner": "ALIGN_NEXT_OLDER" }, - "filter": "metric.type=\"custom.googleapis.com/number_of_instances_ppg_utilization\" resource.type=\"global\"" + "filter": "metric.type=\"custom.googleapis.com/netmon/peering_group/instances_used_ratio\" resource.type=\"global\"" } } } @@ -363,24 +308,26 @@ { "height": 4, "widget": { - "title": "dynamic_routes_per_network_utilization", + "title": "Peering group dynamic route utilization", "xyChart": { "chartOptions": { "mode": "COLOR" }, "dataSets": [ { - "minAlignmentPeriod": "60s", + "minAlignmentPeriod": "3600s", "plotType": "LINE", "targetAxis": "Y1", "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", "timeSeriesFilter": { "aggregation": { - "alignmentPeriod": "60s", - "perSeriesAligner": "ALIGN_MEAN" + "alignmentPeriod": "3600s", + "perSeriesAligner": "ALIGN_NEXT_OLDER" }, - "filter": "metric.type=\"custom.googleapis.com/dynamic_routes_per_network_utilization\" resource.type=\"global\"" + "filter": "metric.type=\"custom.googleapis.com/netmon/peering_group/routes_dynamic_used_ratio\" resource.type=\"global\"", + "secondaryAggregation": { + "alignmentPeriod": "60s" + } } } } @@ -393,34 +340,35 @@ } }, "width": 6, - "xPos": 0, "yPos": 20 }, { "height": 4, "widget": { - "title": "firewalls_per_project_vpc_usage", + "title": "Project firewall rules used ratio", "xyChart": { "chartOptions": { "mode": "COLOR" }, "dataSets": [ { - "minAlignmentPeriod": "60s", + "minAlignmentPeriod": "3600s", "plotType": "LINE", "targetAxis": "Y1", "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", "timeSeriesFilter": { "aggregation": { - "alignmentPeriod": "60s", + "alignmentPeriod": "3600s", "crossSeriesReducer": "REDUCE_SUM", "groupByFields": [ "metric.label.\"project\"" ], - "perSeriesAligner": "ALIGN_MEAN" + "perSeriesAligner": "ALIGN_NEXT_OLDER" }, - "filter": "metric.type=\"custom.googleapis.com/firewalls_per_project_vpc_usage\" resource.type=\"global\"" + "filter": "metric.type=\"custom.googleapis.com/netmon/project/firewall_rules_used_ratio\" resource.type=\"global\"", + "secondaryAggregation": { + "alignmentPeriod": "60s" + } } } } @@ -433,106 +381,185 @@ } }, "width": 6, - "xPos": 0, - "yPos": 32 - }, - { - "height": 4, - "widget": { - "title": "firewalls_per_project_utilization", - "xyChart": { - "chartOptions": { - "mode": "COLOR" - }, - "dataSets": [ - { - "minAlignmentPeriod": "60s", - "plotType": "LINE", - "targetAxis": "Y1", - "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", - "timeSeriesFilter": { - "aggregation": { - "alignmentPeriod": "60s", - "crossSeriesReducer": "REDUCE_MAX", - "groupByFields": [ - "metric.label.\"project\"" - ], - "perSeriesAligner": "ALIGN_MAX" - }, - "filter": "metric.type=\"custom.googleapis.com/firewalls_per_project_utilization\" resource.type=\"global\"" - } - } - } - ], - "timeshiftDuration": "0s", - "yAxis": { - "label": "y1Axis", - "scale": "LINEAR" - } - } - }, - "width": 6, - "xPos": 6, - "yPos": 32 - }, - { - "height": 4, - "widget": { - "title": "tuples_per_firewall_policy_utilization", - "xyChart": { - "chartOptions": { - "mode": "COLOR" - }, - "dataSets": [ - { - "minAlignmentPeriod": "60s", - "plotType": "LINE", - "targetAxis": "Y1", - "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", - "timeSeriesFilter": { - "aggregation": { - "alignmentPeriod": "60s", - "perSeriesAligner": "ALIGN_MEAN" - }, - "filter": "metric.type=\"custom.googleapis.com/firewall_policy_tuples_per_policy_utilization\" resource.type=\"global\"" - } - } - } - ], - "timeshiftDuration": "0s", - "yAxis": { - "label": "y1Axis", - "scale": "LINEAR" - } - } - }, - "width": 6, - "xPos": 6, "yPos": 28 }, { "height": 4, "widget": { - "title": "ip_addresses_per_subnet_utilization", + "title": "Firewall policy tuples used ratio", "xyChart": { "chartOptions": { "mode": "COLOR" }, "dataSets": [ { - "minAlignmentPeriod": "60s", + "minAlignmentPeriod": "3600s", "plotType": "LINE", "targetAxis": "Y1", "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", "timeSeriesFilter": { "aggregation": { - "alignmentPeriod": "60s", - "perSeriesAligner": "ALIGN_MEAN" + "alignmentPeriod": "3600s", + "perSeriesAligner": "ALIGN_NEXT_OLDER" }, - "filter": "metric.type=\"custom.googleapis.com/ip_addresses_per_subnet_utilization\" resource.type=\"global\"" + "filter": "metric.type=\"custom.googleapis.com/netmon/firewall_policy/tuples_used_ratio\" resource.type=\"global\"", + "secondaryAggregation": { + "alignmentPeriod": "60s" + } + } + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + "width": 6, + "xPos": 6, + "yPos": 24 + }, + { + "height": 4, + "widget": { + "title": "IP addressed per subnetwork used ratio", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "3600s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "3600s", + "perSeriesAligner": "ALIGN_NEXT_OLDER" + }, + "filter": "metric.type=\"custom.googleapis.com/netmon/subnetwork/addresses_used_ratio\" resource.type=\"global\"", + "secondaryAggregation": { + "alignmentPeriod": "60s" + } + } + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + "width": 6, + "yPos": 16 + }, + { + "height": 4, + "widget": { + "title": "Project static routes used", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "3600s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "3600s", + "crossSeriesReducer": "REDUCE_SUM", + "groupByFields": [ + "metric.label.\"project\"" + ], + "perSeriesAligner": "ALIGN_NEXT_OLDER" + }, + "filter": "metric.type=\"custom.googleapis.com/netmon/project/routes_static_used_ratio\" resource.type=\"global\"", + "secondaryAggregation": { + "alignmentPeriod": "60s" + } + } + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + "width": 6, + "xPos": 6, + "yPos": 20 + }, + { + "height": 4, + "widget": { + "title": "Peering group static routes used", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "3600s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "3600s", + "perSeriesAligner": "ALIGN_NEXT_OLDER" + }, + "filter": "metric.type=\"custom.googleapis.com/netmon/peering_group/routes_static_used_ratio\" resource.type=\"global\"", + "secondaryAggregation": { + "alignmentPeriod": "60s" + } + } + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + "width": 6, + "yPos": 24 + }, + { + "height": 4, + "widget": { + "title": "Addresses used ratio per psa range [NEXT OLDER]", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "3600s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "3600s", + "perSeriesAligner": "ALIGN_NEXT_OLDER" + }, + "filter": "metric.type=\"custom.googleapis.com/netmon/network/psa/addresses_used_ratio\" resource.type=\"global\"", + "secondaryAggregation": { + "alignmentPeriod": "60s" + } } } } @@ -547,203 +574,6 @@ "width": 6, "xPos": 6, "yPos": 16 - }, - { - "height": 4, - "widget": { - "title": "dynamic_routes_ppg_utilization", - "xyChart": { - "chartOptions": { - "mode": "COLOR" - }, - "dataSets": [ - { - "minAlignmentPeriod": "60s", - "plotType": "LINE", - "targetAxis": "Y1", - "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", - "timeSeriesFilter": { - "aggregation": { - "alignmentPeriod": "60s", - "perSeriesAligner": "ALIGN_MEAN" - }, - "filter": "metric.type=\"custom.googleapis.com/dynamic_routes_per_peering_group_utilization\" resource.type=\"global\"" - } - } - } - ], - "timeshiftDuration": "0s", - "yAxis": { - "label": "y1Axis", - "scale": "LINEAR" - } - } - }, - "width": 6, - "xPos": 6, - "yPos": 20 - }, - { - "height": 4, - "widget": { - "title": "static_routes_per_project_vpc_usage", - "xyChart": { - "chartOptions": { - "mode": "COLOR" - }, - "dataSets": [ - { - "minAlignmentPeriod": "60s", - "plotType": "LINE", - "targetAxis": "Y1", - "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", - "timeSeriesFilter": { - "aggregation": { - "alignmentPeriod": "60s", - "crossSeriesReducer": "REDUCE_SUM", - "groupByFields": [ - "metric.label.\"project\"" - ], - "perSeriesAligner": "ALIGN_MEAN" - }, - "filter": "metric.type=\"custom.googleapis.com/static_routes_per_project_vpc_usage\" resource.type=\"global\"", - "secondaryAggregation": { - "alignmentPeriod": "60s", - "perSeriesAligner": "ALIGN_NONE" - } - } - } - } - ], - "thresholds": [], - "timeshiftDuration": "0s", - "yAxis": { - "label": "y1Axis", - "scale": "LINEAR" - } - } - }, - "width": 6, - "xPos": 0, - "yPos": 24 - }, - { - "height": 4, - "widget": { - "title": "static_routes_per_ppg_utilization", - "xyChart": { - "chartOptions": { - "mode": "COLOR" - }, - "dataSets": [ - { - "minAlignmentPeriod": "60s", - "plotType": "LINE", - "targetAxis": "Y1", - "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", - "timeSeriesFilter": { - "aggregation": { - "alignmentPeriod": "60s", - "perSeriesAligner": "ALIGN_MEAN" - }, - "filter": "metric.type=\"custom.googleapis.com/static_routes_per_peering_group_utilization\" resource.type=\"global\"" - } - } - } - ], - "thresholds": [], - "timeshiftDuration": "0s", - "yAxis": { - "label": "y1Axis", - "scale": "LINEAR" - } - } - }, - "width": 6, - "xPos": 0, - "yPos": 28 - }, - { - "height": 4, - "widget": { - "title": "static_routes_per_project_utilization", - "xyChart": { - "chartOptions": { - "mode": "COLOR" - }, - "dataSets": [ - { - "minAlignmentPeriod": "60s", - "plotType": "LINE", - "targetAxis": "Y1", - "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", - "timeSeriesFilter": { - "aggregation": { - "alignmentPeriod": "60s", - "perSeriesAligner": "ALIGN_MEAN" - }, - "filter": "metric.type=\"custom.googleapis.com/static_routes_per_project_utilization\" resource.type=\"global\"" - } - } - } - ], - "timeshiftDuration": "0s", - "yAxis": { - "label": "y1Axis", - "scale": "LINEAR" - } - } - }, - "width": 6, - "xPos": 6, - "yPos": 24 - }, - { - "height": 4, - "widget": { - "title": "secondary_ip_address_utilization", - "xyChart": { - "chartOptions": { - "mode": "COLOR" - }, - "dataSets": [ - { - "minAlignmentPeriod": "60s", - "plotType": "LINE", - "targetAxis": "Y1", - "timeSeriesQuery": { - "apiSource": "DEFAULT_CLOUD", - "timeSeriesFilter": { - "aggregation": { - "alignmentPeriod": "60s", - "crossSeriesReducer": "REDUCE_NONE", - "perSeriesAligner": "ALIGN_MEAN" - }, - "filter": "metric.type=\"custom.googleapis.com/ip_addresses_per_sr_utilization\" resource.type=\"global\"", - "secondaryAggregation": { - "alignmentPeriod": "60s", - "crossSeriesReducer": "REDUCE_NONE", - "perSeriesAligner": "ALIGN_NONE" - } - } - } - } - ], - "thresholds": [], - "timeshiftDuration": "0s", - "yAxis": { - "label": "y1Axis", - "scale": "LINEAR" - } - } - }, - "width": 6, - "xPos": 0, - "yPos": 36 } ] } diff --git a/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/README.md b/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/README.md new file mode 100644 index 000000000..aa0bdf42e --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/README.md @@ -0,0 +1,89 @@ +# Network Dashboard Discovery via Cloud Function + +This simple Terraform setup allows deploying the [discovery tool for the Network Dashboard](../src/) to a Cloud Function, triggered by a schedule via PubSub. + +GCP resource diagram + +## Project and function-level configuration + +A single project is used both for deploying the function and to collect generated timeseries: writing timeseries to a separate project is not supported here for brevity, but is very simple to implement (basically change the value for `op_project` in the schedule payload queued in PubSub). The project is configured with the required APIs, and it can also optionally be created via the `project_create_config` variable. + +The function uses a dedicated service account which is created for this purpose. Roles to allow discovery can optionally be set at the top-level discovery scope (organization or folder) via the `grant_discovery_iam_roles` variable, those of course require the right set of permissions on the part of the identity running `terraform apply`. The alternative when IAM bindings cannot be managed on the top-level scope, is to assign `roles/compute.viewer` and `roles/cloudasset.viewer` to the function service account from a separate process, or manually in the console. + +A few configuration values for the function which are relevant to this example can also be configured in the `cloud_function_config` variable, particularly the `debug` attribute which turns on verbose logging to help in troubleshooting. + +## Discovery configuration + +Discovery configuration is done via the `discovery_config` variable, which mimicks the set of options available when running the discovery tool in cli mode. Pay particular care in defining the right top-level scope via the `discovery_root` attribute, as this is the root of the hierarchy used to discover Compute resources and it needs to include the individual folders and projects that needs to be monitored, which are defined via the `monitored_folders` and `monitored_projects` attributes. + +The following schematic diagram of a resource hierarchy illustrates the interplay between root scope and monitored resources. The root scope is set to the top-level red folder and completely encloses every resource that needs to be monitored. The blue folder and project are set as monitored defining the actual perimeter used to discover resources. Note that setting the root scope to the blue folder would have resulted in the rightmost project being excluded. + +GCP resource diagram + +This is an example of a working configuration, where the discovery root is set at the org level, but resources used to compute timeseries need to be part of the hierarchy of two specific folders: + +```tfvars +# cloud_function_config = { +# debug = true +# } +discovery_config = { + discovery_root = "organizations/1234567890" + monitored_folders = ["3456789012", "7890123456"] + monitored_projects = [] + # if you have custom quota not returned by the API, compile a file and set + # its pat here; format is described in ../src/custom-quotas.sample + # custom_quota_file = "../src/custom-quotas.yaml" +} +grant_discovery_iam_roles = true +project_create_config = { + billing_account_id = "12345-ABCDEF-12345" + parent_id = "folders/2345678901" +} +project_id = "my-project" +``` + +## Manual triggering for troubleshooting + +If the function crashes or its behaviour is not as expected, you can turn on debugging via the `cloud_function_config.debug` variable attribute, then manually trigger the function from the console by specifying a payload with a single `data` attribute containing the base64-encoded arguments passed to the function by Cloud Scheduler. You can get the pre-computed payload from the `troubleshooting_payload` output: + +```bash +# copy and paste to the function's "Testing" tab in the console +tf output -raw troubleshooting_payload +``` + +## Monitoring dashboard + +A monitoring dashboard can be optionally be deployed int he same project by setting the `dashboard_json_path` variable to the path of a dashboard JSON file. A sample dashboard is in included, and can be deployed with this variable configuration: + +```tfvars +dashboard_json_path = "../dashboards/quotas-utilization.json" +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [discovery_config](variables.tf#L44) | Discovery configuration. Discovery root is the organization or a folder. If monitored folders and projects are empy, every project under the discovery root node will be monitored. | object({…}) | ✓ | | +| [project_id](variables.tf#L90) | Project id where the Cloud Function will be deployed. | string | ✓ | | +| [bundle_path](variables.tf#L17) | Path used to write the intermediate Cloud Function code bundle. | string | | "./bundle.zip" | +| [cloud_function_config](variables.tf#L23) | Optional Cloud Function configuration. | object({…}) | | {} | +| [dashboard_json_path](variables.tf#L38) | Optional monitoring dashboard to deploy. | string | | null | +| [grant_discovery_iam_roles](variables.tf#L62) | Optionally grant required IAM roles to Cloud Function service account. | bool | | false | +| [labels](variables.tf#L69) | Billing labels used for the Cloud Function, and the project if project_create is true. | map(string) | | {} | +| [name](variables.tf#L75) | Name used to create Cloud Function related resources. | string | | "net-dash" | +| [project_create_config](variables.tf#L81) | Optional configuration if project creation is required. | object({…}) | | null | +| [region](variables.tf#L95) | Compute region where the Cloud Function will be deployed. | string | | "europe-west1" | +| [schedule_config](variables.tf#L101) | Schedule timer configuration in crontab format. | string | | "*/30 * * * *" | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [bucket](outputs.tf#L17) | Cloud Function deployment bucket resource. | | +| [cloud-function](outputs.tf#L22) | Cloud Function resource. | | +| [project_id](outputs.tf#L27) | Project id. | | +| [service_account](outputs.tf#L32) | Cloud Function service account. | | +| [troubleshooting_payload](outputs.tf#L40) | Cloud Function payload used for manual triggering. | ✓ | + + diff --git a/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/diagram-scope.png b/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/diagram-scope.png new file mode 100644 index 000000000..6247c1c90 Binary files /dev/null and b/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/diagram-scope.png differ diff --git a/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/diagram.png b/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/diagram.png new file mode 100644 index 000000000..d71540672 Binary files /dev/null and b/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/diagram.png differ diff --git a/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/main.tf b/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/main.tf new file mode 100644 index 000000000..abbea80e2 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/main.tf @@ -0,0 +1,144 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + discovery_roles = ["roles/compute.viewer", "roles/cloudasset.viewer"] +} + +resource "random_string" "default" { + count = var.cloud_function_config.bucket_name == null ? 1 : 0 + length = 8 + special = false + upper = false +} + +module "project" { + source = "../../../../modules/project" + name = var.project_id + billing_account = try(var.project_create_config.billing_account_id, null) + labels = var.project_create_config != null ? var.labels : null + parent = try(var.project_create_config.parent_id, null) + project_create = var.project_create_config != null + services = [ + "cloudasset.googleapis.com", + "cloudbuild.googleapis.com", + "cloudfunctions.googleapis.com", + "cloudscheduler.googleapis.com", + "compute.googleapis.com", + "monitoring.googleapis.com" + ] +} + +module "pubsub" { + source = "../../../../modules/pubsub" + project_id = module.project.project_id + name = var.name + regions = [var.region] + subscriptions = { "${var.name}-default" = null } +} + +module "cloud-function" { + source = "../../../../modules/cloud-function" + project_id = module.project.project_id + name = var.name + bucket_name = coalesce( + var.cloud_function_config.bucket_name, + "${var.name}-${random_string.default.0.id}" + ) + bucket_config = { + location = var.region + } + build_worker_pool = var.cloud_function_config.build_worker_pool_id + bundle_config = { + source_dir = var.cloud_function_config.source_dir + output_path = var.cloud_function_config.bundle_path + } + environment_variables = ( + var.cloud_function_config.debug != true ? {} : { DEBUG = "1" } + ) + function_config = { + entry_point = "main_cf_pubsub" + memory_mb = var.cloud_function_config.memory_mb + timeout_seconds = var.cloud_function_config.timeout_seconds + } + service_account_create = true + trigger_config = { + v1 = { + event = "google.pubsub.topic.publish" + resource = module.pubsub.topic.id + } + } +} + +resource "google_cloud_scheduler_job" "default" { + project = var.project_id + region = var.region + name = var.name + schedule = var.schedule_config + time_zone = "UTC" + + pubsub_target { + attributes = {} + topic_name = module.pubsub.topic.id + data = base64encode(jsonencode({ + discovery_root = var.discovery_config.discovery_root + folders = var.discovery_config.monitored_folders + projects = var.discovery_config.monitored_projects + monitoring_project = module.project.project_id + custom_quota = ( + var.discovery_config.custom_quota_file == null + ? { networks = {}, projects = {} } + : yamldecode(file(var.discovery_config.custom_quota_file)) + ) + })) + } +} + +resource "google_organization_iam_member" "discovery" { + for_each = toset( + var.grant_discovery_iam_roles && + startswith(var.discovery_config.discovery_root, "organizations/") + ? local.discovery_roles + : [] + ) + org_id = split("/", var.discovery_config.discovery_root)[1] + role = each.key + member = module.cloud-function.service_account_iam_email +} + +resource "google_folder_iam_member" "discovery" { + for_each = toset( + var.grant_discovery_iam_roles && + startswith(var.discovery_config.discovery_root, "folders/") + ? local.discovery_roles + : [] + ) + folder = var.discovery_config.discovery_root + role = each.key + member = module.cloud-function.service_account_iam_email +} + +resource "google_project_iam_member" "monitoring" { + project = module.project.project_id + role = "roles/monitoring.metricWriter" + member = module.cloud-function.service_account_iam_email +} + +resource "google_monitoring_dashboard" "dashboard" { + count = var.dashboard_json_path == null ? 0 : 1 + project = var.project_id + dashboard_json = file(var.dashboard_json_path) +} diff --git a/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/outputs.tf b/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/outputs.tf new file mode 100644 index 000000000..0c2c50abe --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/outputs.tf @@ -0,0 +1,46 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +output "bucket" { + description = "Cloud Function deployment bucket resource." + value = module.cloud-function.bucket +} + +output "cloud-function" { + description = "Cloud Function resource." + value = module.cloud-function.function +} + +output "project_id" { + description = "Project id." + value = module.project.project_id +} + +output "service_account" { + description = "Cloud Function service account." + value = { + email = module.cloud-function.service_account_email + iam_email = module.cloud-function.service_account_iam_email + } +} + +output "troubleshooting_payload" { + description = "Cloud Function payload used for manual triggering." + sensitive = true + value = jsonencode({ + data = google_cloud_scheduler_job.default.pubsub_target.0.data + }) +} diff --git a/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/variables.tf b/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/variables.tf new file mode 100644 index 000000000..680b689dd --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/deploy-cloud-function/variables.tf @@ -0,0 +1,105 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "bundle_path" { + description = "Path used to write the intermediate Cloud Function code bundle." + type = string + default = "./bundle.zip" +} + +variable "cloud_function_config" { + description = "Optional Cloud Function configuration." + type = object({ + bucket_name = optional(string) + build_worker_pool_id = optional(string) + bundle_path = optional(string, "./bundle.zip") + debug = optional(bool, false) + memory_mb = optional(number, 256) + source_dir = optional(string, "../src") + timeout_seconds = optional(number, 540) + }) + default = {} + nullable = false +} + +variable "dashboard_json_path" { + description = "Optional monitoring dashboard to deploy." + type = string + default = null +} + +variable "discovery_config" { + description = "Discovery configuration. Discovery root is the organization or a folder. If monitored folders and projects are empy, every project under the discovery root node will be monitored." + type = object({ + discovery_root = string + monitored_folders = list(string) + monitored_projects = list(string) + custom_quota_file = optional(string) + }) + nullable = false + validation { + condition = ( + var.discovery_config.monitored_folders != null && + var.discovery_config.monitored_projects != null + ) + error_message = "Monitored folders and projects can be empty lists, but they cannot be null." + } +} + +variable "grant_discovery_iam_roles" { + description = "Optionally grant required IAM roles to Cloud Function service account." + type = bool + default = false + nullable = false +} + +variable "labels" { + description = "Billing labels used for the Cloud Function, and the project if project_create is true." + type = map(string) + default = {} +} + +variable "name" { + description = "Name used to create Cloud Function related resources." + type = string + default = "net-dash" +} + +variable "project_create_config" { + description = "Optional configuration if project creation is required." + type = object({ + billing_account_id = string + parent_id = optional(string) + }) + default = null +} + +variable "project_id" { + description = "Project id where the Cloud Function will be deployed." + type = string +} + +variable "region" { + description = "Compute region where the Cloud Function will be deployed." + type = string + default = "europe-west1" +} + +variable "schedule_config" { + description = "Schedule timer configuration in crontab format." + type = string + default = "*/30 * * * *" +} diff --git a/blueprints/cloud-operations/network-dashboard/main.tf b/blueprints/cloud-operations/network-dashboard/main.tf deleted file mode 100644 index e74cabd6e..000000000 --- a/blueprints/cloud-operations/network-dashboard/main.tf +++ /dev/null @@ -1,191 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -locals { - project_ids = toset(var.monitored_projects_list) - projects = join(",", local.project_ids) - - folder_ids = toset(var.monitored_folders_list) - folders = join(",", local.folder_ids) - monitoring_project = var.monitoring_project_id == "" ? module.project-monitoring[0].project_id : var.monitoring_project_id -} - -################################################ -# Monitoring project creation # -################################################ - -module "project-monitoring" { - count = var.monitoring_project_id == "" ? 1 : 0 - source = "../../../modules/project" - name = "network-dashboards" - parent = "organizations/${var.organization_id}" - prefix = var.prefix - billing_account = var.billing_account - services = var.project_monitoring_services -} - -################################################ -# Service account creation and IAM permissions # -################################################ - -module "service-account-function" { - source = "../../../modules/iam-service-account" - project_id = local.monitoring_project - name = "sa-dash" - generate_key = false - - # Required IAM permissions for this service account are: - # 1) compute.networkViewer on projects to be monitored (I gave it at organization level for now for simplicity) - # 2) monitoring viewer on the projects to be monitored (I gave it at organization level for now for simplicity) - - iam_organization_roles = { - "${var.organization_id}" = [ - "roles/compute.networkViewer", - "roles/monitoring.viewer", - "roles/cloudasset.viewer" - ] - } - - iam_project_roles = { - "${local.monitoring_project}" = [ - "roles/monitoring.metricWriter", - ] - } -} - -module "service-account-scheduler" { - source = "../../../modules/iam-service-account" - project_id = local.monitoring_project - name = "sa-scheduler" - generate_key = false - - iam_project_roles = { - "${local.monitoring_project}" = [ - "roles/run.invoker", - "roles/cloudfunctions.invoker" - ] - } -} - -################################################ -# Cloud Function configuration (& Scheduler) # -# you can comment out the pub/sub call in case of 2nd generation function -################################################ - -module "pubsub" { - - source = "../../../modules/pubsub" - project_id = local.monitoring_project - name = "network-dashboard-pubsub" - subscriptions = { - "network-dashboard-pubsub-default" = null - } - # the Cloud Scheduler robot service account already has pubsub.topics.publish - # at the project level via roles/cloudscheduler.serviceAgent -} - -resource "google_cloud_scheduler_job" "job" { - count = var.cf_version == "V2" ? 0 : 1 - project = local.monitoring_project - region = var.region - name = "network-dashboard-scheduler" - schedule = var.schedule_cron - time_zone = "UTC" - - pubsub_target { - topic_name = module.pubsub.topic.id - data = base64encode("test") - } -} -#http trigger for 2nd generation function - -resource "google_cloud_scheduler_job" "job_httptrigger" { - count = var.cf_version == "V2" ? 1 : 0 - project = local.monitoring_project - region = var.region - name = "network-dashboard-scheduler" - schedule = var.schedule_cron - time_zone = "UTC" - - http_target { - http_method = "POST" - uri = module.cloud-function.uri - - oidc_token { - service_account_email = module.service-account-scheduler.email - } - } -} - -module "cloud-function" { - v2 = var.cf_version == "V2" - source = "../../../modules/cloud-function" - project_id = local.monitoring_project - name = "network-dashboard-cloud-function" - bucket_name = "${local.monitoring_project}-network-dashboard-bucket" - bucket_config = { - location = var.region - } - region = var.region - - bundle_config = { - source_dir = "cloud-function" - output_path = "cloud-function.zip" - } - - function_config = { - timeout = 480 # Timeout in seconds, increase it if your CF timeouts and use v2 if > 9 minutes. - entry_point = "main" - runtime = "python39" - instances = 1 - memory_mb = 256 - - } - - environment_variables = { - MONITORED_PROJECTS_LIST = local.projects - MONITORED_FOLDERS_LIST = local.folders - MONITORING_PROJECT_ID = local.monitoring_project - ORGANIZATION_ID = var.organization_id - CF_VERSION = var.cf_version - } - - service_account = module.service-account-function.email - # Internal only doesn't seem to work with CFv2: - ingress_settings = var.cf_version == "V2" ? "ALLOW_ALL" : "ALLOW_INTERNAL_ONLY" - - trigger_config = var.cf_version == "V2" ? { - v2 = { - event_type = "google.cloud.pubsub.topic.v1.messagePublished" - pubsub_topic = module.pubsub.topic.id - service_account_create = true - } - } : { - v1 = { - event = "google.pubsub.topic.publish" - resource = module.pubsub.topic.id - } - } -} - -################################################ -# Cloud Monitoring Dashboard creation # -################################################ - -resource "google_monitoring_dashboard" "dashboard" { - dashboard_json = file("${path.module}/dashboards/quotas-utilization.json") - project = local.monitoring_project -} diff --git a/blueprints/cloud-operations/network-dashboard/src/README.md b/blueprints/cloud-operations/network-dashboard/src/README.md new file mode 100644 index 000000000..a7fad8217 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/README.md @@ -0,0 +1,111 @@ +# Network Dashboard Discovery Tool + +This tool constitutes the discovery and data gathering side of the Network Dashboard, and can be used in combination with the related [Terraform deployment examples](../), or packaged in different ways including standalone manual use. + +- [Quick Usage Example](#quick-usage-example) +- [High Level Architecture and Plugin Design](#high-level-architecture-and-plugin-design) +- [Debugging and Troubleshooting](#debugging-and-troubleshooting) + +## Quick Usage Example + +The tool behaves like a regular CLI app, with several options documented via the usual short help: + +```text +./main.py --help + +Usage: main.py [OPTIONS] + + CLI entry point. + +Options: + -dr, --discovery-root TEXT Root node for asset discovery, + organizations/nnn or folders/nnn. [required] + -mon, --monitoring-project TEXT GCP monitoring project where metrics will be + stored. [required] + -p, --project TEXT GCP project id to be monitored, can be specified multiple + times. + -f, --folder INTEGER GCP folder id to be monitored, can be specified multiple + times. + --custom-quota-file FILENAME Custom quota file in yaml format. + --dump-file FILENAME Export JSON representation of resources to + file. + --load-file FILENAME Load JSON resources from file, skips init and + discovery. + --debug-plugin TEXT Run only core and specified timeseries plugin. + --help Show this message and exit. +``` + +In normal use three pieces of information need to be passed in: + +- the monitoring project where metric descriptors and timeseries will be stored +- the discovery root scope (organization or top-level folder, [see here for examples](../deploy-cloud-function/README.md#discovery-configuration)) +- the list of folders and/or projects that contain the resources to be monitored (folders will discover all included projects) + +To account for custom quota which are not yet exposed via API or which are applied to individual networks, a YAML file with quota overrides can be specified via the `--custom-quota-file` option. Refer to the [included sample](./custom-quotas.sample) for details on its format. + +A typical invocation might look like this: + +```bash +./main.py \ + -dr organizations/1234567890 \ + -op my-monitoring-project \ + --folder 1234567890 --folder 987654321 \ + --project my-net-project \ + --custom-quota-file custom-quotas.yaml +``` + +## High Level Architecture and Plugin Design + +The tool is composed of two main processing phases + +- the discovery of resources within a predefined scope using Cloud Asset Inventory and Compute APIs +- the computation of metric timeseries derived from discovered resources + +Once both phases are complete, the tool sends generated timeseries to Cloud Operations together with any missing metric descriptors. + +Every action during those phases is delegated to a series of plugins, which conform to simple interfaces and exchange predefined basic types with the main module. Plugins are registered at runtime, and are split in broad categories depending on the stage where they execute: + +- init plugin functions have the task of preparing the required keys in the shared resource data structure. Usually, init functions are usually small and there's one for each discovery plugin +- discovery plugin functions do the bulk of the work of discovering resources; they return HTTP Requests (e.g. calls to GCP APIs) or Resource objects (extracted from the API responses) to the main module, and receive HTTP Responses +- timeseries plugin read from the shared resource data structure, and return computed Metric Descriptors and Timeseries objects + +Plugins are registered via simple functions defined in the [plugin package initialization file](./plugins/__init__.py), and leverage [utility functions](./plugins/utils.py) for batching API requests and parsing results. + +The main module cycles through stages, calling stage plugins in succession iterating over their results. + +## Debugging and Troubleshooting + +Note that python version > 3.8 is required. + +If you run into a `ModuleNotFoundError`, install the required dependencies: +`pip3 install -r requirements.txt` + +A few convenience options are provided to simplify development, debugging and troubleshooting: + +- the discovery phase results can be dumped to a JSON file, that can then be used to check actual resource representation, or skip the discovery phase entirely to speed up development of timeseries-related functions +- a single timeseries plugin can be optionally run alone, to focus debugging and decrease the amount of noise from logs and outputs + +This is an example call that stores discovery results to a file: + +```bash +./main.py \ + -dr organizations/1234567890 \ + -op my-monitoring-project \ + --folder 1234567890 --folder 987654321 \ + --project my-net-project \ + --custom-quota-file custom-quotas.yaml \ + --dump-file out.json +``` + +And this is the corresponding call that skips the discovery phase and also runs a single timeseries plugin: + +```bash +./main.py \ + -dr organizations/1234567890 \ + -op my-monitoring-project \ + --folder 1234567890 --folder 987654321 \ + --project my-net-project \ + --custom-quota-file custom-quotas.yaml \ + --load-file out.json \ + --debug-plugin plugins.series-firewall-rules.timeseries +``` diff --git a/blueprints/cloud-operations/network-dashboard/src/custom-quotas.sample b/blueprints/cloud-operations/network-dashboard/src/custom-quotas.sample new file mode 100644 index 000000000..9f090b3c5 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/custom-quotas.sample @@ -0,0 +1,8 @@ +projects: + tf-playground-svpc-net: + global: + INTERNAL_FORWARDING_RULES_PER_NETWORK: 750 +networks: + # TODO: what are the quotas that can be overridden at the network level? + projects/tf-playground-svpc-net/global/networks/shared-vpc: + PEERINGS_PER_NETWORK: 40 diff --git a/blueprints/cloud-operations/network-dashboard/src/main.py b/blueprints/cloud-operations/network-dashboard/src/main.py new file mode 100755 index 000000000..bd57f18e4 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/main.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python3 +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'Network dashboard: create network-related metric timeseries for GCP resources.' + +import base64 +import binascii +import collections +import json +import logging +import os + +import click +import google.auth +import plugins +import plugins.monitoring +import yaml + +from google.auth.transport.requests import AuthorizedSession + +HTTP = AuthorizedSession(google.auth.default()[0]) +LOGGER = logging.getLogger('net-dash') +MONITORING_ROOT = 'netmon/' + +Result = collections.namedtuple('Result', 'phase resource data') + + +def do_discovery(resources): + '''Calls discovery plugin functions and collect discovered resources. + + The communication with discovery plugins uses double dispatch, where plugins + accept either no args and return 1-n HTTP request instances, or a single HTTP + response and return 1-n resource instances. A queue is set up for each plugin + results since each call can return multiple requests or resources. + + Args: + resources: pre-initialized map where discovered resources will be stored. + ''' + LOGGER.info(f'discovery start') + for plugin in plugins.get_discovery_plugins(): + # set up the queue with the initial list of HTTP requests from this plugin + q = collections.deque(plugin.func(resources)) + while q: + result = q.popleft() + if isinstance(result, plugins.HTTPRequest): + # fetch a single HTTP request + response = fetch(result) + if not response: + continue + if result.json: + try: + # decode the JSON HTTP response and pass it to the plugin + LOGGER.debug(f'passing JSON result to {plugin.name}') + results = plugin.func(resources, response, response.json()) + except json.decoder.JSONDecodeError as e: + LOGGER.critical( + f'error decoding JSON for {result.url}: {e.args[0]}') + continue + else: + # pass the raw HTTP response to the plugin + LOGGER.debug(f'passing raw result to {plugin.name}') + results = plugin.func(resources, response) + q += collections.deque(results) + elif isinstance(result, plugins.Resource): + # store a resource the plugin derived from a previous HTTP response + LOGGER.debug(f'got resource {result} from {plugin.name}') + if result.key: + # this specific resource is indexed by an additional key + resources[result.type][result.id][result.key] = result.data + else: + resources[result.type][result.id] = result.data + LOGGER.info('discovery end {}'.format( + {k: len(v) for k, v in resources.items() if not isinstance(v, str)})) + + +def do_init(resources, discovery_root, monitoring_project, folders=None, + projects=None, custom_quota=None): + '''Calls init plugins to configure keys in the shared resource map. + + Args: + discovery_root: root node for discovery from configuration. + monitoring_project: monitoring project id id from configuration. + folders: list of folder ids for resource discovery from configuration. + projects: list of project ids for resource discovery from configuration. + ''' + LOGGER.info(f'init start') + folders = [str(f) for f in folders or []] + resources['config:discovery_root'] = discovery_root + resources['config:monitoring_project'] = monitoring_project + resources['config:folders'] = folders + resources['config:projects'] = projects or [] + resources['config:custom_quota'] = custom_quota or {} + resources['config:monitoring_root'] = MONITORING_ROOT + if discovery_root.startswith('organization'): + resources['organization'] = discovery_root.split('/')[-1] + for f in folders: + resources['folders'] = {f: {} for f in folders} + for plugin in plugins.get_init_plugins(): + plugin.func(resources) + LOGGER.info(f'init completed, resources {resources}') + + +def do_timeseries_calc(resources, descriptors, timeseries, debug_plugin=None): + '''Calls timeseries plugins and collect resulting descriptors and timeseries. + + Timeseries plugin return a list of MetricDescriptors and Timeseries instances, + one per each metric. + + Args: + resources: shared map of configuration and discovered resources. + descriptors: list where collected descriptors will be stored. + timeseries: list where collected timeseries will be stored. + debug_plugin: optional name of a single plugin to call + ''' + LOGGER.info(f'timeseries calc start (debug plugin: {debug_plugin})') + for plugin in plugins.get_timeseries_plugins(): + if debug_plugin and plugin.name != debug_plugin: + LOGGER.info(f'skipping {plugin.name}') + continue + num_desc, num_ts = 0, 0 + for result in plugin.func(resources): + if not result: + continue + # append result to the relevant collection (descriptors or timeseries) + if isinstance(result, plugins.MetricDescriptor): + descriptors.append(result) + num_desc += 1 + elif isinstance(result, plugins.TimeSeries): + timeseries.append(result) + num_ts += 1 + LOGGER.info(f'{plugin.name}: {num_desc} descriptors {num_ts} timeseries') + LOGGER.info('timeseries calc end (descriptors: {} timeseries: {})'.format( + len(descriptors), len(timeseries))) + + +def do_timeseries_descriptors(project_id, existing, computed): + '''Executes API calls for each previously computed metric descriptor. + + Args: + project_id: monitoring project id where to write descriptors. + existing: map of existing descriptor types. + computed: list of plugins.MetricDescriptor instances previously computed. + ''' + LOGGER.info('timeseries descriptors start') + requests = plugins.monitoring.descriptor_requests(project_id, MONITORING_ROOT, + existing, computed) + num = 0 + for request in requests: + fetch(request) + num += 1 + LOGGER.info('timeseries descriptors end (computed: {} created: {})'.format( + len(computed), num)) + + +def do_timeseries(project_id, timeseries, descriptors): + '''Executes API calls for each previously computed timeseries. + + Args: + project_id: monitoring project id where to write timeseries. + timeseries: list of plugins.Timeseries instances. + descriptors: list of plugins.MetricDescriptor instances matching timeseries. + ''' + LOGGER.info('timeseries start') + requests = plugins.monitoring.timeseries_requests(project_id, MONITORING_ROOT, + timeseries, descriptors) + num = 0 + for request in requests: + fetch(request) + num += 1 + LOGGER.info('timeseries end (number: {} requests: {})'.format( + len(timeseries), num)) + + +def fetch(request): + '''Minimal HTTP client interface for API calls. + + Executes the HTTP request passed as argument using the google.auth + authenticated session. + + Args: + request: an instance of plugins.HTTPRequest. + Returns: + JSON-decoded or raw response depending on the 'json' request attribute. + ''' + # try + LOGGER.debug(f'fetch {"POST" if request.data else "GET"} {request.url}') + try: + if not request.data: + response = HTTP.get(request.url, headers=request.headers) + else: + response = HTTP.post(request.url, headers=request.headers, + data=request.data) + except google.auth.exceptions.RefreshError as e: + raise SystemExit(e.args[0]) + if response.status_code != 200: + LOGGER.critical( + f'response code {response.status_code} for URL {request.url}') + LOGGER.critical(response.content) + print(request.data) + raise SystemExit(1) + return response + + +def main_cf_pubsub(event, context): + 'Entry point for Cloud Function triggered by a PubSub message.' + debug = os.environ.get('DEBUG') + logging.basicConfig(level=logging.DEBUG if debug else logging.INFO) + LOGGER.info('processing pubsub payload') + try: + payload = json.loads(base64.b64decode(event['data']).decode('utf-8')) + except (binascii.Error, json.JSONDecodeError) as e: + raise SystemExit(f'Invalid payload: e.args[0].') + discovery_root = payload.get('discovery_root') + monitoring_project = payload.get('monitoring_project') + if not discovery_root: + LOGGER.critical('no discovery roo project specified') + LOGGER.info(payload) + raise SystemExit(f'Invalid options') + if not monitoring_project: + LOGGER.critical('no monitoring project specified') + LOGGER.info(payload) + raise SystemExit(f'Invalid options') + if discovery_root.partition('/')[0] not in ('folders', 'organizations'): + raise SystemExit(f'Invalid discovery root {discovery_root}.') + custom_quota = payload.get('custom_quota', {}) + descriptors = [] + folders = payload.get('folders', []) + projects = payload.get('projects', []) + resources = {} + timeseries = [] + do_init(resources, discovery_root, monitoring_project, folders, projects, + custom_quota) + do_discovery(resources) + do_timeseries_calc(resources, descriptors, timeseries) + do_timeseries_descriptors(monitoring_project, resources['metric-descriptors'], + descriptors) + do_timeseries(monitoring_project, timeseries, descriptors) + + +@click.command() +@click.option( + '--discovery-root', '-dr', required=True, + help='Root node for asset discovery, organizations/nnn or folders/nnn.') +@click.option('--monitoring-project', '-mon', required=True, type=str, + help='GCP monitoring project where metrics will be stored.') +@click.option('--project', '-p', type=str, multiple=True, + help='GCP project id, can be specified multiple times.') +@click.option('--folder', '-f', type=int, multiple=True, + help='GCP folder id, can be specified multiple times.') +@click.option('--custom-quota-file', type=click.File('r'), + help='Custom quota file in yaml format.') +@click.option('--dump-file', type=click.File('w'), + help='Export JSON representation of resources to file.') +@click.option('--load-file', type=click.File('r'), + help='Load JSON resources from file, skips init and discovery.') +@click.option('--debug-plugin', + help='Run only core and specified timeseries plugin.') +def main(discovery_root, monitoring_project, project=None, folder=None, + custom_quota_file=None, dump_file=None, load_file=None, + debug_plugin=None): + 'CLI entry point.' + logging.basicConfig(level=logging.INFO) + if discovery_root.partition('/')[0] not in ('folders', 'organizations'): + raise SystemExit('Invalid discovery root.') + descriptors = [] + timeseries = [] + if load_file: + resources = json.load(load_file) + else: + custom_quota = {} + resources = {} + if custom_quota_file: + try: + custom_quota = yaml.load(custom_quota_file, Loader=yaml.Loader) + except yaml.YAMLError as e: + raise SystemExit(f'Error decoding custom quota file: {e.args[0]}') + do_init(resources, discovery_root, monitoring_project, folder, project, + custom_quota) + do_discovery(resources) + if dump_file: + json.dump(resources, dump_file, indent=2) + do_timeseries_calc(resources, descriptors, timeseries, debug_plugin) + do_timeseries_descriptors(monitoring_project, resources['metric-descriptors'], + descriptors) + do_timeseries(monitoring_project, timeseries, descriptors) + + +if __name__ == '__main__': + main(auto_envvar_prefix='NETMON') diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/__init__.py b/blueprints/cloud-operations/network-dashboard/src/plugins/__init__.py new file mode 100644 index 000000000..1bdc4cb20 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/__init__.py @@ -0,0 +1,81 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''Plugin interface objects and registration functions. + +This module export the objects passed to and returned from plugin functions, +and the function used to register plugins for each stage, and get all plugins +for individual stages. +''' + +import collections +import enum +import functools +import importlib +import pathlib +import pkgutil +import types + +__all__ = [ + 'HTTPRequest', 'Level', 'PluginError', 'Resource', 'get_discovery_plugins', + 'get_init_plugins', 'register_discovery', 'register_init' +] + +_PLUGINS_DISCOVERY = [] +_PLUGINS_INIT = [] +_PLUGINS_TIMESERIES = [] + +HTTPRequest = collections.namedtuple('HTTPRequest', 'url headers data json', + defaults=[True]) +Level = enum.IntEnum('Level', 'CORE PRIMARY DERIVED') +MetricDescriptor = collections.namedtuple('MetricDescriptor', + 'type name labels is_ratio', + defaults=[False]) +Plugin = collections.namedtuple('Plugin', 'func name level priority', + defaults=[Level.PRIMARY, 99]) +Resource = collections.namedtuple('Resource', 'type id data key', + defaults=[None]) +TimeSeries = collections.namedtuple('TimeSeries', 'metric value labels') + + +class PluginError(Exception): + pass + + +def _register_plugin(collection, *args): + 'Derive plugin name from function and add to its collection.' + if args and type(args[0]) == types.FunctionType: + collection.append( + Plugin(args[0], f'{args[0].__module__}.{args[0].__name__}')) + return + + def outer(func): + collection.append(Plugin(func, f'{func.__module__}.{func.__name__}', *args)) + return func + + return outer + + +get_discovery_plugins = functools.partial(iter, _PLUGINS_DISCOVERY) +get_init_plugins = functools.partial(iter, _PLUGINS_INIT) +get_timeseries_plugins = functools.partial(iter, _PLUGINS_TIMESERIES) +register_discovery = functools.partial(_register_plugin, _PLUGINS_DISCOVERY) +register_init = functools.partial(_register_plugin, _PLUGINS_INIT) +register_timeseries = functools.partial(_register_plugin, _PLUGINS_TIMESERIES) + +_plugins_path = str(pathlib.Path(__file__).parent) + +for mod_info in pkgutil.iter_modules([_plugins_path], 'plugins.'): + importlib.import_module(mod_info.name) + +_PLUGINS_DISCOVERY.sort(key=lambda i: i.level) diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/core-discover-cai-nodes.py b/blueprints/cloud-operations/network-dashboard/src/plugins/core-discover-cai-nodes.py new file mode 100644 index 000000000..dc5c53247 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/core-discover-cai-nodes.py @@ -0,0 +1,80 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''Project and folder discovery from configuration options. + +This plugin needs to run first, as it's responsible for discovering nodes that +contain resources: folders and projects contained in the hierarchy passed in +via configuration options. Node resources are fetched from Cloud Asset +Inventory based on explicit id or being part of a folder hierarchy. +''' + +import logging + +from . import HTTPRequest, Level, Resource, register_init, register_discovery +from .utils import parse_page_token, parse_cai_results + +LOGGER = logging.getLogger('net-dash.discovery.cai-nodes') + +CAI_URL = ('https://content-cloudasset.googleapis.com/v1p1beta1' + '/{}/resources:searchAll' + '?assetTypes=cloudresourcemanager.googleapis.com/Folder' + '&assetTypes=cloudresourcemanager.googleapis.com/Project' + '&pageSize=500') + + +def _handle_discovery(resources, response, data): + 'Processes asset response and returns project resources or next URLs.' + LOGGER.info('discovery handle request') + for result in parse_cai_results(data, 'nodes'): + asset_type = result['assetType'].split('/')[-1] + name = result['name'].split('/')[-1] + if asset_type == 'Folder': + yield Resource('folders', name, {'name': result['displayName']}) + elif asset_type == 'Project': + number = result['project'].split('/')[1] + data = {'number': number, 'project_id': name} + yield Resource('projects', name, data) + yield Resource('projects:number', number, data) + else: + LOGGER.info(f'unknown resource {name}') + next_url = parse_page_token(data, response.request.url) + if next_url: + LOGGER.info('discovery next url') + yield HTTPRequest(next_url, {}, None) + + +@register_init +def init(resources): + 'Prepares project datastructures in the shared resource map.' + LOGGER.info('init') + resources.setdefault('folders', {}) + resources.setdefault('projects', {}) + resources.setdefault('projects:number', {}) + + +@register_discovery(Level.CORE, 0) +def start_discovery(resources, response=None, data=None): + 'Plugin entry point, triggers discovery and handles requests and responses.' + LOGGER.info(f'discovery (has response: {response is not None})') + if response is None: + # return initial discovery URLs + for v in resources['config:folders']: + yield HTTPRequest(CAI_URL.format(f'folders/{v}'), {}, None) + for v in resources['config:projects']: + if v not in resources['projects']: + yield HTTPRequest(CAI_URL.format(f'projects/{v}'), {}, None) + else: + # pass the API response to the plugin data handler and return results + for result in _handle_discovery(resources, response, data): + yield result diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/discover-cai.py b/blueprints/cloud-operations/network-dashboard/src/plugins/discover-cai.py new file mode 100644 index 000000000..1ac62d066 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/discover-cai.py @@ -0,0 +1,301 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''Compute resources discovery from Cloud Asset Inventory. + +This plugin handles discovery for Compute resources via a broad org-level +scoped CAI search. Common resource attributes are parsed by a generic handler +function, which then delegates parsing of resource-level attributes to smaller +specialized functions, one per resource type. +''' + +import logging + +from . import HTTPRequest, Level, Resource, register_init, register_discovery +from .utils import parse_cai_results + +CAI_URL = ('https://content-cloudasset.googleapis.com/v1' + '/{root}/assets' + '?contentType=RESOURCE&{asset_types}&pageSize=500') +LOGGER = logging.getLogger('net-dash.discovery.cai-compute') +TYPES = { + 'addresses': 'compute.googleapis.com/Address', + 'global_addresses': 'compute.googleapis.com/GlobalAddress', + 'firewall_policies': 'compute.googleapis.com/FirewallPolicy', + 'firewall_rules': 'compute.googleapis.com/Firewall', + 'forwarding_rules': 'compute.googleapis.com/ForwardingRule', + 'instances': 'compute.googleapis.com/Instance', + 'networks': 'compute.googleapis.com/Network', + 'subnetworks': 'compute.googleapis.com/Subnetwork', + 'routers': 'compute.googleapis.com/Router', + 'routes': 'compute.googleapis.com/Route', + 'sql_instances': 'sqladmin.googleapis.com/Instance', + 'filestore_instances': 'file.googleapis.com/Instance', + 'memorystore_instances': 'redis.googleapis.com/Instance', +} +NAMES = {v: k for k, v in TYPES.items()} + + +def _get_parent(parent, resources): + 'Extracts and returns resource parent and type.' + parent_type, parent_id = parent.split('/')[-2:] + if parent_type == 'projects': + project = resources['projects:number'].get(parent_id) + if project: + return {'project_id': project['project_id'], 'project_number': parent_id} + if parent_type == 'folders': + if parent_id in resources['folders']: + return {'parent': f'{parent_type}/{parent_id}'} + if resources.get('organization') == parent_id: + return {'parent': f'{parent_type}/{parent_id}'} + + +def _handle_discovery(resources, response, data): + 'Processes the asset API response and returns parsed resources or next URL.' + LOGGER.info('discovery handle request') + for result in parse_cai_results(data, 'cai-compute', method='list'): + resource = _handle_resource(resources, result['assetType'], + result['resource']) + if not resource: + continue + yield resource + page_token = data.get('nextPageToken') + if page_token: + LOGGER.info('requesting next page') + url = _url(resources) + yield HTTPRequest(f'{url}&pageToken={page_token}', {}, None) + + +def _handle_resource(resources, asset_type, data): + 'Parses and returns a single resource. Calls resource-level handler.' + # general attributes shared by all resource types + attrs = data['data'] + # we use the asset type as the discovery name sometimes does not match + # e.g. assetType = GlobalAddress but discoveryName = Address + resource_name = NAMES[asset_type] + resource = { + 'id': + attrs.get('id'), + 'name': + attrs['name'], + # Some resources (ex: Filestore) don't have a self_link, using parent + name in that case + 'self_link': + f'{data["parent"]}/{attrs["name"]}' + if not 'selfLink' in attrs else _self_link(attrs['selfLink']), + 'assetType': + asset_type + } + # derive parent type and id and skip if parent is not within scope + parent_data = _get_parent(data['parent'], resources) + if not parent_data: + LOGGER.info(f'{resource["self_link"]} outside perimeter') + LOGGER.debug([ + resources['organization'], resources['folders'], + resources['projects:number'] + ]) + return + resource.update(parent_data) + # gets and calls the resource-level handler for type specific attributes + func = globals().get(f'_handle_{resource_name}') + if not callable(func): + raise SystemExit(f'specialized function missing for {resource_name}') + extra_attrs = func(resource, attrs) + if not extra_attrs: + return + resource.update(extra_attrs) + return Resource(resource_name, resource['self_link'], resource) + + +def _handle_addresses(resource, data): + 'Handles address type resource data.' + network = data.get('network') + subnet = data.get('subnetwork') + return { + 'address': data['address'], + 'internal': data.get('addressType') == 'INTERNAL', + 'purpose': data.get('purpose', ''), + 'status': data.get('status', ''), + 'network': None if not network else _self_link(network), + 'subnetwork': None if not subnet else _self_link(subnet) + } + + +def _handle_firewall_policies(resource, data): + 'Handles firewall policy type resource data.' + return { + 'num_rules': len(data.get('rules', [])), + 'num_tuples': data.get('ruleTupleCount', 0) + } + + +def _handle_firewall_rules(resource, data): + 'Handles firewall type resource data.' + return {'network': _self_link(data['network'])} + + +def _handle_forwarding_rules(resource, data): + 'Handles forwarding_rules type resource data.' + network = data.get('network') + region = data.get('region') + subnet = data.get('subnetwork') + return { + 'address': data.get('IPAddress'), + 'load_balancing_scheme': data.get('loadBalancingScheme', ''), + 'network': None if not network else _self_link(network), + 'psc_accepted': data.get('pscConnectionStatus') == 'ACCEPTED', + 'region': None if not region else region.split('/')[-1], + 'subnetwork': None if not subnet else _self_link(subnet) + } + + +def _handle_global_addresses(resource, data): + 'Handles GlobalAddress type resource data (ex: PSA ranges).' + network = data.get('network') + return { + 'address': data['address'], + 'prefixLength': data.get('prefixLength') or None, + 'internal': data.get('addressType') == 'INTERNAL', + 'purpose': data.get('purpose', ''), + 'status': data.get('status', ''), + 'network': None if not network else _self_link(network), + } + + +def _handle_instances(resource, data): + 'Handles instance type resource data.' + if data['status'] != 'RUNNING': + return + networks = [{ + 'network': _self_link(i['network']), + 'subnetwork': _self_link(i['subnetwork']) + } for i in data.get('networkInterfaces', [])] + return {'zone': data['zone'], 'networks': networks} + + +def _handle_networks(resource, data): + 'Handles network type resource data.' + peerings = [{ + 'active': p['state'] == 'ACTIVE', + 'name': p['name'], + 'network': _self_link(p['network']), + 'project_id': _self_link(p['network']).split('/')[1] + } for p in data.get('peerings', [])] + subnets = [_self_link(s) for s in data.get('subnetworks', [])] + return {'peerings': peerings, 'subnetworks': subnets} + + +def _handle_routers(resource, data): + 'Handles router type resource data.' + return { + 'network': _self_link(data['network']), + 'region': data['region'].split('/')[-1] + } + + +def _handle_routes(resource, data): + 'Handles route type resource data.' + hop = [ + a.removeprefix('nextHop').lower() for a in data if a.startswith('nextHop') + ] + return {'next_hop_type': hop[0], 'network': _self_link(data['network'])} + + +def _handle_sql_instances(resource, data): + 'Handles cloud sql instance type resource data.' + return { + 'name': data['name'], + 'self_link': _self_link(data['selfLink']), + 'ipAddresses': [ + i['ipAddress'] for i in data['ipAddresses'] if i['type'] == 'PRIVATE' + ], + 'region': data['region'], + 'availabilityType': data['settings']['availabilityType'], + 'network': data['settings']['ipConfiguration']['privateNetwork'] + } + + +def _handle_filestore_instances(resource, data): + 'Handles filestore instance type resource data.' + return { + # Getting only the instance name, removing the rest + 'name': data['name'].split('/')[-1], + # Is a list but for now, only one network is supported for Filestore + 'network': data['networks'][0]['network'], + 'reservedIpRange': data['networks'][0]['reservedIpRange'], + 'ipAddresses': data['networks'][0]['ipAddresses'] + } + + +def _handle_memorystore_instances(resource, data): + 'Handles Memorystore (Redis) instance type resource data.' + return { + # Getting only the instance name, removing the rest + 'name': + data['name'].split('/')[-1], + 'locationId': + data['locationId'], + 'replicaCount': + 0 if not 'replicaCount' in data else data['replicaCount'], + 'network': + data['authorizedNetwork'], + 'reservedIpRange': + '' if not 'reservedIpRange' in data else data['reservedIpRange'], + 'host': + '' if not 'host' in data else data['host'], + } + + +def _handle_subnetworks(resource, data): + 'Handles subnetwork type resource data.' + secondary_ranges = [{ + 'name': s['rangeName'], + 'cidr_range': s['ipCidrRange'] + } for s in data.get('secondaryIpRanges', [])] + return { + 'cidr_range': data['ipCidrRange'], + 'network': _self_link(data['network']), + 'purpose': data.get('purpose'), + 'region': data['region'].split('/')[-1], + 'secondary_ranges': secondary_ranges + } + + +def _self_link(s): + 'Removes initial part from self links.' + return '/'.join(s.split('/')[5:]) + + +def _url(resources): + 'Returns discovery URL' + discovery_root = resources['config:discovery_root'] + asset_types = '&'.join(f'assetTypes={t}' for t in TYPES.values()) + return CAI_URL.format(root=discovery_root, asset_types=asset_types) + + +@register_init +def init(resources): + 'Prepares the datastructures for types managed here in the resource map.' + LOGGER.info('init') + for name in TYPES: + resources.setdefault(name, {}) + + +@register_discovery(Level.PRIMARY, 10) +def start_discovery(resources, response=None, data=None): + 'Plugin entry point, triggers discovery and handles requests and responses.' + LOGGER.info(f'discovery (has response: {response is not None})') + if response is None: + yield HTTPRequest(_url(resources), {}, None) + else: + for result in _handle_discovery(resources, response, data): + yield result diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/discover-compute-quota.py b/blueprints/cloud-operations/network-dashboard/src/plugins/discover-compute-quota.py new file mode 100644 index 000000000..9c9e8f948 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/discover-compute-quota.py @@ -0,0 +1,88 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''Discovers project quota via Compute API and overlay user overrides. + +This plugin discovers project quota via batch Compute API requests. Project and +network quotas are then optionally overlaid with custom quota modifiers passed +in as options. Region quota discovery is partially implemented but not active. +''' + +import logging + +from . import Level, Resource, register_init, register_discovery +from .utils import batched, poor_man_mp_request, poor_man_mp_response + +LOGGER = logging.getLogger('net-dash.discovery.compute-quota') +NAME = 'quota' + +API_GLOBAL_URL = '/compute/v1/projects/{}' +API_REGION_URL = '/compute/v1/projects/{}/regions/{}' + + +def _handle_discovery(resources, response): + 'Processes asset batch response and overlays custom quota.' + LOGGER.info('discovery handle request') + content_type = response.headers['content-type'] + per_project_quota = resources['config:custom_quota'].get('projects', {}) + # process batch response + for part in poor_man_mp_response(content_type, response.content): + kind = part.get('kind') + quota = { + q['metric']: int(q['limit']) + for q in sorted(part.get('quotas', []), key=lambda v: v['metric']) + } + self_link = part.get('selfLink') + if not self_link: + logging.warn('invalid quota response') + self_link = self_link.split('/') + if kind == 'compute#project': + project_id = self_link[-1] + region = 'global' + elif kind == 'compute#region': + project_id = self_link[-3] + region = self_link[-1] + # custom quota overrides + for k, v in per_project_quota.get(project_id, {}).get(region, {}).items(): + quota[k] = int(v) + if project_id not in resources[NAME]: + resources[NAME][project_id] = {} + yield Resource(NAME, project_id, quota, region) + + +@register_init +def init(resources): + 'Prepares quota datastructures in the shared resource map.' + LOGGER.info('init') + resources.setdefault(NAME, {}) + + +@register_discovery(Level.DERIVED, 0) +def start_discovery(resources, response=None): + 'Plugin entry point, triggers discovery and handles requests and responses.' + LOGGER.info(f'discovery (has response: {response is not None})') + if response is None: + # TODO: regions + urls = [API_GLOBAL_URL.format(p) for p in resources['projects']] + if not urls: + return + for batch in batched(urls, 10): + yield poor_man_mp_request(batch) + else: + for result in _handle_discovery(resources, response): + yield result + # store custom network-level quota + per_network_quota = resources['config:custom_quota'].get('networks', {}) + for network_id, overrides in per_network_quota.items(): + quota = {k: int(v) for k, v in overrides.items()} + yield Resource(NAME, network_id, quota) diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/discover-compute-routerstatus.py b/blueprints/cloud-operations/network-dashboard/src/plugins/discover-compute-routerstatus.py new file mode 100644 index 000000000..cd2840b77 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/discover-compute-routerstatus.py @@ -0,0 +1,89 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''Discovers dynamic route counts via router status. + +This plugin depends on the CAI Compute one as it discovers dynamic route +data by parsing router status, and it needs routers to have already been +discovered. It uses batch Compute API requests via the utils functions. +''' + +import logging + +from . import Level, Resource, register_init, register_discovery +from .utils import batched, poor_man_mp_request, poor_man_mp_response + +LOGGER = logging.getLogger('net-dash.discovery.compute-routes-dynamic') +NAME = 'routes_dynamic' + +API_URL = '/compute/v1/projects/{}/regions/{}/routers/{}/getRouterStatus' + + +def _handle_discovery(resources, response): + 'Processes asset batch response and parses router status data.' + LOGGER.info('discovery handle request') + content_type = response.headers['content-type'] + routers = [r for r in resources['routers'].values()] + # process batch response + for i, part in enumerate(poor_man_mp_response(content_type, + response.content)): + router = routers[i] + result = part.get('result') + if not result: + LOGGER.info(f'skipping router {router["self_link"]}, no result') + continue + bgp_peer_status = result.get('bgpPeerStatus') + if not bgp_peer_status: + LOGGER.info(f'skipping router {router["self_link"]}, no bgp peer status') + continue + network = result.get('network') + if not network: + LOGGER.info(f'skipping router {router["self_link"]}, no bgp peer status') + continue + if not network.endswith(router['network']): + LOGGER.warn( + f'router network mismatch: got {network} expected {router["network"]}' + ) + continue + num_learned_routes = sum( + int(p.get('numLearnedRoutes', 0)) for p in bgp_peer_status) + if router['network'] not in resources[NAME]: + resources[NAME][router['network']] = {} + yield Resource(NAME, router['network'], num_learned_routes, + router['self_link']) + yield + + +@register_init +def init(resources): + 'Prepares dynamic routes datastructure in the shared resource map.' + LOGGER.info('init') + resources.setdefault(NAME, {}) + + +@register_discovery(Level.DERIVED) +def start_discovery(resources, response=None): + 'Plugin entry point, triggers discovery and handles requests and responses.' + LOGGER.info(f'discovery (has response: {response is not None})') + if not response: + urls = [ + API_URL.format(r['project_id'], r['region'], r['name']) + for r in resources['routers'].values() + ] + if not urls: + return + for batch in batched(urls, 10): + yield poor_man_mp_request(batch) + else: + for result in _handle_discovery(resources, response): + yield result diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/discover-group-networks.py b/blueprints/cloud-operations/network-dashboard/src/plugins/discover-group-networks.py new file mode 100644 index 000000000..350c288b4 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/discover-group-networks.py @@ -0,0 +1,39 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'Group discovered networks by project.' + +import itertools +import logging + +from . import Level, Resource, register_init, register_discovery + +LOGGER = logging.getLogger('net-dash.discovery.compute-routes-dynamic') +NAME = 'networks:project' + + +@register_init +def init(resources): + 'Prepares datastructure in the shared resource map.' + LOGGER.info('init') + resources.setdefault(NAME, {}) + + +@register_discovery(Level.DERIVED) +def start_discovery(resources, response=None): + 'Plugin entry point, group and return discovered networks.' + LOGGER.info(f'discovery (has response: {response is not None})') + grouped = itertools.groupby(resources['networks'].values(), + lambda v: v['project_id']) + for project_id, vpcs in grouped: + yield Resource(NAME, project_id, [v['self_link'] for v in vpcs]) diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/discover-metric-descriptors.py b/blueprints/cloud-operations/network-dashboard/src/plugins/discover-metric-descriptors.py new file mode 100644 index 000000000..a9e4090de --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/discover-metric-descriptors.py @@ -0,0 +1,69 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''Discover existing network dashboard metric descriptors. + +Populating this data allows the tool to later compute which metric descriptors +need to be created. +''' + +import logging +import urllib.parse + +from . import HTTPRequest, Level, Resource, register_init, register_discovery +from .utils import parse_page_token + +LOGGER = logging.getLogger('net-dash.discovery.metrics') +NAME = 'metric-descriptors' + +URL = ('https://content-monitoring.googleapis.com/v3/projects' + '/{}/metricDescriptors' + '?filter=metric.type%3Dstarts_with(%22custom.googleapis.com%2F{}%22)' + '&pageSize=500') + + +def _handle_discovery(resources, response, data): + 'Processes monitoring API response and parses descriptor data.' + LOGGER.info('discovery handle request') + descriptors = data.get('metricDescriptors') + if not descriptors: + LOGGER.info('no descriptors found') + return + for d in descriptors: + yield Resource(NAME, d['type'], {}) + next_url = parse_page_token(data, response.request.url) + if next_url: + LOGGER.info('discovery next url') + yield HTTPRequest(next_url, {}, None) + + +@register_init +def init(resources): + 'Prepares datastructure in the shared resource map.' + LOGGER.info('init') + resources.setdefault(NAME, {}) + + +@register_discovery(Level.CORE, 99) +def start_discovery(resources, response=None, data=None): + 'Plugin entry point, triggers discovery and handles requests and responses.' + LOGGER.info(f'discovery (has response: {response is not None})') + project_id = resources['config:monitoring_project'] + type_root = resources['config:monitoring_root'] + url = URL.format(urllib.parse.quote_plus(project_id), + urllib.parse.quote_plus(type_root)) + if response is None: + yield HTTPRequest(url, {}, None) + else: + for result in _handle_discovery(resources, response, data): + yield result diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/monitoring.py b/blueprints/cloud-operations/network-dashboard/src/plugins/monitoring.py new file mode 100644 index 000000000..de4eae897 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/monitoring.py @@ -0,0 +1,106 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'Utility functions to create monitoring API requests.' + +import collections +import datetime +import json +import logging + +from . import HTTPRequest +from .utils import batched + +DESCRIPTOR_TYPE_BASE = 'custom.googleapis.com/{}' +DESCRIPTOR_URL = ('https://content-monitoring.googleapis.com/v3' + '/projects/{}/metricDescriptors?alt=json') +HEADERS = {'content-type': 'application/json'} +LOGGER = logging.getLogger('net-dash.plugins.monitoring') +TIMESERIES_URL = ('https://content-monitoring.googleapis.com/v3' + '/projects/{}/timeSeries?alt=json') + + +def descriptor_requests(project_id, root, existing, computed): + 'Returns create requests for missing descriptors.' + type_base = DESCRIPTOR_TYPE_BASE.format(root) + url = DESCRIPTOR_URL.format(project_id) + for descriptor in computed: + d_type = f'{type_base}{descriptor.type}' + if d_type in existing: + continue + LOGGER.info(f'creating descriptor {d_type}') + if descriptor.is_ratio: + unit = '10^2.%' + value_type = 'DOUBLE' + else: + unit = '1' + value_type = 'INT64' + data = json.dumps({ + 'type': d_type, + 'displayName': descriptor.name, + 'metricKind': 'GAUGE', + 'valueType': value_type, + 'unit': unit, + 'monitoredResourceTypes': ['global'], + 'labels': [{ + 'key': l, + 'valueType': 'STRING' + } for l in descriptor.labels] + }) + yield HTTPRequest(url, HEADERS, data) + + +def timeseries_requests(project_id, root, timeseries, descriptors): + 'Returns create requests for timeseries.' + descriptor_valuetypes = {d.type: d.is_ratio for d in descriptors} + end_time = ''.join((datetime.datetime.utcnow().isoformat('T'), 'Z')) + type_base = DESCRIPTOR_TYPE_BASE.format(root) + url = TIMESERIES_URL.format(project_id) + # group timeseries in buckets by their type so that multiple timeseries + # can be grouped in a single API request without grouping duplicates types + ts_buckets = {} + for ts in timeseries: + bucket = ts_buckets.setdefault(ts.metric, collections.deque()) + bucket.append(ts) + LOGGER.info(f'metric types {list(ts_buckets.keys())}') + ts_buckets = list(ts_buckets.values()) + while ts_buckets: + data = {'timeSeries': []} + for bucket in ts_buckets: + ts = bucket.popleft() + if descriptor_valuetypes[ts.metric]: + pv = 'doubleValue' + else: + pv = 'int64Value' + data['timeSeries'].append({ + 'metric': { + 'type': f'{type_base}{ts.metric}', + 'labels': ts.labels + }, + 'resource': { + 'type': 'global' + }, + 'points': [{ + 'interval': { + 'endTime': end_time + }, + 'value': { + pv: ts.value + } + }] + }) + req_num = len(data['timeSeries']) + tot_num = sum(len(b) for b in ts_buckets) + LOGGER.info(f'sending {req_num} remaining: {tot_num}') + yield HTTPRequest(url, HEADERS, json.dumps(data)) + ts_buckets = [b for b in ts_buckets if b] diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/series-firewall-policies.py b/blueprints/cloud-operations/network-dashboard/src/plugins/series-firewall-policies.py new file mode 100644 index 000000000..defd69753 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/series-firewall-policies.py @@ -0,0 +1,43 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'Prepares descriptors and timeseries for firewall policy resources.' + +import logging + +from . import MetricDescriptor, TimeSeries, register_timeseries + +DESCRIPTOR_ATTRS = { + 'tuples_used': 'Firewall tuples used per policy', + 'tuples_available': 'Firewall tuples limit per policy', + 'tuples_used_ratio': 'Firewall tuples used ratio per policy' +} +DESCRIPTOR_LABELS = ('parent', 'name') +LOGGER = logging.getLogger('net-dash.timeseries.firewall-policies') +TUPLE_LIMIT = 2000 + + +@register_timeseries +def timeseries(resources): + 'Returns used/available/ratio firewall tuples timeseries by policy.' + LOGGER.info('timeseries') + for dtype, name in DESCRIPTOR_ATTRS.items(): + yield MetricDescriptor(f'firewall_policy/{dtype}', name, DESCRIPTOR_LABELS, + dtype.endswith('ratio')) + for v in resources['firewall_policies'].values(): + tuples = int(v['num_tuples']) + labels = {'parent': v['parent'], 'name': v['name']} + yield TimeSeries('firewall_policy/tuples_used', tuples, labels) + yield TimeSeries('firewall_policy/tuples_available', TUPLE_LIMIT, labels) + yield TimeSeries('firewall_policy/tuples_used_ratio', tuples / TUPLE_LIMIT, + labels) diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/series-firewall-rules.py b/blueprints/cloud-operations/network-dashboard/src/plugins/series-firewall-rules.py new file mode 100644 index 000000000..5490e6d3b --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/series-firewall-rules.py @@ -0,0 +1,59 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'Prepares descriptors and timeseries for firewall rules by project and network.' + +import itertools +import logging + +from . import MetricDescriptor, TimeSeries, register_timeseries + +DESCRIPTOR_ATTRS = { + 'firewall_rules_used': 'Firewall rules used per project', + 'firewall_rules_available': 'Firewall rules limit per project', + 'firewall_rules_used_ratio': 'Firewall rules used ratio per project', +} +LOGGER = logging.getLogger('net-dash.timeseries.firewall-rules') + + +@register_timeseries +def timeseries(resources): + 'Returns used/available/ratio firewall timeseries by project and network.' + LOGGER.info('timeseries') + # return a single descriptor for network as we don't have limits + yield MetricDescriptor(f'network/firewall_rules_used', + 'Firewall rules used per network', ('project', 'name')) + # return used/vailable/ratio descriptors for project + for dtype, name in DESCRIPTOR_ATTRS.items(): + yield MetricDescriptor(f'project/{dtype}', name, ('project',), + dtype.endswith('ratio')) + # group firewall rules by network then prepare and return timeseries + grouped = itertools.groupby(resources['firewall_rules'].values(), + lambda v: v['network']) + for network_id, rules in grouped: + count = len(list(rules)) + labels = { + 'name': resources['networks'][network_id]['name'], + 'project': resources['networks'][network_id]['project_id'] + } + yield TimeSeries('network/firewall_rules_used', count, labels) + # group firewall rules by project then prepare and return timeseries + grouped = itertools.groupby(resources['firewall_rules'].values(), + lambda v: v['project_id']) + for project_id, rules in grouped: + count = len(list(rules)) + limit = int(resources['quota'][project_id]['global']['FIREWALLS']) + labels = {'project': project_id} + yield TimeSeries('project/firewall_rules_used', count, labels) + yield TimeSeries('project/firewall_rules_available', limit, labels) + yield TimeSeries('project/firewall_rules_used_ratio', count / limit, labels) diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/series-networks.py b/blueprints/cloud-operations/network-dashboard/src/plugins/series-networks.py new file mode 100644 index 000000000..0ce7a4b30 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/series-networks.py @@ -0,0 +1,142 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''Prepares descriptors and timeseries for network-level metrics. + +This plugin computes metrics for a variety of network resource types like +subnets, instances, peerings, etc. It mostly does so by first grouping +resources for a type, and then using a generalized function to derive counts +and ratios and compute the actual timeseries. +''' + +import functools +import itertools +import logging + +from . import MetricDescriptor, TimeSeries, register_timeseries + +DESCRIPTOR_ATTRS = { + 'forwarding_rules_l4_available': 'L4 fwd rules limit per network', + 'forwarding_rules_l4_used': 'L4 fwd rules used per network', + 'forwarding_rules_l4_used_ratio': 'L4 fwd rules used ratio per network', + 'forwarding_rules_l7_available': 'L7 fwd rules limit per network', + 'forwarding_rules_l7_used': 'L7 fwd rules used per network', + 'forwarding_rules_l7_used_ratio': 'L7 fwd rules used ratio per network', + 'instances_available': 'Instance limit per network', + 'instances_used': 'Instance used per network', + 'instances_used_ratio': 'Instance used ratio per network', + 'peerings_active_available': 'Active peering limit per network', + 'peerings_active_used': 'Active peering used per network', + 'peerings_active_used_ratio': 'Active peering used ratio per network', + 'peerings_total_available': 'Total peering limit per network', + 'peerings_total_used': 'Total peering used per network', + 'peerings_total_used_ratio': 'Total peering used ratio per network', + 'subnets_available': 'Subnet limit per network', + 'subnets_used': 'Subnet used per network', + 'subnets_used_ratio': 'Subnet used ratio per network' +} +LIMITS = { + 'INSTANCES_PER_NETWORK_GLOBAL': 15000, + 'INTERNAL_FORWARDING_RULES_PER_NETWORK': 500, + 'INTERNAL_MANAGED_FORWARDING_RULES_PER_NETWORK': 75, + 'ROUTES': 250, + 'SUBNET_RANGES_PER_NETWORK': 300 +} +LOGGER = logging.getLogger('net-dash.timeseries.networks') + + +def _group_timeseries(name, resources, grouped, limit_name): + 'Generalized function that returns timeseries from data grouped by network.' + for network_id, elements in grouped: + network = resources['networks'].get(network_id) + if not network: + LOGGER.info(f'out of scope {name} network {network_id}') + continue + count = len(list(elements)) + labels = {'project': network['project_id'], 'network': network['name']} + quota = resources['quota'][network['project_id']]['global'] + limit = quota.get(limit_name, LIMITS[limit_name]) + yield TimeSeries(f'network/{name}_used', count, labels) + yield TimeSeries(f'network/{name}_available', limit, labels) + yield TimeSeries(f'network/{name}_used_ratio', count / limit, labels) + + +def _forwarding_rules(resources): + 'Groups forwarding rules by network/type and returns relevant timeseries.' + # create two separate iterators filtered by L4 and L7 balancing schemes + filter = lambda n, v: v['load_balancing_scheme'] != n + forwarding_rules = resources['forwarding_rules'].values() + forwarding_rules_l4 = itertools.filterfalse( + functools.partial(filter, 'INTERNAL'), forwarding_rules) + forwarding_rules_l7 = itertools.filterfalse( + functools.partial(filter, 'INTERNAL_MANAGED'), forwarding_rules) + # group each iterator by network and return timeseries + grouped_l4 = itertools.groupby(forwarding_rules_l4, lambda i: i['network']) + grouped_l7 = itertools.groupby(forwarding_rules_l7, lambda i: i['network']) + return itertools.chain( + _group_timeseries('forwarding_rules_l4', resources, grouped_l4, + 'INTERNAL_FORWARDING_RULES_PER_NETWORK'), + _group_timeseries('forwarding_rules_l7', resources, grouped_l7, + 'INTERNAL_MANAGED_FORWARDING_RULES_PER_NETWORK'), + ) + + +def _instances(resources): + 'Groups instances by network and returns relevant timeseries.' + instance_networks = itertools.chain.from_iterable( + i['networks'] for i in resources['instances'].values()) + grouped = itertools.groupby(instance_networks, lambda i: i['network']) + return _group_timeseries('instances', resources, grouped, + 'INSTANCES_PER_NETWORK_GLOBAL') + + +def _peerings(resources): + 'Counts peerings by network and returns relevant timeseries.' + quota = resources['quota'] + for network_id, network in resources['networks'].items(): + labels = {'project': network['project_id'], 'network': network['name']} + limit = quota.get(network_id, {}).get('PEERINGS_PER_NETWORK', 250) + p_active = len([p for p in network['peerings'] if p['active']]) + p_total = len(network['peerings']) + yield TimeSeries('network/peerings_active_used', p_active, labels) + yield TimeSeries('network/peerings_active_available', limit, labels) + yield TimeSeries('network/peerings_active_used_ratio', p_active / limit, + labels) + yield TimeSeries('network/peerings_total_used', p_total, labels) + yield TimeSeries('network/peerings_total_available', limit, labels) + yield TimeSeries('network/peerings_total_used_ratio', p_total / limit, + labels) + + +def _subnet_ranges(resources): + 'Groups subnetworks by network and returns relevant timeseries.' + grouped = itertools.groupby(resources['subnetworks'].values(), + lambda v: v['network']) + return _group_timeseries('subnets', resources, grouped, + 'SUBNET_RANGES_PER_NETWORK') + + +@register_timeseries +def timeseries(resources): + 'Returns used/available/ratio timeseries by network for different resources.' + LOGGER.info('timeseries') + # return descriptors + for dtype, name in DESCRIPTOR_ATTRS.items(): + yield MetricDescriptor(f'network/{dtype}', name, ('project', 'network'), + dtype.endswith('ratio')) + + # chain iterators from specialized functions and yield combined timeseries + results = itertools.chain(_forwarding_rules(resources), _instances(resources), + _peerings(resources), _subnet_ranges(resources)) + for result in results: + yield result diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/series-peering-groups.py b/blueprints/cloud-operations/network-dashboard/src/plugins/series-peering-groups.py new file mode 100644 index 000000000..9f7926850 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/series-peering-groups.py @@ -0,0 +1,180 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'Prepares descriptors and timeseries for peering group metrics.' + +import itertools +import logging + +from . import MetricDescriptor, TimeSeries, register_timeseries + +DESCRIPTOR_ATTRS = { + 'forwarding_rules_l4_available': + 'L4 fwd rules limit per peering group', + 'forwarding_rules_l4_used': + 'L4 fwd rules used per peering group', + 'forwarding_rules_l4_used_ratio': + 'L4 fwd rules used ratio per peering group', + 'forwarding_rules_l7_available': + 'L7 fwd rules limit per peering group', + 'forwarding_rules_l7_used': + 'L7 fwd rules used per peering group', + 'forwarding_rules_l7_used_ratio': + 'L7 fwd rules used ratio per peering group', + 'instances_available': + 'Instance limit per peering group', + 'instances_used': + 'Instance used per peering group', + 'instances_used_ratio': + 'Instance used ratio per peering group', + 'routes_dynamic_available': + 'Dynamic route limit per peering group', + 'routes_dynamic_used': + 'Dynamic route used per peering group', + 'routes_dynamic_used_ratio': + 'Dynamic route used ratio per peering group', + 'routes_static_available': + 'Static route limit per peering group', + 'routes_static_used': + 'Static route used per peering group', + 'routes_static_used_ratio': + 'Static route used ratio per peering group', +} +LIMITS = { + 'forwarding_rules_l4': { + 'pg': ('INTERNAL_FORWARDING_RULES_PER_PEERING_GROUP', 500), + 'prj': ('INTERNAL_FORWARDING_RULES_PER_NETWORK', 500) + }, + 'forwarding_rules_l7': { + 'pg': ('INTERNAL_MANAGED_FORWARDING_RULES_PER_PEERING_GROUP', 175), + 'prj': ('INTERNAL_MANAGED_FORWARDING_RULES_PER_NETWORK', 75) + }, + 'instances': { + 'pg': ('INSTANCES_PER_PEERING_GROUP', 15500), + 'prj': ('INSTANCES_PER_NETWORK_GLOBAL', 15000) + }, + 'routes_static': { + 'pg': ('STATIC_ROUTES_PER_PEERING_GROUP', 300), + 'prj': ('ROUTES', 250) + }, + 'routes_dynamic': { + 'pg': ('DYNAMIC_ROUTES_PER_PEERING_GROUP', 300), + 'prj': ('', 100) + } +} +LOGGER = logging.getLogger('net-dash.timeseries.peerings') + + +def _count_forwarding_rules_l4(resources, network_ids): + 'Returns count of L4 forwarding rules for specified network ids.' + return len([ + r for r in resources['forwarding_rules'].values() if + r['network'] in network_ids and r['load_balancing_scheme'] == 'INTERNAL' + ]) + + +def _count_forwarding_rules_l7(resources, network_ids): + 'Returns count of L7 forwarding rules for specified network ids.' + return len([ + r for r in resources['forwarding_rules'].values() + if r['network'] in network_ids and + r['load_balancing_scheme'] == 'INTERNAL_MANAGED' + ]) + + +def _count_instances(resources, network_ids): + 'Returns count of instances for specified network ids.' + count = 0 + for i in resources['instances'].values(): + if any(n['network'] in network_ids for n in i['networks']): + count += 1 + return count + + +def _count_routes_static(resources, network_ids): + 'Returns count of static routes for specified network ids.' + return len( + [r for r in resources['routes'].values() if r['network'] in network_ids]) + + +def _count_routes_dynamic(resources, network_ids): + 'Returns count of dynamic routes for specified network ids.' + return sum([ + sum(v.values()) + for k, v in resources['routes_dynamic'].items() + if k in network_ids + ]) + + +def _get_limit_max(quota, network_id, project_id, resource_name): + 'Returns maximum limit value in project / peering group / network limits.' + pg_name, pg_default = LIMITS[resource_name]['pg'] + prj_name, prj_default = LIMITS[resource_name]['prj'] + network_quota = quota.get(network_id, {}) + project_quota = quota.get(project_id, {}).get('global', {}) + return max([ + network_quota.get(pg_name, 0), + project_quota.get(prj_name, prj_default), + project_quota.get(pg_name, pg_default) + ]) + + +def _get_limit(quota, network, resource_name): + 'Computes and returns peering group limit.' + # reference https://cloud.google.com/vpc/docs/quota#vpc-peering-ilb-example + # step 1 - vpc_max = max(vpc limit, pg limit) + vpc_max = _get_limit_max(quota, network['self_link'], network['project_id'], + resource_name) + # step 2 - peers_max = [max(vpc limit, pg limit) for v in peered vpcs] + # step 3 - peers_min = min(peers_max) + peers_min = min([ + _get_limit_max(quota, p['network'], p['project_id'], resource_name) + for p in network['peerings'] + ]) + # step 4 - max(vpc_max, peers_min) + return max([vpc_max, peers_min]) + + +def _peering_group_timeseries(resources, network): + 'Computes and returns peering group timeseries for network.' + if len(network['peerings']) == 0: + return + network_ids = [network['self_link'] + ] + [p['network'] for p in network['peerings']] + for resource_name in LIMITS: + limit = _get_limit(resources['quota'], network, resource_name) + func = globals().get(f'_count_{resource_name}') + if not func or not callable(func): + LOGGER.critical(f'no handler for {resource_name} or handler not callable') + continue + count = func(resources, network_ids) + labels = {'project': network['project_id'], 'network': network['name']} + yield TimeSeries(f'peering_group/{resource_name}_used', count, labels) + yield TimeSeries(f'peering_group/{resource_name}_available', limit, labels) + yield TimeSeries(f'peering_group/{resource_name}_used_ratio', count / limit, + labels) + + +@register_timeseries +def timeseries(resources): + 'Returns peering group timeseries for all networks.' + LOGGER.info('timeseries') + # returns metric descriptors + for dtype, name in DESCRIPTOR_ATTRS.items(): + yield MetricDescriptor(f'peering_group/{dtype}', name, + ('project', 'network'), dtype.endswith('ratio')) + # chain timeseries for each network and return each one individually + results = itertools.chain(*(_peering_group_timeseries(resources, n) + for n in resources['networks'].values())) + for result in results: + yield result diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/series-psa.py b/blueprints/cloud-operations/network-dashboard/src/plugins/series-psa.py new file mode 100644 index 000000000..82e06009d --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/series-psa.py @@ -0,0 +1,101 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'Prepares descriptors and timeseries for subnetwork-level metrics.' + +import collections +import ipaddress +import itertools +import logging + +from . import MetricDescriptor, TimeSeries, register_timeseries + +DESCRIPTOR_ATTRS = { + 'addresses_available': 'Address limit per psa range', + 'addresses_used': 'Addresses used per psa range', + 'addresses_used_ratio': 'Addresses used ratio per psa range' +} +LOGGER = logging.getLogger('net-dash.timeseries.psa') + + +def _sql_addresses(sql_instances): + 'Returns counts of Cloud SQL instances per PSA range.' + for v in sql_instances.values(): + if not v['ipAddresses']: + continue + # 1 IP for the instance + 1 IP for the ILB + 1 IP if HA + yield v['ipAddresses'][ + 0], 2 if v['availabilityType'] != 'REGIONAL' else 3, v['network'] + + +def _filestore_addresses(filestore_instances): + 'Returns counts of Filestore instances per PSA range.' + for v in filestore_instances.values(): + if not v['ipAddresses'] or not v['reservedIpRange']: + continue + # Subnet size (reservedIpRange) can be /29, /26 or /24 + yield v['ipAddresses'][0], ipaddress.ip_network( + v['reservedIpRange']).num_addresses, v['network'] + + +def _memorystore_addresses(memorystore_instances): + 'Returns counts of Memorystore (Redis) instances per PSA range.' + for v in memorystore_instances.values(): + if not v['reservedIpRange'] or v['reservedIpRange'] == '': + continue + # Subnet size (reservedIpRange) can be minimum /28 or /29 + yield v['host'], ipaddress.ip_network( + v['reservedIpRange']).num_addresses, v['network'] + + +@register_timeseries +def timeseries(resources): + 'Returns used/available/ratio timeseries for addresses by PSA ranges.' + LOGGER.info('timeseries') + for dtype, name in DESCRIPTOR_ATTRS.items(): + yield MetricDescriptor(f'network/psa/{dtype}', name, + ('project', 'network', 'subnetwork'), + dtype.endswith('ratio')) + psa_nets = { + k: { + 'network_link': + v['network'], + 'network_prefix': + ipaddress.ip_network('{}/{}'.format(v['address'], + v['prefixLength'])) + } for k, v in resources['global_addresses'].items() if v['prefixLength'] + } + psa_counts = {} + for address, ip_count, network in itertools.chain( + _sql_addresses(resources.get('sql_instances', {})), + _filestore_addresses(resources.get('filestore_instances', {})), + _memorystore_addresses(resources.get('memorystore_instances', {}))): + ip_address = ipaddress.ip_address(address) + for k, v in psa_nets.items(): + if network == v['network_link'] and ip_address in v['network_prefix']: + psa_counts[k] = psa_counts.get(k, 0) + ip_count + break + + for k, v in psa_counts.items(): + max_ips = psa_nets[k]['network_prefix'].num_addresses - 4 + psa_range = resources['global_addresses'][k] + labels = { + 'network': psa_range['network'], + 'project': psa_range['project_id'], + 'psa_range': psa_range['name'] + } + + yield TimeSeries('network/psa/addresses_available', max_ips, labels) + yield TimeSeries('network/psa/addresses_used', v, labels) + yield TimeSeries('network/psa/addresses_used_ratio', + 0 if v == 0 else v / max_ips, labels) diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/series-routes.py b/blueprints/cloud-operations/network-dashboard/src/plugins/series-routes.py new file mode 100644 index 000000000..89011215c --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/series-routes.py @@ -0,0 +1,93 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'Prepares descriptors and timeseries for network-level route metrics.' + +import itertools +import logging + +from . import MetricDescriptor, TimeSeries, register_timeseries + +DESCRIPTOR_ATTRS = { + 'network/routes_dynamic_used': + 'Dynamic routes limit per network', + 'network/routes_dynamic_available': + 'Dynamic routes used per network', + 'network/routes_dynamic_used_ratio': + 'Dynamic routes used ratio per network', + 'network/routes_static_used': + 'Static routes limit per network', + 'project/routes_dynamic_used': + 'Dynamic routes limit per project', + 'project/routes_dynamic_available': + 'Dynamic routes used per project', + 'project/routes_dynamic_used_ratio': + 'Dynamic routes used ratio per project', + 'project/routes_static_used': + 'Static routes limit per project', + 'project/routes_static_available': + 'Static routes used per project', + 'project/routes_static_used_ratio': + 'Static routes used ratio per project' +} +LIMITS = {'ROUTES': 250, 'ROUTES_DYNAMIC': 100} +LOGGER = logging.getLogger('net-dash.timeseries.routes') + + +def _dynamic(resources): + 'Computes network-level timeseries for dynamic routes.' + for network_id, router_counts in resources['routes_dynamic'].items(): + network = resources['networks'][network_id] + count = sum(router_counts.values()) + labels = {'project': network['project_id'], 'network': network['name']} + limit = LIMITS['ROUTES_DYNAMIC'] + yield TimeSeries('network/routes_dynamic_used', count, labels) + yield TimeSeries('network/routes_dynamic_available', limit, labels) + yield TimeSeries('network/routes_dynamic_used_ratio', count / limit, labels) + + +def _static(resources): + 'Computes network and project-level timeseries for dynamic routes.' + filter = lambda v: v['next_hop_type'] in ('peering', 'network') + routes = itertools.filterfalse(filter, resources['routes'].values()) + grouped = itertools.groupby(routes, lambda v: v['network']) + project_counts = {} + for network_id, elements in grouped: + network = resources['networks'].get(network_id) + count = len(list(elements)) + labels = {'project': network['project_id'], 'network': network['name']} + yield TimeSeries('network/routes_static_used', count, labels) + project_counts[network['project_id']] = project_counts.get( + network['project_id'], 0) + count + for project_id, count in project_counts.items(): + labels = {'project': project_id} + quota = resources['quota'][project_id]['global'] + limit = quota.get('ROUTES', LIMITS['ROUTES']) + yield TimeSeries('project/routes_static_used', count, labels) + yield TimeSeries('project/routes_static_available', limit, labels) + yield TimeSeries('project/routes_static_used_ratio', count / limit, labels) + + +@register_timeseries +def timeseries(resources): + 'Returns used/available/ratio timeseries by network and project.' + LOGGER.info('timeseries') + # return descriptors + for dtype, name in DESCRIPTOR_ATTRS.items(): + labels = ('project') if dtype.startswith('project') else ('project', + 'network') + yield MetricDescriptor(dtype, name, labels, dtype.endswith('ratio')) + # chain static and dynamic route timeseries then return each one individually + results = itertools.chain(_static(resources), _dynamic(resources)) + for result in results: + yield result diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/series-subnets.py b/blueprints/cloud-operations/network-dashboard/src/plugins/series-subnets.py new file mode 100644 index 000000000..a9f0a5f30 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/series-subnets.py @@ -0,0 +1,100 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'Prepares descriptors and timeseries for subnetwork-level metrics.' + +import collections +import ipaddress +import itertools +import logging + +from . import MetricDescriptor, TimeSeries, register_timeseries + +DESCRIPTOR_ATTRS = { + 'addresses_available': 'Address limit per subnet', + 'addresses_used': 'Addresses used per subnet', + 'addresses_used_ratio': 'Addresses used ratio per subnet' +} +LOGGER = logging.getLogger('net-dash.timeseries.subnets') + + +def _subnet_addresses(resources): + 'Returns count of addresses per subnetwork.' + for v in resources['addresses'].values(): + if v['status'] != 'RESERVED': + continue + if v['purpose'] in ('GCE_ENDPOINT', 'DNS_RESOLVER'): + yield v['subnetwork'], 1 + + +def _subnet_forwarding_rules(resources, subnet_nets): + 'Returns counts of forwarding rules per subnetwork.' + for v in resources['forwarding_rules'].values(): + if v['load_balancing_scheme'].startswith('INTERNAL'): + yield v['subnetwork'], 1 + continue + if v['psc_accepted']: + network = resources['networks'].get(v['network']) + if not network: + LOGGER.warn(f'PSC address for missing network {v["network"]}') + continue + address = ipaddress.ip_address(v['address']) + for subnet_self_link in network['subnetworks']: + if address in subnet_nets[subnet_self_link]: + yield subnet_self_link, 1 + break + continue + + +def _subnet_instances(resources): + 'Returns counts of instances per subnetwork.' + vm_networks = itertools.chain.from_iterable( + i['networks'] for i in resources['instances'].values()) + return collections.Counter(v['subnetwork'] for v in vm_networks).items() + + +@register_timeseries +def timeseries(resources): + 'Returns used/available/ratio timeseries for addresses by subnetwork.' + LOGGER.info('timeseries') + # return descriptors + for dtype, name in DESCRIPTOR_ATTRS.items(): + yield MetricDescriptor(f'subnetwork/{dtype}', name, + ('project', 'network', 'subnetwork', 'region'), + dtype.endswith('ratio')) + # aggregate per-resource counts in total per-subnet counts + subnet_nets = { + k: ipaddress.ip_network(v['cidr_range']) + for k, v in resources['subnetworks'].items() + } + # TODO: add counter functions for PSA + subnet_counts = {k: 0 for k in resources['subnetworks']} + counters = itertools.chain(_subnet_addresses(resources), + _subnet_forwarding_rules(resources, subnet_nets), + _subnet_instances(resources)) + for subnet_self_link, count in counters: + subnet_counts[subnet_self_link] += count + # compute and return metrics + for subnet_self_link, count in subnet_counts.items(): + max_ips = subnet_nets[subnet_self_link].num_addresses - 4 + subnet = resources['subnetworks'][subnet_self_link] + labels = { + 'network': resources['networks'][subnet['network']]['name'], + 'project': subnet['project_id'], + 'region': subnet['region'], + 'subnetwork': subnet['name'] + } + yield TimeSeries('subnetwork/addresses_available', max_ips, labels) + yield TimeSeries('subnetwork/addresses_used', count, labels) + yield TimeSeries('subnetwork/addresses_used_ratio', + 0 if count == 0 else count / max_ips, labels) diff --git a/blueprints/cloud-operations/network-dashboard/src/plugins/utils.py b/blueprints/cloud-operations/network-dashboard/src/plugins/utils.py new file mode 100644 index 000000000..5be659988 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/plugins/utils.py @@ -0,0 +1,101 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'Utility functions for API requests and responses.' + +import itertools +import json +import logging +import re + +from . import HTTPRequest, PluginError + +MP_PART = '''\ +Content-Type: application/http +MIME-Version: 1.0 +Content-Transfer-Encoding: binary + +GET {}?alt=json HTTP/1.1 +Content-Type: application/json +MIME-Version: 1.0 +Content-Length: 0 +Accept: application/json +Accept-Encoding: gzip, deflate +Host: compute.googleapis.com + +''' +RE_URL = re.compile(r'nextPageToken=[^&]+&?') + + +def batched(iterable, n): + 'Batches data into lists of length n. The last batch may be shorter.' + # batched('ABCDEFG', 3) --> ABC DEF G + if n < 1: + raise ValueError('n must be at least one') + it = iter(iterable) + while (batch := list(itertools.islice(it, n))): + yield batch + + +def parse_cai_results(data, name, resource_type=None, method='search'): + 'Parses an asset API response and returns individual results.' + results = data.get('results' if method == 'search' else 'assets') + if not results: + logging.info(f'no results for {name}') + return + for result in results: + if resource_type and result['assetType'] != resource_type: + logging.warn(f'result for wrong type {result["assetType"]}') + continue + yield result + + +def parse_page_token(data, url): + 'Detect next page token in result and return next page URL.' + page_token = data.get('nextPageToken') + if page_token: + logging.info(f'page token {page_token}') + if page_token: + return RE_URL.sub(f'pageToken={page_token}&', url) + + +def poor_man_mp_request(urls, boundary='1234567890'): + 'Bundles URLs into a single multipart mixed batched request.' + boundary = f'--{boundary}' + data = [boundary] + for url in urls: + data += ['\n', MP_PART.format(url), boundary] + data.append('--\n') + headers = {'content-type': f'multipart/mixed; boundary={boundary[2:]}'} + return HTTPRequest('https://compute.googleapis.com/batch/compute/v1', headers, + ''.join(data), False) + + +def poor_man_mp_response(content_type, content): + 'Parses a multipart mixed response and returns individual parts.' + try: + _, boundary = content_type.split('=') + except ValueError: + raise PluginError('no boundary found in content type') + content = content.decode('utf-8').strip()[:-2] + if boundary not in content: + raise PluginError('MIME boundary not found') + for part in content.split(f'--{boundary}'): + part = part.strip() + if not part: + continue + try: + mime_header, header, body = part.split('\r\n\r\n', 3) + except ValueError: + raise PluginError('cannot parse MIME part') + yield json.loads(body) diff --git a/blueprints/cloud-operations/network-dashboard/src/requirements.txt b/blueprints/cloud-operations/network-dashboard/src/requirements.txt new file mode 100644 index 000000000..3ca529bc3 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/requirements.txt @@ -0,0 +1,4 @@ +click==8.1.3 +google-auth==2.14.1 +PyYAML==6.0 +requests==2.28.1 diff --git a/blueprints/cloud-operations/network-dashboard/src/tools/remove-descriptors.py b/blueprints/cloud-operations/network-dashboard/src/tools/remove-descriptors.py new file mode 100755 index 000000000..93b1110e4 --- /dev/null +++ b/blueprints/cloud-operations/network-dashboard/src/tools/remove-descriptors.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'Delete metric descriptors matching filter.' + +import json +import logging + +import click +import google.auth + +from google.auth.transport.requests import AuthorizedSession + +HEADERS = {'content-type': 'application/json'} +HTTP = AuthorizedSession(google.auth.default()[0]) +URL_DELETE = 'https://monitoring.googleapis.com/v3/{}' +URL_LIST = ( + 'https://monitoring.googleapis.com/v3/projects/{}' + '/metricDescriptors?filter=metric.type=starts_with("custom.googleapis.com/netmon/")' + '&alt=json') + + +def fetch(url, delete=False): + 'Minimal HTTP client interface for API calls.' + # try + try: + if not delete: + response = HTTP.get(url, headers=HEADERS) + else: + response = HTTP.delete(url) + except google.auth.exceptions.RefreshError as e: + raise SystemExit(e.args[0]) + if response.status_code != 200: + logging.critical(f'response code {response.status_code} for URL {url}') + logging.critical(response.content) + return + return response.json() + + +@click.command() +@click.option('--monitoring-project', '-op', required=True, type=str, + help='GCP monitoring project where metrics will be stored.') +def main(monitoring_project): + 'Module entry point.' + # if not click.confirm('Do you want to continue?'): + # raise SystemExit(0) + logging.info('fetching descriptors') + result = fetch(URL_LIST.format(monitoring_project)) + descriptors = result.get('metricDescriptors') + if not descriptors: + raise SystemExit(0) + logging.info(f'{len(descriptors)} descriptors') + for d in descriptors: + name = d['name'] + logging.info(f'delete {name}') + result = fetch(URL_DELETE.format(name), True) + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + main() diff --git a/blueprints/cloud-operations/network-dashboard/tests/README.md b/blueprints/cloud-operations/network-dashboard/tests/README.md deleted file mode 100644 index 6e4779d45..000000000 --- a/blueprints/cloud-operations/network-dashboard/tests/README.md +++ /dev/null @@ -1 +0,0 @@ -Creating here resources to test the Cloud Function and ensuring metrics are correctly populated \ No newline at end of file diff --git a/blueprints/cloud-operations/network-dashboard/tests/test.tf b/blueprints/cloud-operations/network-dashboard/tests/test.tf deleted file mode 100644 index bb9d6d317..000000000 --- a/blueprints/cloud-operations/network-dashboard/tests/test.tf +++ /dev/null @@ -1,287 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -resource "google_folder" "test-net-dash" { - display_name = "test-net-dash" - parent = "organizations/${var.organization_id}" -} - -##### Creating host projects, VPCs, service projects ##### - -module "project-hub" { - source = "../../../../modules/project" - name = "test-host-hub" - parent = google_folder.test-net-dash.name - prefix = var.prefix - billing_account = var.billing_account - services = var.project_vm_services - - shared_vpc_host_config = { - enabled = true - } -} - -module "vpc-hub" { - source = "../../../../modules/net-vpc" - project_id = module.project-hub.project_id - name = "vpc-hub" - subnets = [ - { - ip_cidr_range = "10.0.10.0/24" - name = "subnet-hub-1" - region = var.region - } - ] -} - -module "project-svc-hub" { - source = "../../../../modules/project" - parent = google_folder.test-net-dash.name - billing_account = var.billing_account - prefix = var.prefix - name = "test-svc-hub" - services = var.project_vm_services - - shared_vpc_service_config = { - attach = true - host_project = module.project-hub.project_id - } -} - -module "project-prod" { - source = "../../../../modules/project" - name = "test-host-prod" - parent = google_folder.test-net-dash.name - prefix = var.prefix - billing_account = var.billing_account - services = var.project_vm_services - - shared_vpc_host_config = { - enabled = true - } -} - -module "vpc-prod" { - source = "../../../../modules/net-vpc" - project_id = module.project-prod.project_id - name = "vpc-prod" - subnets = [ - { - ip_cidr_range = "10.0.20.0/24" - name = "subnet-prod-1" - region = var.region - } - ] -} - -module "project-svc-prod" { - source = "../../../../modules/project" - parent = google_folder.test-net-dash.name - billing_account = var.billing_account - prefix = var.prefix - name = "test-svc-prod" - services = var.project_vm_services - - shared_vpc_service_config = { - attach = true - host_project = module.project-prod.project_id - } -} - -module "project-dev" { - source = "../../../../modules/project" - name = "test-host-dev" - parent = google_folder.test-net-dash.name - prefix = var.prefix - billing_account = var.billing_account - services = var.project_vm_services - - shared_vpc_host_config = { - enabled = true - } -} - -module "vpc-dev" { - source = "../../../../modules/net-vpc" - project_id = module.project-dev.project_id - name = "vpc-dev" - subnets = [ - { - ip_cidr_range = "10.0.30.0/24" - name = "subnet-dev-1" - region = var.region - } - ] -} - -module "project-svc-dev" { - source = "../../../../modules/project" - parent = google_folder.test-net-dash.name - billing_account = var.billing_account - prefix = var.prefix - name = "test-svc-dev" - services = var.project_vm_services - - shared_vpc_service_config = { - attach = true - host_project = module.project-dev.project_id - } -} - -##### Creating VPC peerings ##### - -module "hub-to-prod-peering" { - source = "../../../../modules/net-vpc-peering" - local_network = module.vpc-hub.self_link - peer_network = module.vpc-prod.self_link -} - -module "prod-to-hub-peering" { - source = "../../../../modules/net-vpc-peering" - local_network = module.vpc-prod.self_link - peer_network = module.vpc-hub.self_link - depends_on = [module.hub-to-prod-peering] -} - -module "hub-to-dev-peering" { - source = "../../../../modules/net-vpc-peering" - local_network = module.vpc-hub.self_link - peer_network = module.vpc-dev.self_link -} - -module "dev-to-hub-peering" { - source = "../../../../modules/net-vpc-peering" - local_network = module.vpc-dev.self_link - peer_network = module.vpc-hub.self_link - depends_on = [module.hub-to-dev-peering] -} - -##### Creating VMs ##### - -resource "google_compute_instance" "test-vm-prod1" { - project = module.project-svc-prod.project_id - name = "test-vm-prod1" - machine_type = "f1-micro" - zone = var.zone - - tags = ["${var.region}"] - - boot_disk { - initialize_params { - image = "debian-cloud/debian-9" - } - } - - network_interface { - subnetwork = module.vpc-prod.subnet_self_links["${var.region}/subnet-prod-1"] - subnetwork_project = module.project-prod.project_id - } - - allow_stopping_for_update = true -} - -resource "google_compute_instance" "test-vm-prod2" { - project = module.project-prod.project_id - name = "test-vm-prod2" - machine_type = "f1-micro" - zone = var.zone - - tags = [var.region] - - boot_disk { - initialize_params { - image = "debian-cloud/debian-9" - } - } - - network_interface { - subnetwork = module.vpc-prod.subnet_self_links["${var.region}/subnet-prod-1"] - subnetwork_project = module.project-prod.project_id - } - - allow_stopping_for_update = true -} - -resource "google_compute_instance" "test-vm-dev1" { - count = 10 - project = module.project-svc-dev.project_id - name = "test-vm-dev${count.index}" - machine_type = "f1-micro" - zone = var.zone - - tags = ["${var.region}"] - - boot_disk { - initialize_params { - image = "debian-cloud/debian-9" - } - } - - network_interface { - subnetwork = module.vpc-dev.subnet_self_links["${var.region}/subnet-dev-1"] - subnetwork_project = module.project-dev.project_id - } - - allow_stopping_for_update = true -} - -resource "google_compute_instance" "test-vm-hub1" { - project = module.project-svc-hub.project_id - name = "test-vm-hub1" - machine_type = "f1-micro" - zone = var.zone - - tags = ["${var.region}"] - - boot_disk { - initialize_params { - image = "debian-cloud/debian-9" - } - } - - network_interface { - subnetwork = module.vpc-hub.subnet_self_links["${var.region}/subnet-hub-1"] - subnetwork_project = module.project-hub.project_id - } - - allow_stopping_for_update = true -} - -# Forwarding Rules -resource "google_compute_forwarding_rule" "forwarding-rule-dev" { - count = 10 - name = "forwarding-rule-dev${count.index}" - project = module.project-svc-dev.project_id - network = module.vpc-dev.self_link - subnetwork = module.vpc-dev.subnet_self_links["${var.region}/subnet-dev-1"] - - region = var.region - backend_service = google_compute_region_backend_service.test-backend.id - ip_protocol = "TCP" - load_balancing_scheme = "INTERNAL" - all_ports = true - allow_global_access = true - -} - -# backend service -resource "google_compute_region_backend_service" "test-backend" { - name = "test-backend" - region = var.region - project = module.project-svc-dev.project_id - protocol = "TCP" - load_balancing_scheme = "INTERNAL" -} diff --git a/blueprints/cloud-operations/network-dashboard/tests/variables.tf b/blueprints/cloud-operations/network-dashboard/tests/variables.tf deleted file mode 100644 index dd01b29fd..000000000 --- a/blueprints/cloud-operations/network-dashboard/tests/variables.tf +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -variable "organization_id" { - description = "The organization id for the associated services" -} - -variable "billing_account" { - description = "The ID of the billing account to associate this project with" -} - -variable "prefix" { - description = "Prefix used for resource names." - type = string - validation { - condition = var.prefix != "" - error_message = "Prefix cannot be empty." - } -} - -variable "project_vm_services" { - description = "Service APIs enabled by default in new projects." - default = [ - "cloudbilling.googleapis.com", - "compute.googleapis.com", - "logging.googleapis.com", - "monitoring.googleapis.com", - "servicenetworking.googleapis.com", - ] -} -variable "region" { - description = "Region used to deploy subnets" - default = "europe-west1" -} - -variable "zone" { - description = "Zone used to deploy vms" - default = "europe-west1-b" -} diff --git a/blueprints/cloud-operations/network-dashboard/variables.tf b/blueprints/cloud-operations/network-dashboard/variables.tf deleted file mode 100644 index 2744eed62..000000000 --- a/blueprints/cloud-operations/network-dashboard/variables.tf +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -variable "billing_account" { - description = "The ID of the billing account to associate this project with." -} - -variable "cf_version" { - description = "Cloud Function version 2nd Gen or 1st Gen. Possible options: 'V1' or 'V2'.Use CFv2 if your Cloud Function timeouts after 9 minutes. By default it is using CFv1." - default = "V1" - validation { - condition = var.cf_version == "V1" || var.cf_version == "V2" - error_message = "The value of cf_version must be either V1 or V2." - } -} - -variable "monitored_folders_list" { - type = list(string) - description = "ID of the projects to be monitored (where limits and quotas data will be pulled)." - default = [] -} - -variable "monitored_projects_list" { - type = list(string) - description = "ID of the projects to be monitored (where limits and quotas data will be pulled)." -} - -variable "monitoring_project_id" { - description = "Monitoring project where the dashboard will be created and the solution deployed; a project will be created if set to empty string." - default = "" -} - -variable "organization_id" { - description = "The organization id for the associated services." -} - -variable "prefix" { - description = "Prefix used for resource names." - type = string - validation { - condition = var.prefix != "" - error_message = "Prefix cannot be empty." - } -} - -variable "project_monitoring_services" { - description = "Service APIs enabled in the monitoring project if it will be created." - default = [ - "artifactregistry.googleapis.com", - "cloudasset.googleapis.com", - "cloudbilling.googleapis.com", - "cloudbuild.googleapis.com", - "cloudfunctions.googleapis.com", - "cloudresourcemanager.googleapis.com", - "cloudscheduler.googleapis.com", - "compute.googleapis.com", - "iam.googleapis.com", - "iamcredentials.googleapis.com", - "logging.googleapis.com", - "monitoring.googleapis.com", - "pubsub.googleapis.com", - "run.googleapis.com", - "servicenetworking.googleapis.com", - "serviceusage.googleapis.com", - "storage-component.googleapis.com" - ] -} -variable "region" { - description = "Region used to deploy the cloud functions and scheduler." - default = "europe-west1" -} - -variable "schedule_cron" { - description = "Cron format schedule to run the Cloud Function. Default is every 10 minutes." - default = "*/10 * * * *" -} diff --git a/blueprints/cloud-operations/onprem-sa-key-management/versions.tf b/blueprints/cloud-operations/onprem-sa-key-management/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/cloud-operations/onprem-sa-key-management/versions.tf +++ b/blueprints/cloud-operations/onprem-sa-key-management/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/cloud-operations/packer-image-builder/versions.tf b/blueprints/cloud-operations/packer-image-builder/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/cloud-operations/packer-image-builder/versions.tf +++ b/blueprints/cloud-operations/packer-image-builder/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/cloud-operations/quota-monitoring/versions.tf b/blueprints/cloud-operations/quota-monitoring/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/cloud-operations/quota-monitoring/versions.tf +++ b/blueprints/cloud-operations/quota-monitoring/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } 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 286536a65..08492c6f9 100644 --- a/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/versions.tf +++ b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md index 534d65992..fd869ae1a 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md @@ -7,7 +7,7 @@ This is a helper module to prepare GCP Credentials from Terraform Enterprise wor module "tfe_oidc" { source = "./tfc-oidc" - impersonate_service_account_email = "tfe-test@tfe-test-wif.iam.gserviceaccount.com" + impersonate_service_account_email = "tfe-test@tfe-test-wif.iam.gserviceaccount.com" } provider "google" { diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/versions.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/versions.tf index a079e99c4..08492c6f9 100644 --- a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/versions.tf +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/versions.tf @@ -14,4 +14,16 @@ terraform { required_version = ">= 1.3.1" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.50.0" # tftest + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.50.0" # tftest + } + } } + + diff --git a/blueprints/cloud-operations/unmanaged-instances-healthcheck/main.tf b/blueprints/cloud-operations/unmanaged-instances-healthcheck/main.tf index d69dddf2b..19f2d467b 100644 --- a/blueprints/cloud-operations/unmanaged-instances-healthcheck/main.tf +++ b/blueprints/cloud-operations/unmanaged-instances-healthcheck/main.tf @@ -146,13 +146,11 @@ module "cf-healthchecker" { name = "cf-healthchecker" region = var.region bucket_name = module.cf-restarter.bucket_name - bundle_config = { source_dir = "${path.module}/function/healthchecker" output_path = "healthchecker.zip" } service_account = module.service-account-healthchecker.email - function_config = { entry_point = "HealthCheck" ingress_settings = null @@ -161,7 +159,6 @@ module "cf-healthchecker" { runtime = "go116" timeout = 300 } - environment_variables = { FILTER = "name = nginx-*" GRACE_PERIOD = var.grace_period @@ -171,7 +168,6 @@ module "cf-healthchecker" { TCP_PORT = var.tcp_port TIMEOUT = var.timeout } - vpc_connector = { create = true name = "hc-connector" @@ -230,23 +226,25 @@ resource "google_cloud_scheduler_job" "healthcheck-job" { module "cos-nginx" { source = "../../../modules/cloud-config-container/nginx" - test_instance = { - project_id = module.project.project_id - zone = "${var.region}-b" - name = "nginx-test" - type = "f1-micro" +} + +module "test-vm" { + source = "../../../modules/compute-vm" + project_id = module.project.project_id + zone = "${var.region}-b" + name = "nginx-test" + boot_disk = { + image = "projects/cos-cloud/global/images/family/cos-stable" + type = "pd-ssd" + size = 10 + } + metadata = { + user-data = module.cos-nginx.cloud_config + google-logging-enabled = true + } + network_interfaces = [{ network = module.vpc.self_link subnetwork = module.vpc.subnet_self_links["${var.region}/apps"] - } - test_instance_defaults = { - disks = {} - image = null - metadata = {} - nat = false - service_account_roles = [ - "roles/logging.logWriter", - "roles/monitoring.metricWriter" - ] - tags = ["ssh"] - } + }] + tags = ["ssh"] } diff --git a/blueprints/data-solutions/README.md b/blueprints/data-solutions/README.md index 4919f29a4..3da4e7e92 100644 --- a/blueprints/data-solutions/README.md +++ b/blueprints/data-solutions/README.md @@ -52,6 +52,20 @@ running on a VPC with a private IP and a dedicated Service Account. A GCS bucket ### SQL Server Always On Availability Groups -This [blueprint](./data-platform-foundations/) implements SQL Server Always On Availability Groups using Fabric modules. It builds a two node cluster with a fileshare witness instance in an existing VPC and adds the necessary firewalling. The actual setup process (apart from Active Directory operations) has been scripted, so that least amount of manual works needs to performed. +This [blueprint](./sqlserver-alwayson/) implements SQL Server Always On Availability Groups using Fabric modules. It builds a two node cluster with a fileshare witness instance in an existing VPC and adds the necessary firewalling. The actual setup process (apart from Active Directory operations) has been scripted, so that least amount of manual works needs to performed. + +
+ +### MLOps with Vertex AI + + +This [blueprint](./vertex-mlops/) implements the infrastructure required to have a fully functional MLOPs environment using Vertex AI: required GCP services activation, Vertex Workbench, GCS buckets to host Vertex AI and Cloud Build artifacts, Artifact Registry docker repository to host custom images, required service accounts, networking and Workload Identity Federation Provider for Github integration (optional). + +
+ +### Shielded Folder + + +This [blueprint](./shielded-folder/) implements an opinionated folder configuration according to GCP best practices. Configurations implemented on the folder would be beneficial to host workloads inheriting constraints from the folder they belong to.
diff --git a/blueprints/data-solutions/cmek-via-centralized-kms/README.md b/blueprints/data-solutions/cmek-via-centralized-kms/README.md index 88590c74b..3813c90c2 100644 --- a/blueprints/data-solutions/cmek-via-centralized-kms/README.md +++ b/blueprints/data-solutions/cmek-via-centralized-kms/README.md @@ -1,8 +1,8 @@ # GCE and GCS CMEK via centralized Cloud KMS -This example creates a sample centralized [Cloud KMS](https://cloud.google.com/kms?hl=it) configuration, and uses it to implement CMEK for [Cloud Storage](https://cloud.google.com/storage/docs/encryption/using-customer-managed-keys) and [Compute Engine](https://cloud.google.com/compute/docs/disks/customer-managed-encryption) in a separate project. +This example creates a sample centralized [Cloud KMS](https://cloud.google.com/kms?hl=it) configuration, and uses it to implement CMEK for [Cloud Storage](https://cloud.google.com/storage/docs/encryption/using-customer-managed-keys) and [Compute Engine](https://cloud.google.com/compute/docs/disks/customer-managed-encryption) in a service project. -The example is designed to match real-world use cases with a minimum amount of resources, and be used as a starting point for scenarios where application projects implement CMEK using keys managed by a central team. It also includes the IAM wiring needed to make such scenarios work. +The example is designed to match real-world use cases with a minimum amount of resources, and be used as a starting point for scenarios where application projects implement CMEK using keys managed by a central team. It also includes the IAM wiring needed to make such scenarios work. Regional resources are used in this example, but the same logic will apply for 'dual regional', 'multi regional' or 'global' resources. This is the high level diagram: @@ -35,12 +35,10 @@ This sample creates several distinct groups of resources: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [billing_account](variables.tf#L16) | Billing account id used as default for new projects. | string | ✓ | | -| [root_node](variables.tf#L45) | The resource name of the parent Folder or Organization. Must be of the form folders/folder_id or organizations/org_id. | string | ✓ | | -| [location](variables.tf#L21) | The location where resources will be deployed. | string | | "europe" | -| [project_kms_name](variables.tf#L27) | Name for the new KMS Project. | string | | "my-project-kms-001" | -| [project_service_name](variables.tf#L33) | Name for the new Service Project. | string | | "my-project-service-001" | -| [region](variables.tf#L39) | The region where resources will be deployed. | string | | "europe-west1" | +| [prefix](variables.tf#L21) | Optional prefix used to generate resources names. | string | ✓ | | +| [project_config](variables.tf#L27) | Provide 'billing_account_id' and 'parent' values if project creation is needed, uses existing 'projects_id' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…}) | ✓ | | +| [location](variables.tf#L15) | The location where resources will be deployed. | string | | "europe" | +| [region](variables.tf#L44) | The region where resources will be deployed. | string | | "europe-west1" | | [vpc_ip_cidr_range](variables.tf#L50) | Ip range used in the subnet deployef in the Service Project. | string | | "10.0.0.0/20" | | [vpc_name](variables.tf#L56) | Name of the VPC created in the Service Project. | string | | "local" | | [vpc_subnet_name](variables.tf#L62) | Name of the subnet created in the Service Project. | string | | "subnet" | diff --git a/blueprints/data-solutions/cmek-via-centralized-kms/main.tf b/blueprints/data-solutions/cmek-via-centralized-kms/main.tf index fb7f9fdd1..73f2b7018 100644 --- a/blueprints/data-solutions/cmek-via-centralized-kms/main.tf +++ b/blueprints/data-solutions/cmek-via-centralized-kms/main.tf @@ -12,33 +12,61 @@ # See the License for the specific language governing permissions and # limitations under the License. +locals { + # Needed when you create KMS keys and encrypted resources in the same terraform state but different projects. + kms_keys = { + gce = "projects/${module.project-kms.project_id}/locations/${var.region}/keyRings/${var.prefix}-${var.region}/cryptoKeys/key-gcs" + gcs = "projects/${module.project-kms.project_id}/locations/${var.region}/keyRings/${var.prefix}-${var.region}/cryptoKeys/key-gcs" + } +} + ############################################################################### # Projects # ############################################################################### module "project-service" { source = "../../../modules/project" - name = var.project_service_name - parent = var.root_node - billing_account = var.billing_account + name = var.project_config.project_ids.service + parent = var.project_config.parent + billing_account = var.project_config.billing_account_id + project_create = var.project_config.billing_account_id != null + prefix = var.project_config.billing_account_id == null ? null : var.prefix services = [ "compute.googleapis.com", "servicenetworking.googleapis.com", - "storage-component.googleapis.com" + "storage.googleapis.com", + "storage-component.googleapis.com", + ] + service_encryption_key_ids = { + compute = [ + local.kms_keys.gce + ] + storage = [ + local.kms_keys.gcs + ] + } + service_config = { + disable_on_destroy = false, disable_dependent_services = false + } + depends_on = [ + module.kms ] - oslogin = true } module "project-kms" { source = "../../../modules/project" - name = var.project_kms_name - parent = var.root_node - billing_account = var.billing_account + name = var.project_config.project_ids.encryption + parent = var.project_config.parent + billing_account = var.project_config.billing_account_id + project_create = var.project_config.billing_account_id != null + prefix = var.project_config.billing_account_id == null ? null : var.prefix services = [ "cloudkms.googleapis.com", "servicenetworking.googleapis.com" ] - oslogin = true + service_config = { + disable_on_destroy = false, disable_dependent_services = false + } } ############################################################################### @@ -48,11 +76,11 @@ module "project-kms" { module "vpc" { source = "../../../modules/net-vpc" project_id = module.project-service.project_id - name = var.vpc_name + name = "${var.prefix}-vpc" subnets = [ { - ip_cidr_range = var.vpc_ip_cidr_range - name = var.vpc_subnet_name + ip_cidr_range = "10.0.0.0/20" + name = "${var.prefix}-${var.region}" region = var.region } ] @@ -63,7 +91,7 @@ module "vpc-firewall" { project_id = module.project-service.project_id network = module.vpc.name default_rules_config = { - admin_ranges = [var.vpc_ip_cidr_range] + admin_ranges = ["10.0.0.0/20"] } } @@ -75,22 +103,10 @@ module "kms" { source = "../../../modules/kms" project_id = module.project-kms.project_id keyring = { - name = "my-keyring", - location = var.location + name = "${var.prefix}-${var.region}", + location = var.region } keys = { key-gce = null, key-gcs = null } - key_iam = { - key-gce = { - "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ - "serviceAccount:${module.project-service.service_accounts.robots.compute}", - ] - }, - key-gcs = { - "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ - "serviceAccount:${module.project-service.service_accounts.robots.storage}", - ] - } - } } ############################################################################### @@ -101,10 +117,10 @@ module "vm_example" { source = "../../../modules/compute-vm" project_id = module.project-service.project_id zone = "${var.region}-b" - name = "kms-vm" + name = "${var.prefix}-vm" network_interfaces = [{ network = module.vpc.self_link, - subnetwork = module.vpc.subnet_self_links["${var.region}/subnet"], + subnetwork = module.vpc.subnet_self_links["${var.region}/${var.prefix}-${var.region}"], nat = false, addresses = null }] @@ -127,7 +143,7 @@ module "vm_example" { encryption = { encrypt_boot = true disk_encryption_key_raw = null - kms_key_self_link = module.kms.key_ids.key-gce + kms_key_self_link = local.kms_keys.gce } } @@ -138,7 +154,9 @@ module "vm_example" { module "kms-gcs" { source = "../../../modules/gcs" project_id = module.project-service.project_id - prefix = "my-bucket-001" - name = "kms-gcs" - encryption_key = module.kms.keys.key-gcs.id + prefix = var.prefix + name = "${var.prefix}-bucket" + location = var.region + storage_class = "REGIONAL" + encryption_key = local.kms_keys.gcs } diff --git a/blueprints/data-solutions/cmek-via-centralized-kms/variables.tf b/blueprints/data-solutions/cmek-via-centralized-kms/variables.tf index 737bde3dd..5d35351c9 100644 --- a/blueprints/data-solutions/cmek-via-centralized-kms/variables.tf +++ b/blueprints/data-solutions/cmek-via-centralized-kms/variables.tf @@ -12,28 +12,33 @@ # See the License for the specific language governing permissions and # limitations under the License. - -variable "billing_account" { - description = "Billing account id used as default for new projects." - type = string -} - variable "location" { description = "The location where resources will be deployed." type = string default = "europe" } -variable "project_kms_name" { - description = "Name for the new KMS Project." +variable "prefix" { + description = "Optional prefix used to generate resources names." type = string - default = "my-project-kms-001" + nullable = false } -variable "project_service_name" { - description = "Name for the new Service Project." - type = string - default = "my-project-service-001" +variable "project_config" { + description = "Provide 'billing_account_id' and 'parent' values if project creation is needed, uses existing 'projects_id' if null. Parent is in 'folders/nnn' or 'organizations/nnn' format." + type = object({ + billing_account_id = optional(string, null) + parent = optional(string, null) + project_ids = optional(object({ + encryption = string + service = string + }), { + encryption = "encryption", + service = "service" + } + ) + }) + nullable = false } variable "region" { @@ -42,11 +47,6 @@ variable "region" { default = "europe-west1" } -variable "root_node" { - description = "The resource name of the parent Folder or Organization. Must be of the form folders/folder_id or organizations/org_id." - type = string -} - variable "vpc_ip_cidr_range" { description = "Ip range used in the subnet deployef in the Service Project." type = string diff --git a/blueprints/data-solutions/cmek-via-centralized-kms/versions.tf b/blueprints/data-solutions/cmek-via-centralized-kms/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/data-solutions/cmek-via-centralized-kms/versions.tf +++ b/blueprints/data-solutions/cmek-via-centralized-kms/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/data-solutions/data-platform-foundations/03-composer.tf b/blueprints/data-solutions/data-platform-foundations/03-composer.tf index 2622ffa20..f806f0e51 100644 --- a/blueprints/data-solutions/data-platform-foundations/03-composer.tf +++ b/blueprints/data-solutions/data-platform-foundations/03-composer.tf @@ -14,6 +14,41 @@ # tfdoc:file:description Orchestration Cloud Composer definition. +locals { + env_variables = { + BQ_LOCATION = var.location + DATA_CAT_TAGS = try(jsonencode(module.common-datacatalog.tags), "{}") + DF_KMS_KEY = try(var.service_encryption_keys.dataflow, "") + DRP_PRJ = module.drop-project.project_id + DRP_BQ = module.drop-bq-0.dataset_id + DRP_GCS = module.drop-cs-0.url + DRP_PS = module.drop-ps-0.id + DWH_LAND_PRJ = module.dwh-lnd-project.project_id + DWH_LAND_BQ_DATASET = module.dwh-lnd-bq-0.dataset_id + DWH_LAND_GCS = module.dwh-lnd-cs-0.url + DWH_CURATED_PRJ = module.dwh-cur-project.project_id + DWH_CURATED_BQ_DATASET = module.dwh-cur-bq-0.dataset_id + DWH_CURATED_GCS = module.dwh-cur-cs-0.url + DWH_CONFIDENTIAL_PRJ = module.dwh-conf-project.project_id + DWH_CONFIDENTIAL_BQ_DATASET = module.dwh-conf-bq-0.dataset_id + DWH_CONFIDENTIAL_GCS = module.dwh-conf-cs-0.url + GCP_REGION = var.region + LOD_PRJ = module.load-project.project_id + LOD_GCS_STAGING = module.load-cs-df-0.url + LOD_NET_VPC = local.load_vpc + LOD_NET_SUBNET = local.load_subnet + LOD_SA_DF = module.load-sa-df-0.email + ORC_PRJ = module.orch-project.project_id + ORC_GCS = module.orch-cs-0.url + ORC_GCS_TMP_DF = module.orch-cs-df-template.url + TRF_PRJ = module.transf-project.project_id + TRF_GCS_STAGING = module.transf-cs-df-0.url + TRF_NET_VPC = local.transf_vpc + TRF_NET_SUBNET = local.transf_subnet + TRF_SA_DF = module.transf-sa-df-0.email + TRF_SA_BQ = module.transf-sa-bq-0.email + } +} module "orch-sa-cmp-0" { source = "../../../modules/iam-service-account" project_id = module.orch-project.project_id @@ -27,21 +62,51 @@ module "orch-sa-cmp-0" { } resource "google_composer_environment" "orch-cmp-0" { - provider = google-beta - project = module.orch-project.project_id - name = "${var.prefix}-orc-cmp-0" - region = var.region + count = var.composer_config.disable_deployment == true ? 0 : 1 + project = module.orch-project.project_id + name = "${var.prefix}-orc-cmp-0" + region = var.region config { - node_count = var.composer_config.node_count + 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) + image_version = try(var.composer_config.software_config.image_version, null) + } + dynamic "workloads_config" { + for_each = (try(var.composer_config.workloads_config, null) != null ? { 1 = 1 } : {}) + + content { + scheduler { + cpu = try(var.composer_config.workloads_config.scheduler.cpu, null) + memory_gb = try(var.composer_config.workloads_config.scheduler.memory_gb, null) + storage_gb = try(var.composer_config.workloads_config.scheduler.storage_gb, null) + count = try(var.composer_config.workloads_config.scheduler.count, null) + } + web_server { + cpu = try(var.composer_config.workloads_config.web_server.cpu, null) + memory_gb = try(var.composer_config.workloads_config.web_server.memory_gb, null) + storage_gb = try(var.composer_config.workloads_config.web_server.storage_gb, null) + } + worker { + cpu = try(var.composer_config.workloads_config.worker.cpu, null) + memory_gb = try(var.composer_config.workloads_config.worker.memory_gb, null) + storage_gb = try(var.composer_config.workloads_config.worker.storage_gb, null) + min_count = try(var.composer_config.workloads_config.worker.min_count, null) + max_count = try(var.composer_config.workloads_config.worker.max_count, null) + } + } + } + + environment_size = var.composer_config.environment_size + node_config { - zone = "${var.region}-b" - service_account = module.orch-sa-cmp-0.email network = local.orch_vpc subnetwork = local.orch_subnet - tags = ["composer-worker", "http-server", "https-server"] - enable_ip_masq_agent = true + service_account = module.orch-sa-cmp-0.email + enable_ip_masq_agent = "true" + tags = ["composer-worker"] ip_allocation_policy { - use_ip_aliases = "true" cluster_secondary_range_name = try( var.network_config.composer_secondary_ranges.pods, "pods" ) @@ -58,80 +123,20 @@ resource "google_composer_environment" "orch-cmp-0" { master_ipv4_cidr_block = try( var.network_config.composer_ip_ranges.gke_master, "10.20.11.0/28" ) - web_server_ipv4_cidr_block = try( - var.network_config.composer_ip_ranges.web_server, "10.20.11.16/28" - ) } - software_config { - image_version = var.composer_config.airflow_version - env_variables = merge( - var.composer_config.env_variables, { - BQ_LOCATION = var.location - DATA_CAT_TAGS = try(jsonencode(module.common-datacatalog.tags), "{}") - DF_KMS_KEY = try(var.service_encryption_keys.dataflow, "") - DRP_PRJ = module.drop-project.project_id - DRP_BQ = module.drop-bq-0.dataset_id - DRP_GCS = module.drop-cs-0.url - DRP_PS = module.drop-ps-0.id - DWH_LAND_PRJ = module.dwh-lnd-project.project_id - DWH_LAND_BQ_DATASET = module.dwh-lnd-bq-0.dataset_id - DWH_LAND_GCS = module.dwh-lnd-cs-0.url - DWH_CURATED_PRJ = module.dwh-cur-project.project_id - DWH_CURATED_BQ_DATASET = module.dwh-cur-bq-0.dataset_id - DWH_CURATED_GCS = module.dwh-cur-cs-0.url - DWH_CONFIDENTIAL_PRJ = module.dwh-conf-project.project_id - DWH_CONFIDENTIAL_BQ_DATASET = module.dwh-conf-bq-0.dataset_id - DWH_CONFIDENTIAL_GCS = module.dwh-conf-cs-0.url - DWH_PLG_PRJ = module.dwh-plg-project.project_id - DWH_PLG_BQ_DATASET = module.dwh-plg-bq-0.dataset_id - DWH_PLG_GCS = module.dwh-plg-cs-0.url - GCP_REGION = var.region - LOD_PRJ = module.load-project.project_id - LOD_GCS_STAGING = module.load-cs-df-0.url - LOD_NET_VPC = local.load_vpc - LOD_NET_SUBNET = local.load_subnet - LOD_SA_DF = module.load-sa-df-0.email - ORC_PRJ = module.orch-project.project_id - ORC_GCS = module.orch-cs-0.url - TRF_PRJ = module.transf-project.project_id - TRF_GCS_STAGING = module.transf-cs-df-0.url - TRF_NET_VPC = local.transf_vpc - TRF_NET_SUBNET = local.transf_subnet - TRF_SA_DF = module.transf-sa-df-0.email - TRF_SA_BQ = module.transf-sa-bq-0.email - } - ) - } - dynamic "encryption_config" { for_each = ( - try(local.service_encryption_keys.composer != null, false) + try(var.service_encryption_keys[var.region], null) != null ? { 1 = 1 } : {} ) content { - kms_key_name = try(local.service_encryption_keys.composer, null) + kms_key_name = try(var.service_encryption_keys[var.region], null) } } - - # dynamic "web_server_network_access_control" { - # for_each = toset( - # var.network_config.web_server_network_access_control == null - # ? [] - # : [var.network_config.web_server_network_access_control] - # ) - # content { - # dynamic "allowed_ip_range" { - # for_each = toset(web_server_network_access_control.key) - # content { - # value = allowed_ip_range.key - # } - # } - # } - # } - } depends_on = [ google_project_iam_member.shared_vpc, + module.orch-project ] } diff --git a/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf index 2974c1227..a202afdd0 100644 --- a/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf +++ b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf @@ -25,6 +25,11 @@ locals { ? var.network_config.network_self_link : module.orch-vpc.0.self_link ) + + # Note: This formatting is needed for output purposes since the fabric artifact registry + # module doesn't yet expose the docker usage path of a registry folder in the needed format. + orch_docker_path = format("%s-docker.pkg.dev/%s/%s", + var.region, module.orch-project.project_id, module.orch-artifact-reg.name) } module "orch-project" { @@ -44,6 +49,8 @@ module "orch-project" { "roles/iam.serviceAccountUser", "roles/storage.objectAdmin", "roles/storage.admin", + "roles/artifactregistry.admin", + "roles/serviceusage.serviceUsageConsumer", ] } iam = { @@ -54,6 +61,9 @@ module "orch-project" { "roles/bigquery.jobUser" = [ module.orch-sa-cmp-0.iam_email, ] + "roles/composer.ServiceAgentV2Ext" = [ + "serviceAccount:${module.orch-project.service_accounts.robots.composer}" + ] "roles/composer.worker" = [ module.orch-sa-cmp-0.iam_email ] @@ -62,16 +72,19 @@ module "orch-project" { ] "roles/storage.objectAdmin" = [ module.orch-sa-cmp-0.iam_email, + module.orch-sa-df-build.iam_email, "serviceAccount:${module.orch-project.service_accounts.robots.composer}", + "serviceAccount:${module.orch-project.service_accounts.robots.cloudbuild}", + ] + "roles/artifactregistry.reader" = [ + module.load-sa-df-0.iam_email, + ] + "roles/cloudbuild.serviceAgent" = [ + module.orch-sa-df-build.iam_email, ] "roles/storage.objectViewer" = [module.load-sa-df-0.iam_email] } oslogin = false - org_policies = { - "constraints/compute.requireOsLogin" = { - enforce = false - } - } services = concat(var.project_services, [ "artifactregistry.googleapis.com", "bigquery.googleapis.com", @@ -83,6 +96,7 @@ module "orch-project" { "compute.googleapis.com", "container.googleapis.com", "containerregistry.googleapis.com", + "artifactregistry.googleapis.com", "dataflow.googleapis.com", "orgpolicy.googleapis.com", "pubsub.googleapis.com", @@ -150,3 +164,46 @@ module "orch-nat" { region = var.region router_network = module.orch-vpc.0.name } + +module "orch-artifact-reg" { + source = "../../../modules/artifact-registry" + project_id = module.orch-project.project_id + id = "${var.prefix}-app-images" + location = var.region + format = "DOCKER" + description = "Docker repository storing application images e.g. Dataflow, Cloud Run etc..." +} + +module "orch-cs-df-template" { + source = "../../../modules/gcs" + project_id = module.orch-project.project_id + prefix = var.prefix + name = "orc-cs-df-template" + location = var.region + storage_class = "REGIONAL" + encryption_key = try(local.service_encryption_keys.storage, null) +} + +module "orch-cs-build-staging" { + source = "../../../modules/gcs" + project_id = module.orch-project.project_id + prefix = var.prefix + name = "orc-cs-build-staging" + location = var.region + storage_class = "REGIONAL" + encryption_key = try(local.service_encryption_keys.storage, null) +} + +module "orch-sa-df-build" { + source = "../../../modules/iam-service-account" + project_id = module.orch-project.project_id + prefix = var.prefix + name = "orc-sa-df-build" + display_name = "Data platform Dataflow build service account" + # Note values below should pertain to the system / group / users who are able to + # invoke the build via this service account + iam = { + "roles/iam.serviceAccountTokenCreator" = [local.groups_iam.data-engineers] + "roles/iam.serviceAccountUser" = [local.groups_iam.data-engineers] + } +} diff --git a/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf b/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf index 879a0e0b1..0db5ce440 100644 --- a/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf +++ b/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf @@ -30,21 +30,6 @@ locals { "roles/storage.objectViewer", ] } - dwh_plg_group_iam = { - (local.groups.data-engineers) = [ - "roles/bigquery.dataEditor", - "roles/storage.admin", - ], - (local.groups.data-analysts) = [ - "roles/bigquery.dataEditor", - "roles/bigquery.jobUser", - "roles/bigquery.metadataViewer", - "roles/bigquery.user", - "roles/datacatalog.viewer", - "roles/datacatalog.tagTemplateViewer", - "roles/storage.objectAdmin", - ] - } dwh_lnd_iam = { "roles/bigquery.dataOwner" = [ module.load-sa-df-0.iam_email, @@ -140,21 +125,6 @@ module "dwh-conf-project" { } } -module "dwh-plg-project" { - source = "../../../modules/project" - parent = var.folder_id - billing_account = var.billing_account_id - prefix = var.prefix - name = "dwh-plg${local.project_suffix}" - group_iam = local.dwh_plg_group_iam - iam = {} - services = local.dwh_services - service_encryption_key_ids = { - bq = [try(local.service_encryption_keys.bq, null)] - storage = [try(local.service_encryption_keys.storage, null)] - } -} - # Bigquery module "dwh-lnd-bq-0" { @@ -181,14 +151,6 @@ module "dwh-conf-bq-0" { encryption_key = try(local.service_encryption_keys.bq, null) } -module "dwh-plg-bq-0" { - source = "../../../modules/bigquery-dataset" - project_id = module.dwh-plg-project.project_id - id = "${replace(var.prefix, "-", "_")}_dwh_plg_bq_0" - location = var.location - encryption_key = try(local.service_encryption_keys.bq, null) -} - # Cloud storage module "dwh-lnd-cs-0" { @@ -223,14 +185,3 @@ module "dwh-conf-cs-0" { encryption_key = try(local.service_encryption_keys.storage, null) force_destroy = var.data_force_destroy } - -module "dwh-plg-cs-0" { - source = "../../../modules/gcs" - project_id = module.dwh-plg-project.project_id - prefix = var.prefix - name = "dwh-plg-cs-0" - location = var.location - storage_class = "MULTI_REGIONAL" - encryption_key = try(local.service_encryption_keys.storage, null) - force_destroy = var.data_force_destroy -} diff --git a/blueprints/data-solutions/data-platform-foundations/IAM.md b/blueprints/data-solutions/data-platform-foundations/IAM.md index 54d35939b..dd898bd75 100644 --- a/blueprints/data-solutions/data-platform-foundations/IAM.md +++ b/blueprints/data-solutions/data-platform-foundations/IAM.md @@ -57,14 +57,6 @@ Legend: + additive, conditional. |trf-bq-0
serviceAccount|[roles/bigquery.dataOwner](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataOwner)
[roles/datacatalog.categoryAdmin](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.categoryAdmin) | |trf-df-0
serviceAccount|[roles/bigquery.dataOwner](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataOwner) | -## Project dwh-plg - -| members | roles | -|---|---| -|gcp-data-analysts
group|[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/bigquery.metadataViewer](https://cloud.google.com/iam/docs/understanding-roles#bigquery.metadataViewer)
[roles/bigquery.user](https://cloud.google.com/iam/docs/understanding-roles#bigquery.user)
[roles/datacatalog.tagTemplateViewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.tagTemplateViewer)
[roles/datacatalog.viewer](https://cloud.google.com/iam/docs/understanding-roles#datacatalog.viewer)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | -|gcp-data-engineers
group|[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor)
[roles/storage.admin](https://cloud.google.com/iam/docs/understanding-roles#storage.admin) | -|SERVICE_IDENTITY_service-networking
serviceAccount|[roles/servicenetworking.serviceAgent](https://cloud.google.com/iam/docs/understanding-roles#servicenetworking.serviceAgent) +| - ## Project lod | members | roles | @@ -79,11 +71,13 @@ Legend: + additive, conditional. | members | roles | |---|---| -|gcp-data-engineers
group|[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/cloudbuild.builds.editor](https://cloud.google.com/iam/docs/understanding-roles#cloudbuild.builds.editor)
[roles/composer.admin](https://cloud.google.com/iam/docs/understanding-roles#composer.admin)
[roles/composer.environmentAndStorageObjectAdmin](https://cloud.google.com/iam/docs/understanding-roles#composer.environmentAndStorageObjectAdmin)
[roles/iam.serviceAccountUser](https://cloud.google.com/iam/docs/understanding-roles#iam.serviceAccountUser)
[roles/iap.httpsResourceAccessor](https://cloud.google.com/iam/docs/understanding-roles#iap.httpsResourceAccessor)
[roles/storage.admin](https://cloud.google.com/iam/docs/understanding-roles#storage.admin)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | -|SERVICE_IDENTITY_cloudcomposer-accounts
serviceAccount|[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | +|gcp-data-engineers
group|[roles/artifactregistry.admin](https://cloud.google.com/iam/docs/understanding-roles#artifactregistry.admin)
[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/cloudbuild.builds.editor](https://cloud.google.com/iam/docs/understanding-roles#cloudbuild.builds.editor)
[roles/composer.admin](https://cloud.google.com/iam/docs/understanding-roles#composer.admin)
[roles/composer.environmentAndStorageObjectAdmin](https://cloud.google.com/iam/docs/understanding-roles#composer.environmentAndStorageObjectAdmin)
[roles/iam.serviceAccountUser](https://cloud.google.com/iam/docs/understanding-roles#iam.serviceAccountUser)
[roles/iap.httpsResourceAccessor](https://cloud.google.com/iam/docs/understanding-roles#iap.httpsResourceAccessor)
[roles/serviceusage.serviceUsageConsumer](https://cloud.google.com/iam/docs/understanding-roles#serviceusage.serviceUsageConsumer)
[roles/storage.admin](https://cloud.google.com/iam/docs/understanding-roles#storage.admin)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | +|SERVICE_IDENTITY_cloudcomposer-accounts
serviceAccount|[roles/composer.ServiceAgentV2Ext](https://cloud.google.com/iam/docs/understanding-roles#composer.ServiceAgentV2Ext)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | +|SERVICE_IDENTITY_gcp-sa-cloudbuild
serviceAccount|[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | |SERVICE_IDENTITY_service-networking
serviceAccount|[roles/servicenetworking.serviceAgent](https://cloud.google.com/iam/docs/understanding-roles#servicenetworking.serviceAgent) +| -|load-df-0
serviceAccount|[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor)
[roles/storage.objectViewer](https://cloud.google.com/iam/docs/understanding-roles#storage.objectViewer) | +|load-df-0
serviceAccount|[roles/artifactregistry.reader](https://cloud.google.com/iam/docs/understanding-roles#artifactregistry.reader)
[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor)
[roles/storage.objectViewer](https://cloud.google.com/iam/docs/understanding-roles#storage.objectViewer) | |orc-cmp-0
serviceAccount|[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/composer.worker](https://cloud.google.com/iam/docs/understanding-roles#composer.worker)
[roles/iam.serviceAccountUser](https://cloud.google.com/iam/docs/understanding-roles#iam.serviceAccountUser)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | +|orc-sa-df-build
serviceAccount|[roles/cloudbuild.serviceAgent](https://cloud.google.com/iam/docs/understanding-roles#cloudbuild.serviceAgent)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | |trf-df-0
serviceAccount|[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor) | ## Project trf diff --git a/blueprints/data-solutions/data-platform-foundations/README.md b/blueprints/data-solutions/data-platform-foundations/README.md index 8da143b29..08b24b211 100644 --- a/blueprints/data-solutions/data-platform-foundations/README.md +++ b/blueprints/data-solutions/data-platform-foundations/README.md @@ -21,7 +21,7 @@ The approach adapts to different high-level requirements: - least privilege principle - rely on service account impersonation -The code in this blueprint doesn't address Organization-level configurations (Organization policy, VPC-SC, centralized logs). We expect those elements to be managed by automation stages external to this script like those in [FAST](../../../fast). +The code in this blueprint doesn't address Organization-level configurations (Organization policy, VPC-SC, centralized logs). We expect those elements to be managed by automation stages external to this script like those in [FAST](../../../fast) and this blueprint deployed on top of them as one of the [stages](../../../fast/stages/3-data-platform/dev/README.md). ### Project structure @@ -39,14 +39,13 @@ This separation into projects allows adhering to the least-privilege principle b The script will create the following projects: - **Drop off** Used to store temporary data. Data is pushed to Cloud Storage, BigQuery, or Cloud PubSub. Resources are configured with a customizable lifecycle policy. -- **Load** Used to load data from the drop off zone to the data warehouse. The load is made with minimal to zero transformation logic (mainly `cast`). Anonymization or tokenization of Personally Identifiable Information (PII) can be implemented here or in the transformation stage, depending on your requirements. The use of [Cloud Dataflow templates](https://cloud.google.com/dataflow/docs/concepts/dataflow-templates) is recommended. +- **Load** Used to load data from the drop off zone to the data warehouse. The load is made with minimal to zero transformation logic (mainly `cast`). Anonymization or tokenization of Personally Identifiable Information (PII) can be implemented here or in the transformation stage, depending on your requirements. The use of [Cloud Dataflow templates](https://cloud.google.com/dataflow/docs/concepts/dataflow-templates) is recommended. When you need to handle workloads from different teams, if strong role separation is needed between them, we suggest to customize the scirpt and have separate `Load` projects. - **Data Warehouse** Several projects distributed across 3 separate layers, to host progressively processed and refined data: - **Landing - Raw data** Structured Data, stored in relevant formats: structured data stored in BigQuery, unstructured data stored on Cloud Storage with additional metadata stored in BigQuery (for example pictures stored in Cloud Storage and analysis of the images for Cloud Vision API stored in BigQuery). - **Curated - Cleansed, aggregated and curated data** - **Confidential - Curated and unencrypted layer** - - **Playground** Temporary tables that Data Analyst may use to perform R&D on data available in other Data Warehouse layers. - **Orchestration** Used to host Cloud Composer, which orchestrates all tasks that move data across layers. -- **Transformation** Used to move data between Data Warehouse layers. We strongly suggest relying on BigQuery Engine to perform the transformations. If BigQuery doesn't have the features needed to perform your transformations, you can use Cloud Dataflow with [Cloud Dataflow templates](https://cloud.google.com/dataflow/docs/concepts/dataflow-templates). This stage can also optionally anonymize or tokenize PII. +- **Transformation** Used to move data between Data Warehouse layers. We strongly suggest relying on BigQuery Engine to perform the transformations. If BigQuery doesn't have the features needed to perform your transformations, you can use Cloud Dataflow with [Cloud Dataflow templates](https://cloud.google.com/dataflow/docs/concepts/dataflow-templates). This stage can also optionally anonymize or tokenize PII. When you need to handle workloads from different teams, if strong role separation is needed between them, we suggest to customize the scirpt and have separate `Tranformation` projects. - **Exposure** Used to host resources that share processed data with external systems. Depending on the access pattern, data can be presented via Cloud SQL, BigQuery, or Bigtable. For BigQuery data, we strongly suggest relying on [Authorized views](https://cloud.google.com/bigquery/docs/authorized-views). ### Roles @@ -80,10 +79,10 @@ We use three groups to control access to resources: The table below shows a high level overview of roles for each group on each project, using `READ`, `WRITE` and `ADMIN` access patterns for simplicity. For detailed roles please refer to the code. -|Group|Drop off|Load|Transformation|DHW Landing|DWH Curated|DWH Confidential|DWH Playground|Orchestration|Common| -|-|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:| -|Data Engineers|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`| -|Data Analysts|-|-|-|-|-|`READ`|`READ`/`WRITE`|-|-| +|Group|Drop off|Load|Transformation|DHW Landing|DWH Curated|DWH Confidential|Orchestration|Common| +|-|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:| +|Data Engineers|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`| +|Data Analysts|-|-|-|-|-|`READ`|-|-| |Data Security|-|-|-|-|-|-|-|-|`ADMIN`| You can configure groups via the `groups` variable. @@ -109,14 +108,13 @@ In both VPC scenarios, you also need these ranges for Composer: - one /24 for Cloud SQL - one /28 for the GKE control plane -- one /28 for the web server ### Resource naming conventions Resources follow the naming convention described below. - `prefix-layer` for projects -- `prefix-layer-prduct` for resources +- `prefix-layer-product` for resources - `prefix-layer[2]-gcp-product[2]-counter` for services and service accounts ### Encryption @@ -221,7 +219,7 @@ module "data-platform" { prefix = "myprefix" } -# tftest modules=42 resources=316 +# tftest modules=43 resources=297 ``` ## Customizations @@ -247,31 +245,32 @@ You can find examples in the `[demo](./demo)` folder. | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [billing_account_id](variables.tf#L17) | Billing account id. | string | ✓ | | -| [folder_id](variables.tf#L53) | Folder to be used for the networking resources in folders/nnnn format. | string | ✓ | | -| [organization_domain](variables.tf#L98) | Organization domain. | string | ✓ | | -| [prefix](variables.tf#L103) | Prefix used for resource names. | string | ✓ | | -| [composer_config](variables.tf#L22) | Cloud Composer config. | object({…}) | | {…} | -| [data_catalog_tags](variables.tf#L36) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(map(list(string))) | | {…} | -| [data_force_destroy](variables.tf#L47) | Flag to set 'force_destroy' on data services like BiguQery or Cloud Storage. | bool | | false | -| [groups](variables.tf#L58) | User groups. | map(string) | | {…} | -| [location](variables.tf#L68) | Location used for multi-regional resources. | string | | "eu" | -| [network_config](variables.tf#L74) | Shared VPC network configurations to use. If null networks will be created in projects with preconfigured values. | object({…}) | | null | -| [project_services](variables.tf#L112) | List of core services enabled on all projects. | list(string) | | […] | -| [project_suffix](variables.tf#L123) | Suffix used only for project ids. | string | | null | -| [region](variables.tf#L129) | Region used for regional resources. | string | | "europe-west1" | -| [service_encryption_keys](variables.tf#L135) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | null | +| [folder_id](variables.tf#L122) | Folder to be used for the networking resources in folders/nnnn format. | string | ✓ | | +| [organization_domain](variables.tf#L166) | Organization domain. | string | ✓ | | +| [prefix](variables.tf#L171) | Prefix used for resource names. | string | ✓ | | +| [composer_config](variables.tf#L22) | Cloud Composer config. | object({…}) | | {…} | +| [data_catalog_tags](variables.tf#L105) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(map(list(string))) | | {…} | +| [data_force_destroy](variables.tf#L116) | Flag to set 'force_destroy' on data services like BiguQery or Cloud Storage. | bool | | false | +| [groups](variables.tf#L127) | User groups. | map(string) | | {…} | +| [location](variables.tf#L137) | Location used for multi-regional resources. | string | | "eu" | +| [network_config](variables.tf#L143) | Shared VPC network configurations to use. If null networks will be created in projects with preconfigured values. | object({…}) | | null | +| [project_services](variables.tf#L180) | List of core services enabled on all projects. | list(string) | | […] | +| [project_suffix](variables.tf#L191) | Suffix used only for project ids. | string | | null | +| [region](variables.tf#L197) | Region used for regional resources. | string | | "europe-west1" | +| [service_encryption_keys](variables.tf#L203) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | null | ## Outputs | name | description | sensitive | |---|---|:---:| -| [bigquery-datasets](outputs.tf#L17) | BigQuery datasets. | | -| [demo_commands](outputs.tf#L28) | Demo commands. | | -| [gcs-buckets](outputs.tf#L41) | GCS buckets. | | -| [kms_keys](outputs.tf#L55) | Cloud MKS keys. | | -| [projects](outputs.tf#L60) | GCP Projects informations. | | -| [vpc_network](outputs.tf#L88) | VPC network. | | -| [vpc_subnet](outputs.tf#L97) | VPC subnetworks. | | +| [bigquery-datasets](outputs.tf#L16) | BigQuery datasets. | | +| [demo_commands](outputs.tf#L26) | Demo commands. Relevant only if Composer is deployed. | | +| [df_template](outputs.tf#L49) | Dataflow template image and template details. | | +| [gcs-buckets](outputs.tf#L58) | GCS buckets. | | +| [kms_keys](outputs.tf#L71) | Cloud MKS keys. | | +| [projects](outputs.tf#L76) | GCP Projects informations. | | +| [vpc_network](outputs.tf#L102) | VPC network. | | +| [vpc_subnet](outputs.tf#L111) | VPC subnetworks. | | ## TODOs diff --git a/blueprints/data-solutions/data-platform-foundations/demo/README.md b/blueprints/data-solutions/data-platform-foundations/demo/README.md index 97add086a..639549fca 100644 --- a/blueprints/data-solutions/data-platform-foundations/demo/README.md +++ b/blueprints/data-solutions/data-platform-foundations/demo/README.md @@ -23,10 +23,11 @@ Below you can find a description of each example: ## Running the demo To run demo examples, please follow the following steps: -- 01: copy sample data to the `drop off` Cloud Storage bucket impersonating the `load` service account. -- 02: copy sample data structure definition in the `orchestration` Cloud Storage bucket impersonating the `orchestration` service account. -- 03: copy the Cloud Composer DAG to the Cloud Composer Storage bucket impersonating the `orchestration` service account. -- 04: Open the Cloud Composer Airflow UI and run the imported DAG. -- 05: Run the BigQuery query to see results. +- 01: Copy sample data to the `drop off` Cloud Storage bucket impersonating the `load` service account. +- 02: Copy sample data structure definition in the `orchestration` Cloud Storage bucket impersonating the `orchestration` service account. +- 03: Copy the Cloud Composer DAG to the Cloud Composer Storage bucket impersonating the `orchestration` service account. +- 04: Build the Dataflow Flex template and image via a Cloud Build pipeline +- 05: Open the Cloud Composer Airflow UI and run the imported DAG. +- 06: Run the BigQuery query to see results. You can find pre-computed commands in the `demo_commands` output variable of the deployed terraform [data pipeline](../). diff --git a/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/.gitignore b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/.gitignore new file mode 100644 index 000000000..68bc17f9f --- /dev/null +++ b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/Dockerfile b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/Dockerfile new file mode 100644 index 000000000..69c6d2eef --- /dev/null +++ b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/Dockerfile @@ -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 +# +# https://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. + +FROM gcr.io/dataflow-templates-base/python39-template-launcher-base + +ENV FLEX_TEMPLATE_PYTHON_REQUIREMENTS_FILE="/template/requirements.txt" +ENV FLEX_TEMPLATE_PYTHON_PY_FILE="/template/csv2bq.py" + +COPY ./src/ /template + +RUN apt-get update \ + && apt-get install -y libffi-dev git \ + && rm -rf /var/lib/apt/lists/* \ + && pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r $FLEX_TEMPLATE_PYTHON_REQUIREMENTS_FILE \ + && pip download --no-cache-dir --dest /tmp/dataflow-requirements-cache -r $FLEX_TEMPLATE_PYTHON_REQUIREMENTS_FILE + +ENV PIP_NO_DEPS=True diff --git a/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md new file mode 100644 index 000000000..b052fab05 --- /dev/null +++ b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md @@ -0,0 +1,122 @@ +## Pipeline summary +This demo serves as a simple example of building and launching a Flex Template Dataflow pipeline. The code mainly focuses on reading a CSV file as input along with a JSON schema file as side input. The pipeline Parses both inputs and writes the data to the relevant BigQuery table while applying the schema passed from input. + +![Dataflow pipeline overview](../../images/df_demo_pipeline.png "Dataflow pipeline overview") + + +## Local development run + +For local development, the pipeline can be launched from the local machine for testing purposes using different runners depending on the scope of the test. + +### Using the Beam DirectRunner +The below example uses the Beam DirectRunner. The use case for this runner is mainly for quick local tests on the development environment with low volume of data. + +``` +CSV_FILE=gs://[TEST-BUCKET]/customers.csv +JSON_SCHEMA=gs://[TEST-BUCKET]/customers_schema.json +OUTPUT_TABLE=[TEST-PROJ].[TEST-DATASET].customers +PIPELINE_STAGIN_PATH="gs://[TEST-STAGING-BUCKET]" + +python src/csv2bq.py \ +--runner="DirectRunner" \ +--csv_file=$CSV_FILE \ +--json_schema=$JSON_SCHEMA \ +--output_table=$OUTPUT_TABLE \ +--temp_location=$PIPELINE_STAGIN_PATH/tmp +``` + +*Note:* All paths mentioned can be local paths or on GCS. For cloud resources referenced (GCS and BigQuery), make sure that the user launching the command is authenticated to GCP via `gcloud auth application-default login` and has the required access privileges to those resources. + +### Using the DataflowRunner with a local CLI launch + +The below example triggers the pipeline on Dataflow from your local development environment. The use case for this is for running local tests on larger volumes of test data and verifying that the pipeline runs well on Dataflow, before compiling it into a template. + +``` +PROJECT_ID=[TEST-PROJECT] +REGION=[REGION] +SUBNET=[SUBNET-NAME] +DEV_SERVICE_ACCOUNT=[DEV-SA] + +PIPELINE_STAGIN_PATH="gs://[TEST-STAGING-BUCKET]" +CSV_FILE=gs://[TEST-BUCKET]/customers.csv +JSON_SCHEMA=gs://[TEST-BUCKET]/customers_schema.json +OUTPUT_TABLE=[TEST-PROJ].[TEST-DATASET].customers + +python src/csv2bq.py \ +--runner="Dataflow" \ +--project=$PROJECT_ID \ +--region=$REGION \ +--csv_file=$CSV_FILE \ +--json_schema=$JSON_SCHEMA \ +--output_table=$OUTPUT_TABLE \ +--temp_location=$PIPELINE_STAGIN_PATH/tmp +--staging_location=$PIPELINE_STAGIN_PATH/stage \ +--subnetwork="regions/$REGION/subnetworks/$SUBNET" \ +--impersonate_service_account=$DEV_SERVICE_ACCOUNT \ +--no_use_public_ips +``` + +In terms of resource access privilege, you can choose to impersonate another service account, which could be defined for development resource access. The authenticated user launching this pipeline will need to have the role `roles/iam.serviceAccountTokenCreator`. If you choose to launch the pipeline without service account impersonation, it will use the default compute service account assigned of the target project. + +## Dataflow Flex Template run + +For production, and as outline in the Data Platform demo, we build and launch the pipeline as a Flex Template, making it available for other cloud services(such as Apache Airflow) and users to trigger launch instances of it on demand. + +### Build launch + +Below is an example for triggering the Dataflow flex template build pipeline defined in `cloudbuild.yaml`. The Terraform output provides an example as well filled with the parameters values based on the generated resources in the data platform. + +``` +GCP_PROJECT="[ORCHESTRATION-PROJECT]" +TEMPLATE_IMAGE="[REGION].pkg.dev/[ORCHESTRATION-PROJECT]/[REPOSITORY]/csv2bq:latest" +TEMPLATE_PATH="gs://[DATAFLOW-TEMPLATE-BUCKEt]/csv2bq.json" +STAGIN_PATH="gs://[ORCHESTRATION-STAGING-BUCKET]/build" +LOG_PATH="gs://[ORCHESTRATION-LOGS-BUCKET]/logs" +REGION="[REGION]" +BUILD_SERVICE_ACCOUNT=orc-sa-df-build@[SERVICE_PROJECT_ID].iam.gserviceaccount.com + +gcloud builds submit \ + --config=cloudbuild.yaml \ + --project=$GCP_PROJECT \ + --region=$REGION \ + --gcs-log-dir=$LOG_PATH \ + --gcs-source-staging-dir=$STAGIN_PATH \ + --substitutions=_TEMPLATE_IMAGE=$TEMPLATE_IMAGE,_TEMPLATE_PATH=$TEMPLATE_PATH,_DOCKER_DIR="." \ + --impersonate-service-account=$BUILD_SERVICE_ACCOUNT +``` + +**Note:** For the scope of the demo, the launch of this build is manual, but in production, this build would be launched via a configured cloud build trigger when new changes are merged into the code branch of the Dataflow template. + +### Dataflow Flex Template run + +After the build step succeeds. You can launch dataflow pipeline from CLI (outline in this example) or the API via Airflow's operator. For the use case of the data platform, the Dataflow pipeline would be launched via the orchestration service account, which is what the Airflow DAG is also using in the scope of this demo. + +**Note:** In the data platform demo, the launch of this Dataflow pipeline is handled by the airflow operator (DataflowStartFlexTemplateOperator). + +``` +#!/bin/bash + +PROJECT_ID=[LOAD-PROJECT] +REGION=[REGION] +ORCH_SERVICE_ACCOUNT=orchestrator@[SERVICE_PROJECT_ID].iam.gserviceaccount.com +SUBNET=[SUBNET-NAME] + +PIPELINE_STAGIN_PATH="gs://[LOAD-STAGING-BUCKET]/build" +CSV_FILE=gs://[DROP-ZONE-BUCKET]/customers.csv +JSON_SCHEMA=gs://[ORCHESTRATION-BUCKET]/customers_schema.json +OUTPUT_TABLE=[DESTINATION-PROJ].[DESTINATION-DATASET].customers +TEMPLATE_PATH=gs://[ORCHESTRATION-DF-GCS]/csv2bq.json + + +gcloud dataflow flex-template run "csv2bq-`date +%Y%m%d-%H%M%S`" \ + --template-file-gcs-location $TEMPLATE_PATH \ + --parameters temp_location="$PIPELINE_STAGIN_PATH/tmp" \ + --parameters staging_location="$PIPELINE_STAGIN_PATH/stage" \ + --parameters csv_file=$CSV_FILE \ + --parameters json_schema=$JSON_SCHEMA\ + --parameters output_table=$OUTPUT_TABLE \ + --region $REGION \ + --project $PROJECT_ID \ + --subnetwork="regions/$REGION/subnetworks/$SUBNET" \ + --service-account-email=$ORCH_SERVICE_ACCOUNT +``` diff --git a/blueprints/cloud-operations/network-dashboard/versions.tf b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/cloudbuild.yaml similarity index 54% rename from blueprints/cloud-operations/network-dashboard/versions.tf rename to blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/cloudbuild.yaml index 3bdf23370..11354c2ed 100644 --- a/blueprints/cloud-operations/network-dashboard/versions.tf +++ b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/cloudbuild.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,16 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -terraform { - required_version = ">= 1.3.1" - required_providers { - google = { - source = "hashicorp/google" - version = ">= 4.40.0" # tftest - } - google-beta = { - source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest - } - } -} +steps: +- name: gcr.io/cloud-builders/gcloud + id: "Build docker image" + args: ['builds', 'submit', '--tag', '$_TEMPLATE_IMAGE', '.'] + dir: '$_DOCKER_DIR' + waitFor: ['-'] +- name: gcr.io/cloud-builders/gcloud + id: "Build template" + args: ['dataflow', + 'flex-template', + 'build', + '$_TEMPLATE_PATH', + '--image=$_TEMPLATE_IMAGE', + '--sdk-language=PYTHON' + ] + waitFor: ['Build docker image'] diff --git a/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/src/csv2bq.py b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/src/csv2bq.py new file mode 100644 index 000000000..0f8ad1275 --- /dev/null +++ b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/src/csv2bq.py @@ -0,0 +1,79 @@ +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import apache_beam as beam +from apache_beam.io import ReadFromText, Read, WriteToBigQuery, BigQueryDisposition +from apache_beam.options.pipeline_options import PipelineOptions, SetupOptions +from apache_beam.io.filesystems import FileSystems +import json +import argparse + + +class ParseRow(beam.DoFn): + """ + Splits a given csv row by a seperator, validates fields and returns a dict + structure compatible with the BigQuery transform + """ + + def process(self, element: str, table_fields: list, delimiter: str): + split_row = element.split(delimiter) + parsed_row = {} + + for i, field in enumerate(table_fields['BigQuery Schema']): + parsed_row[field['name']] = split_row[i] + + yield parsed_row + +def run(argv=None, save_main_session=True): + parser = argparse.ArgumentParser() + parser.add_argument('--csv_file', + type=str, + required=True, + help='Path to the CSV file') + parser.add_argument('--json_schema', + type=str, + required=True, + help='Path to the JSON schema') + parser.add_argument('--output_table', + type=str, + required=True, + help='BigQuery path for the output table') + + args, pipeline_args = parser.parse_known_args(argv) + pipeline_options = PipelineOptions(pipeline_args) + pipeline_options.view_as( + SetupOptions).save_main_session = save_main_session + + with beam.Pipeline(options=pipeline_options) as p: + + def get_table_schema(table_path, table_schema): + return {'fields': table_schema['BigQuery Schema']} + + csv_input = p | 'Read CSV' >> ReadFromText(args.csv_file) + schema_input = p | 'Load Schema' >> beam.Create( + json.loads(FileSystems.open(args.json_schema).read())) + + table_fields = beam.pvalue.AsDict(schema_input) + parsed = csv_input | 'Parse and validate rows' >> beam.ParDo( + ParseRow(), table_fields, ',') + + parsed | 'Write to BigQuery' >> WriteToBigQuery( + args.output_table, + schema=get_table_schema, + create_disposition=BigQueryDisposition.CREATE_IF_NEEDED, + write_disposition=BigQueryDisposition.WRITE_TRUNCATE, + schema_side_inputs=(table_fields, )) + +if __name__ == "__main__": + run() diff --git a/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/src/requirements.txt b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/src/requirements.txt new file mode 100644 index 000000000..21c569a0d --- /dev/null +++ b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/src/requirements.txt @@ -0,0 +1 @@ +apache-beam==2.44.0 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 new file mode 100644 index 000000000..f911e335e --- /dev/null +++ b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags_flex.py @@ -0,0 +1,461 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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. + +# -------------------------------------------------------------------------------- +# Load The Dependencies +# -------------------------------------------------------------------------------- + +import datetime +import json +import os +import time + +from airflow import models +from airflow.operators import dummy +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 + +# -------------------------------------------------------------------------------- +# 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" + +# -------------------------------------------------------------------------------- +# Set default arguments +# -------------------------------------------------------------------------------- + +# If you are running Airflow in more than one time zone +# see https://airflow.apache.org/docs/apache-airflow/stable/timezone.html +# for best practices +yesterday = datetime.datetime.now() - datetime.timedelta(days=1) + +default_args = { + 'owner': 'airflow', + 'start_date': yesterday, + 'depends_on_past': False, + 'email': [''], + 'email_on_failure': False, + 'email_on_retry': False, + 'retries': 1, + 'retry_delay': datetime.timedelta(minutes=5), +} + +dataflow_environment = { + 'serviceAccountEmail': LOD_SA_DF, + 'workerZone': DF_ZONE, + 'stagingLocation': f'{LOD_GCS_STAGING}/staging', + 'tempLocation': f'{LOD_GCS_STAGING}/tmp', + 'subnetwork': LOD_NET_SUBNET, + 'kmsKeyName': DF_KMS_KEY, + 'ipConfiguration': 'WORKER_IP_PRIVATE' +} + +# -------------------------------------------------------------------------------- +# Main DAG +# -------------------------------------------------------------------------------- + +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') + + end = dummy.DummyOperator(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. + with TaskGroup('upsert_table') as upsert_table: + upsert_table_customers = BigQueryUpsertTableOperator( + task_id="upsert_table_customers", + project_id=DWH_LAND_PRJ, + dataset_id=DWH_LAND_BQ_DATASET, + impersonation_chain=[TRF_SA_DF], + table_resource={ + "tableReference": { + "tableId": "customers" + }, + }, + ) + + upsert_table_purchases = BigQueryUpsertTableOperator( + task_id="upsert_table_purchases", + project_id=DWH_LAND_PRJ, + dataset_id=DWH_LAND_BQ_DATASET, + impersonation_chain=[TRF_SA_BQ], + table_resource={"tableReference": { + "tableId": "purchases" + }}, + ) + + upsert_table_customer_purchase_curated = BigQueryUpsertTableOperator( + task_id="upsert_table_customer_purchase_curated", + project_id=DWH_CURATED_PRJ, + dataset_id=DWH_CURATED_BQ_DATASET, + impersonation_chain=[TRF_SA_BQ], + table_resource={ + "tableReference": { + "tableId": "customer_purchase" + } + }, + ) + + upsert_table_customer_purchase_confidential = BigQueryUpsertTableOperator( + task_id="upsert_table_customer_purchase_confidential", + project_id=DWH_CONFIDENTIAL_PRJ, + dataset_id=DWH_CONFIDENTIAL_BQ_DATASET, + impersonation_chain=[TRF_SA_BQ], + table_resource={ + "tableReference": { + "tableId": "customer_purchase" + } + }, + ) + + # Bigquery Tables schema defined here for demo porpuse. + # Consider a dedicated pipeline or tool for a real life scenario. + with TaskGroup('update_schema_table') as update_schema_table: + update_table_schema_customers = BigQueryUpdateTableSchemaOperator( + task_id="update_table_schema_customers", + project_id=DWH_LAND_PRJ, + dataset_id=DWH_LAND_BQ_DATASET, + table_id="customers", + impersonation_chain=[TRF_SA_BQ], + include_policy_tags=True, + schema_fields_updates=[{ + "mode": "REQUIRED", + "name": "id", + "type": "INTEGER", + "description": "ID" + }, { + "mode": "REQUIRED", + "name": "name", + "type": "STRING", + "description": "Name", + "policyTags": { + "names": [DATA_CAT_TAGS.get('2_Private', None)] + } + }, { + "mode": "REQUIRED", + "name": "surname", + "type": "STRING", + "description": "Surname", + "policyTags": { + "names": [DATA_CAT_TAGS.get('2_Private', None)] + } + }, { + "mode": "REQUIRED", + "name": "timestamp", + "type": "TIMESTAMP", + "description": "Timestamp" + }]) + + update_table_schema_purchases = BigQueryUpdateTableSchemaOperator( + task_id="update_table_schema_purchases", + project_id=DWH_LAND_PRJ, + dataset_id=DWH_LAND_BQ_DATASET, + table_id="purchases", + impersonation_chain=[TRF_SA_BQ], + include_policy_tags=True, + schema_fields_updates=[{ + "mode": "REQUIRED", + "name": "id", + "type": "INTEGER", + "description": "ID" + }, { + "mode": "REQUIRED", + "name": "customer_id", + "type": "INTEGER", + "description": "ID" + }, { + "mode": "REQUIRED", + "name": "item", + "type": "STRING", + "description": "Item Name" + }, { + "mode": "REQUIRED", + "name": "price", + "type": "FLOAT", + "description": "Item Price" + }, { + "mode": "REQUIRED", + "name": "timestamp", + "type": "TIMESTAMP", + "description": "Timestamp" + }]) + + update_table_schema_customer_purchase_curated = BigQueryUpdateTableSchemaOperator( + task_id="update_table_schema_customer_purchase_curated", + project_id=DWH_CURATED_PRJ, + dataset_id=DWH_CURATED_BQ_DATASET, + table_id="customer_purchase", + impersonation_chain=[TRF_SA_BQ], + include_policy_tags=True, + schema_fields_updates=[{ + "mode": "REQUIRED", + "name": "customer_id", + "type": "INTEGER", + "description": "ID" + }, { + "mode": "REQUIRED", + "name": "purchase_id", + "type": "INTEGER", + "description": "ID" + }, { + "mode": "REQUIRED", + "name": "name", + "type": "STRING", + "description": "Name", + "policyTags": { + "names": [DATA_CAT_TAGS.get('2_Private', None)] + } + }, { + "mode": "REQUIRED", + "name": "surname", + "type": "STRING", + "description": "Surname", + "policyTags": { + "names": [DATA_CAT_TAGS.get('2_Private', None)] + } + }, { + "mode": "REQUIRED", + "name": "item", + "type": "STRING", + "description": "Item Name" + }, { + "mode": "REQUIRED", + "name": "price", + "type": "FLOAT", + "description": "Item Price" + }, { + "mode": "REQUIRED", + "name": "timestamp", + "type": "TIMESTAMP", + "description": "Timestamp" + }]) + + update_table_schema_customer_purchase_confidential = BigQueryUpdateTableSchemaOperator( + task_id="update_table_schema_customer_purchase_confidential", + project_id=DWH_CONFIDENTIAL_PRJ, + dataset_id=DWH_CONFIDENTIAL_BQ_DATASET, + table_id="customer_purchase", + impersonation_chain=[TRF_SA_BQ], + include_policy_tags=True, + schema_fields_updates=[{ + "mode": "REQUIRED", + "name": "customer_id", + "type": "INTEGER", + "description": "ID" + }, { + "mode": "REQUIRED", + "name": "purchase_id", + "type": "INTEGER", + "description": "ID" + }, { + "mode": "REQUIRED", + "name": "name", + "type": "STRING", + "description": "Name", + "policyTags": { + "names": [DATA_CAT_TAGS.get('2_Private', None)] + } + }, { + "mode": "REQUIRED", + "name": "surname", + "type": "STRING", + "description": "Surname", + "policyTags": { + "names": [DATA_CAT_TAGS.get('2_Private', None)] + } + }, { + "mode": "REQUIRED", + "name": "item", + "type": "STRING", + "description": "Item Name" + }, { + "mode": "REQUIRED", + "name": "price", + "type": "FLOAT", + "description": "Item Price" + }, { + "mode": "REQUIRED", + "name": "timestamp", + "type": "TIMESTAMP", + "description": "Timestamp" + }]) + + customers_import = DataflowStartFlexTemplateOperator( + task_id='dataflow_customers_import', + project_id=LOD_PRJ, + location=DF_REGION, + body={ + 'launchParameter': { + 'jobName': f'dataflow-customers-import-{round(time.time())}', + 'containerSpecGcsPath': f'{ORC_GCS_TMP_DF}/csv2bq.json', + 'environment': { + 'serviceAccountEmail': LOD_SA_DF, + 'workerZone': DF_ZONE, + 'stagingLocation': f'{LOD_GCS_STAGING}/staging', + 'tempLocation': f'{LOD_GCS_STAGING}/tmp', + 'subnetwork': LOD_NET_SUBNET, + 'kmsKeyName': DF_KMS_KEY, + 'ipConfiguration': 'WORKER_IP_PRIVATE' + }, + 'parameters': { + 'csv_file': + f'{DRP_GCS}/customers.csv', + 'json_schema': + f'{ORC_GCS}/customers_schema.json', + 'output_table': + f'{DWH_LAND_PRJ}:{DWH_LAND_BQ_DATASET}.customers', + } + } + }) + + purchases_import = DataflowStartFlexTemplateOperator( + task_id='dataflow_purchases_import', + project_id=LOD_PRJ, + location=DF_REGION, + body={ + 'launchParameter': { + 'jobName': f'dataflow-purchases-import-{round(time.time())}', + 'containerSpecGcsPath': f'{ORC_GCS_TMP_DF}/csv2bq.json', + 'environment': { + 'serviceAccountEmail': LOD_SA_DF, + 'workerZone': DF_ZONE, + 'stagingLocation': f'{LOD_GCS_STAGING}/staging', + 'tempLocation': f'{LOD_GCS_STAGING}/tmp', + 'subnetwork': LOD_NET_SUBNET, + 'kmsKeyName': DF_KMS_KEY, + 'ipConfiguration': 'WORKER_IP_PRIVATE' + }, + 'parameters': { + 'csv_file': + f'{DRP_GCS}/purchases.csv', + 'json_schema': + f'{ORC_GCS}/purchases_schema.json', + 'output_table': + f'{DWH_LAND_PRJ}:{DWH_LAND_BQ_DATASET}.purchases', + } + } + }) + + join_customer_purchase = BigQueryInsertJobOperator( + task_id='bq_join_customer_purchase', + gcp_conn_id='bigquery_default', + project_id=TRF_PRJ, + location=BQ_LOCATION, + configuration={ + 'jobType': 'QUERY', + 'query': { + 'query': + """SELECT + c.id as customer_id, + p.id as purchase_id, + c.name as name, + c.surname as surname, + p.item as item, + p.price as price, + p.timestamp as timestamp + FROM `{dwh_0_prj}.{dwh_0_dataset}.customers` c + JOIN `{dwh_0_prj}.{dwh_0_dataset}.purchases` p ON c.id = p.customer_id + """.format( + dwh_0_prj=DWH_LAND_PRJ, + dwh_0_dataset=DWH_LAND_BQ_DATASET, + ), + 'destinationTable': { + 'projectId': DWH_CURATED_PRJ, + 'datasetId': DWH_CURATED_BQ_DATASET, + 'tableId': 'customer_purchase' + }, + 'writeDisposition': + 'WRITE_TRUNCATE', + "useLegacySql": + False + } + }, + impersonation_chain=[TRF_SA_BQ]) + + confidential_customer_purchase = BigQueryInsertJobOperator( + task_id='bq_confidential_customer_purchase', + gcp_conn_id='bigquery_default', + project_id=TRF_PRJ, + location=BQ_LOCATION, + configuration={ + 'jobType': 'QUERY', + 'query': { + 'query': + """SELECT + customer_id, + purchase_id, + name, + surname, + item, + price, + timestamp + FROM `{dwh_cur_prj}.{dwh_cur_dataset}.customer_purchase` + """.format( + dwh_cur_prj=DWH_CURATED_PRJ, + dwh_cur_dataset=DWH_CURATED_BQ_DATASET, + ), + 'destinationTable': { + 'projectId': DWH_CONFIDENTIAL_PRJ, + 'datasetId': DWH_CONFIDENTIAL_BQ_DATASET, + 'tableId': 'customer_purchase' + }, + 'writeDisposition': + 'WRITE_TRUNCATE', + "useLegacySql": + False + } + }, + impersonation_chain=[TRF_SA_BQ]) + +start >> upsert_table >> update_schema_table >> [ + customers_import, purchases_import +] >> join_customer_purchase >> confidential_customer_purchase >> end diff --git a/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_flex.py b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_flex.py new file mode 100644 index 000000000..34ff10ccd --- /dev/null +++ b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_flex.py @@ -0,0 +1,225 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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. + +# -------------------------------------------------------------------------------- +# Load The Dependencies +# -------------------------------------------------------------------------------- + +import datetime +import json +import os +import time + +from airflow import models +from airflow.providers.google.cloud.operators.dataflow import DataflowStartFlexTemplateOperator +from airflow.operators import dummy +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" + +# -------------------------------------------------------------------------------- +# Set default arguments +# -------------------------------------------------------------------------------- + +# If you are running Airflow in more than one time zone +# see https://airflow.apache.org/docs/apache-airflow/stable/timezone.html +# for best practices +yesterday = datetime.datetime.now() - datetime.timedelta(days=1) + +default_args = { + 'owner': 'airflow', + 'start_date': yesterday, + 'depends_on_past': False, + 'email': [''], + 'email_on_failure': False, + 'email_on_retry': False, + 'retries': 1, + 'retry_delay': datetime.timedelta(minutes=5), +} + +dataflow_environment = { + 'serviceAccountEmail': LOD_SA_DF, + 'workerZone': DF_ZONE, + 'stagingLocation': f'{LOD_GCS_STAGING}/staging', + 'tempLocation': f'{LOD_GCS_STAGING}/tmp', + 'subnetwork': LOD_NET_SUBNET, + 'kmsKeyName': DF_KMS_KEY, + 'ipConfiguration': 'WORKER_IP_PRIVATE' +} + +# -------------------------------------------------------------------------------- +# Main DAG +# -------------------------------------------------------------------------------- + +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') + + end = dummy.DummyOperator(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. + customers_import = DataflowStartFlexTemplateOperator( + task_id='dataflow_customers_import', + project_id=LOD_PRJ, + location=DF_REGION, + body={ + 'launchParameter': { + 'jobName': f'dataflow-customers-import-{round(time.time())}', + 'containerSpecGcsPath': f'{ORC_GCS_TMP_DF}/csv2bq.json', + 'environment': dataflow_environment, + 'parameters': { + 'csv_file': + f'{DRP_GCS}/customers.csv', + 'json_schema': + f'{ORC_GCS}/customers_schema.json', + 'output_table': + f'{DWH_LAND_PRJ}:{DWH_LAND_BQ_DATASET}.customers', + } + } + }) + + purchases_import = DataflowStartFlexTemplateOperator( + task_id='dataflow_purchases_import', + project_id=LOD_PRJ, + location=DF_REGION, + body={ + 'launchParameter': { + 'jobName': f'dataflow-purchases-import-{round(time.time())}', + 'containerSpecGcsPath': f'{ORC_GCS_TMP_DF}/csv2bq.json', + 'environment': dataflow_environment, + 'parameters': { + 'csv_file': + f'{DRP_GCS}/purchases.csv', + 'json_schema': + f'{ORC_GCS}/purchases_schema.json', + 'output_table': + f'{DWH_LAND_PRJ}:{DWH_LAND_BQ_DATASET}.purchases', + } + } + }) + + join_customer_purchase = BigQueryInsertJobOperator( + task_id='bq_join_customer_purchase', + gcp_conn_id='bigquery_default', + project_id=TRF_PRJ, + location=BQ_LOCATION, + configuration={ + 'jobType': 'QUERY', + 'query': { + 'query': + """SELECT + c.id as customer_id, + p.id as purchase_id, + p.item as item, + p.price as price, + p.timestamp as timestamp + FROM `{dwh_0_prj}.{dwh_0_dataset}.customers` c + JOIN `{dwh_0_prj}.{dwh_0_dataset}.purchases` p ON c.id = p.customer_id + """.format( + dwh_0_prj=DWH_LAND_PRJ, + dwh_0_dataset=DWH_LAND_BQ_DATASET, + ), + 'destinationTable': { + 'projectId': DWH_CURATED_PRJ, + 'datasetId': DWH_CURATED_BQ_DATASET, + 'tableId': 'customer_purchase' + }, + 'writeDisposition': + 'WRITE_TRUNCATE', + "useLegacySql": + False + } + }, + impersonation_chain=[TRF_SA_BQ]) + + confidential_customer_purchase = BigQueryInsertJobOperator( + task_id='bq_confidential_customer_purchase', + gcp_conn_id='bigquery_default', + project_id=TRF_PRJ, + location=BQ_LOCATION, + configuration={ + 'jobType': 'QUERY', + 'query': { + 'query': + """SELECT + c.id as customer_id, + p.id as purchase_id, + c.name as name, + c.surname as surname, + p.item as item, + p.price as price, + p.timestamp as timestamp + FROM `{dwh_0_prj}.{dwh_0_dataset}.customers` c + JOIN `{dwh_0_prj}.{dwh_0_dataset}.purchases` p ON c.id = p.customer_id + """.format( + dwh_0_prj=DWH_LAND_PRJ, + dwh_0_dataset=DWH_LAND_BQ_DATASET, + ), + 'destinationTable': { + 'projectId': DWH_CONFIDENTIAL_PRJ, + 'datasetId': DWH_CONFIDENTIAL_BQ_DATASET, + 'tableId': 'customer_purchase' + }, + 'writeDisposition': + 'WRITE_TRUNCATE', + "useLegacySql": + False + } + }, + impersonation_chain=[TRF_SA_BQ]) + + start >> [ + customers_import, purchases_import + ] >> join_customer_purchase >> confidential_customer_purchase >> end diff --git a/blueprints/data-solutions/data-platform-foundations/images/df_demo_pipeline.png b/blueprints/data-solutions/data-platform-foundations/images/df_demo_pipeline.png new file mode 100644 index 000000000..541532b41 Binary files /dev/null and b/blueprints/data-solutions/data-platform-foundations/images/df_demo_pipeline.png differ diff --git a/blueprints/data-solutions/data-platform-foundations/images/overview_diagram.png b/blueprints/data-solutions/data-platform-foundations/images/overview_diagram.png index 642c81c2f..073ec870c 100644 Binary files a/blueprints/data-solutions/data-platform-foundations/images/overview_diagram.png and b/blueprints/data-solutions/data-platform-foundations/images/overview_diagram.png differ diff --git a/blueprints/data-solutions/data-platform-foundations/outputs.tf b/blueprints/data-solutions/data-platform-foundations/outputs.tf index b941776cb..ae853da00 100644 --- a/blueprints/data-solutions/data-platform-foundations/outputs.tf +++ b/blueprints/data-solutions/data-platform-foundations/outputs.tf @@ -13,7 +13,6 @@ # limitations under the License. # tfdoc:file:description Output variables. - output "bigquery-datasets" { description = "BigQuery datasets." value = { @@ -21,30 +20,47 @@ output "bigquery-datasets" { dwh-landing-bq-0 = module.dwh-lnd-bq-0.dataset_id, dwh-curated-bq-0 = module.dwh-cur-bq-0.dataset_id, dwh-confidential-bq-0 = module.dwh-conf-bq-0.dataset_id, - dwh-plg-bq-0 = module.dwh-plg-bq-0.dataset_id, } } output "demo_commands" { - description = "Demo commands." + description = "Demo commands. Relevant only if Composer is deployed." value = { 01 = "gsutil -i ${module.drop-sa-cs-0.email} cp demo/data/*.csv gs://${module.drop-cs-0.name}" - 02 = "gsutil -i ${module.orch-sa-cmp-0.email} cp demo/data/*.j* gs://${module.orch-cs-0.name}" - 03 = "gsutil -i ${module.orch-sa-cmp-0.email} cp demo/*.py ${google_composer_environment.orch-cmp-0.config[0].dag_gcs_prefix}/" - 04 = "Open ${google_composer_environment.orch-cmp-0.config.0.airflow_uri} and run uploaded DAG." - 05 = <string | ✓ | | -| [project_id](variables.tf#L40) | Project id, references existing project if `project_create` is null. | string | ✓ | | +| [prefix](variables.tf#L32) | Prefix used for resource names. | string | ✓ | | +| [project_id](variables.tf#L50) | Project id, references existing project if `project_create` is null. | string | ✓ | | | [location](variables.tf#L16) | The location where resources will be deployed. | string | | "EU" | -| [project_create](variables.tf#L31) | Provide values if project creation is needed, uses existing project if null. Parent format: folders/folder_id or organizations/org_id. | object({…}) | | null | -| [region](variables.tf#L45) | The region where resources will be deployed. | string | | "europe-west1" | -| [vpc_config](variables.tf#L61) | Parameters to create a VPC. | object({…}) | | {…} | +| [network_config](variables.tf#L22) | Shared VPC network configurations to use. If null networks will be created in projects with preconfigured values. | object({…}) | | null | +| [project_create](variables.tf#L41) | Provide values if project creation is needed, uses existing project if null. Parent format: folders/folder_id or organizations/org_id. | object({…}) | | null | +| [region](variables.tf#L55) | The region where resources will be deployed. | string | | "europe-west1" | ## Outputs diff --git a/blueprints/data-solutions/data-playground/main.tf b/blueprints/data-solutions/data-playground/main.tf index ff079d5eb..548bee37d 100644 --- a/blueprints/data-solutions/data-playground/main.tf +++ b/blueprints/data-solutions/data-playground/main.tf @@ -17,6 +17,39 @@ ############################################################################### locals { service_encryption_keys = var.service_encryption_keys + shared_vpc_project = try(var.network_config.host_project, null) + + subnet = ( + local.use_shared_vpc + ? var.network_config.subnet_self_link + : values(module.vpc.0.subnet_self_links)[0] + ) + vpc = ( + local.use_shared_vpc + ? var.network_config.network_self_link + : module.vpc.0.self_link + ) + use_shared_vpc = var.network_config != null + + shared_vpc_bindings = { + "roles/compute.networkUser" = [ + "robot-df", "notebooks" + ] + } + + shared_vpc_role_members = { + robot-df = "serviceAccount:${module.project.service_accounts.robots.dataflow}" + notebooks = "serviceAccount:${module.project.service_accounts.robots.notebooks}" + } + + # reassemble in a format suitable for for_each + shared_vpc_bindings_map = { + for binding in flatten([ + for role, members in local.shared_vpc_bindings : [ + for member in members : { role = role, member = member } + ] + ]) : "${binding.role}-${binding.member}" => binding + } } module "project" { @@ -27,6 +60,7 @@ module "project" { project_create = var.project_create != null prefix = var.project_create == null ? null : var.prefix services = [ + "aiplatform.googleapis.com", "bigquery.googleapis.com", "bigquerystorage.googleapis.com", "bigqueryreservation.googleapis.com", @@ -42,17 +76,26 @@ module "project" { "storage.googleapis.com", "storage-component.googleapis.com" ] + + shared_vpc_service_config = local.shared_vpc_project == null ? null : { + attach = true + host_project = local.shared_vpc_project + } + org_policies = { # "constraints/compute.requireOsLogin" = { # enforce = false # } - # Example of applying a project wide policy, mainly useful for Composer + # Example of applying a project wide policy, mainly useful for Composer 1 } service_encryption_key_ids = { compute = [try(local.service_encryption_keys.compute, null)] bq = [try(local.service_encryption_keys.bq, null)] storage = [try(local.service_encryption_keys.storage, null)] } + service_config = { + disable_on_destroy = false, disable_dependent_services = false + } } ############################################################################### @@ -61,11 +104,12 @@ module "project" { module "vpc" { source = "../../../modules/net-vpc" + count = local.use_shared_vpc ? 0 : 1 project_id = module.project.project_id name = "${var.prefix}-vpc" subnets = [ { - ip_cidr_range = var.vpc_config.ip_cidr_range + ip_cidr_range = "10.0.0.0/20" name = "${var.prefix}-subnet" region = var.region } @@ -74,10 +118,11 @@ module "vpc" { module "vpc-firewall" { source = "../../../modules/net-vpc-firewall" + count = local.use_shared_vpc ? 0 : 1 project_id = module.project.project_id - network = module.vpc.name + network = module.vpc.0.name default_rules_config = { - admin_ranges = [var.vpc_config.ip_cidr_range] + admin_ranges = ["10.0.0.0/20"] } ingress_rules = { #TODO Remove and rely on 'ssh' tag once terraform-provider-google/issues/9273 is fixed @@ -92,12 +137,21 @@ module "vpc-firewall" { module "cloudnat" { source = "../../../modules/net-cloudnat" + count = local.use_shared_vpc ? 0 : 1 project_id = module.project.project_id name = "${var.prefix}-default" region = var.region - router_network = module.vpc.name + router_network = module.vpc.0.name } +resource "google_project_iam_member" "shared_vpc" { + count = local.use_shared_vpc ? 1 : 0 + project = var.network_config.host_project + role = "roles/compute.networkUser" + member = "serviceAccount:${module.project.service_accounts.robots.notebooks}" +} + + ############################################################################### # Storage # ############################################################################### @@ -121,8 +175,6 @@ module "dataset" { ############################################################################### # Vertex AI Notebook # ############################################################################### -# TODO: Add encryption_key to Vertex AI notebooks as well -# TODO: Add shared VPC support module "service-account-notebook" { source = "../../../modules/iam-service-account" @@ -160,11 +212,19 @@ resource "google_notebooks_instance" "playground" { no_public_ip = true no_proxy_access = false - network = module.vpc.network.id - subnet = module.vpc.subnets[format("%s/%s", var.region, "${var.prefix}-subnet")].id + network = local.vpc + subnet = local.subnet service_account = module.service-account-notebook.email + # Remove once terraform-provider-google/issues/9164 is fixed + lifecycle { + ignore_changes = [disk_encryption, kms_key] + } + #TODO Uncomment once terraform-provider-google/issues/9273 is fixed # tags = ["ssh"] + depends_on = [ + google_project_iam_member.shared_vpc, + ] } diff --git a/blueprints/data-solutions/data-playground/outputs.tf b/blueprints/data-solutions/data-playground/outputs.tf index 4b80c311c..35f2efeb1 100644 --- a/blueprints/data-solutions/data-playground/outputs.tf +++ b/blueprints/data-solutions/data-playground/outputs.tf @@ -37,5 +37,5 @@ output "project" { output "vpc" { description = "VPC Network." - value = module.vpc.name + value = local.vpc } diff --git a/blueprints/data-solutions/data-playground/variables.tf b/blueprints/data-solutions/data-playground/variables.tf index 173540673..3bd0ca65b 100644 --- a/blueprints/data-solutions/data-playground/variables.tf +++ b/blueprints/data-solutions/data-playground/variables.tf @@ -19,6 +19,16 @@ variable "location" { default = "EU" } +variable "network_config" { + description = "Shared VPC network configurations to use. If null networks will be created in projects with preconfigured values." + type = object({ + host_project = string + network_self_link = string + subnet_self_link = string + }) + default = null +} + variable "prefix" { description = "Prefix used for resource names." type = string @@ -57,13 +67,3 @@ variable "service_encryption_keys" { # service encription key }) default = null } - -variable "vpc_config" { - description = "Parameters to create a VPC." - type = object({ - ip_cidr_range = string - }) - default = { - ip_cidr_range = "10.0.0.0/20" - } -} diff --git a/blueprints/data-solutions/data-playground/versions.tf b/blueprints/data-solutions/data-playground/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/data-solutions/data-playground/versions.tf +++ b/blueprints/data-solutions/data-playground/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } 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 286536a65..08492c6f9 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 @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/data-solutions/shielded-folder/README.md b/blueprints/data-solutions/shielded-folder/README.md new file mode 100644 index 000000000..aaf67e6ac --- /dev/null +++ b/blueprints/data-solutions/shielded-folder/README.md @@ -0,0 +1,180 @@ +# Shielded folder + +This blueprint implements an opinionated folder configuration according to GCP best practices. Configurations set at the folder level would be beneficial to host workloads inheriting constraints from the folder they belong to. + +In this blueprint, a folder will be created setting following features: + +- Organizational policies +- Hierarchical firewall rules +- Cloud KMS +- VPC-SC + +Within the folder, the following projects will be created: + +- 'audit-logs' where Audit Logs sink will be created +- 'sec-core' where Cloud KMS and Cloud Secret manager will be configured + +The following diagram is a high-level reference of the resources created and managed here: + +![Shielded architecture overview](./images/overview_diagram.png "Shielded architecture overview") + +## Design overview and choices + +Despite its simplicity, this blueprint implements the basics of a design that we've seen working well for various customers. + +The approach adapts to different high-level requirements: + +- IAM roles inheritance +- Organizational policies +- Audit log sink +- VPC Service Control +- Cloud KMS + +## Project structure + +The Shielded Folder blueprint is designed to rely on several projects: + +- `audit-log`: to host Audit logging buckets and Audit log sync to GCS, BigQuery or PubSub +- `sec-core`: to host security-related resources such as Cloud KMS and Cloud Secrets Manager + +This separation into projects allows adhering to the least-privilege principle by using project-level roles. + +## User groups + +User groups provide a stable frame of reference that allows decoupling the final set of permissions from the stage where entities and resources are created, and their IAM bindings are defined. + +We use groups to control access to resources: + +- `data-engineers`: They handle and run workloads on the `workload` subfolder. They have editor access to all resources in the `workload` folder in order to troubleshoot possible issues within the workload. This team can also impersonate any service account in the workload folder. +- `data-security`: They handle security configurations for the shielded folder. They have owner access to the `audit-log` and `sec-core` projects. + +## Encryption + +The blueprint supports the configuration of an instance of Cloud KMS to handle encryption on the resources. The encryption is disabled by default, but you can enable it by configuring the `enable_features.encryption` variable. + +The script will create keys to encrypt log sink buckets/datasets/topics in the specified regions. Configuring the `kms_keys` variable, you can create additional KMS keys needed by your workload. + +## Customizations + +### Organization policy + +You can configure the Organization policies enforced on the folder editing yaml files in the [org-policies](./data/org-policies/) folder. An opinionated list of policies that we suggest enforcing is listed. + +Some additional Organization policy constraints you may want to evaluate adding: + +- `constraints/gcp.resourceLocations`: to define the locations where location-based GCP resources can be created. +- `constraints/gcp.restrictCmekCryptoKeyProjects`: to define which projects may be used to supply Customer-Managed Encryption Keys (CMEK) when creating resources. + +### VPC Service Control + +VPC Service Control is configured to have a Perimeter containing all projects within the folder. Additional projects you may add to the folder won't be automatically added to the perimeter, and a new `terraform apply` is needed to add the project to the perimeter. + +The VPC SC configuration is set to dry-run mode, but switching to enforced mode is a simple operation involving modifying a few lines of code highlighted by ad-hoc comments. + +Access level rules are not defined. Before moving the configuration to enforced mode, configure access policies to continue accessing resources from outside of the perimeter. + +An access level based on the network range you are using to reach the console (e.g. Proxy IP, Internet connection, ...) is suggested. Example: + +```tfvars +vpc_sc_access_levels = { + users = { + conditions = [ + { members = ["user:user1@example.com"] } + ] + } +} +``` + +Alternatively, you can configure an access level based on the identity that needs to reach resources from outside the perimeter. + +```tfvars +vpc_sc_access_levels = { + users = { + conditions = [ + { ip_subnetworks = ["101.101.101.0/24"] } + ] + } +} +``` + +## How to run this script + +To deploy this blueprint in your GCP organization, you will need + +- a folder or organization where resources will be created +- a billing account that will be associated with the new projects + +The Shielded Folder blueprint is meant to be executed by a Service Account (or a regular user) having this minimal set of permission: + +- Billing account + - `roles/billing.user` +- Folder level + - `roles/resourcemanager.folderAdmin` + - `roles/resourcemanager.projectCreator` + +The shielded Folfer blueprint assumes [groups described](#user-groups) are created in your GCP organization. + +### Variable configuration PIPPO + +There are several sets of variables you will need to fill in: + +```tfvars +access_policy_config = { + access_policy_create = { + parent = "organizations/1234567890123" + title = "ShieldedMVP" + } +} +folder_config = { + folder_create = { + display_name = "ShieldedMVP" + parent = "organizations/1234567890123" + } +} +organization = { + domain = "example.com" + id = "1122334455" +} +prefix = "prefix" +project_config = { + billing_account_id = "123456-123456-123456" +} +``` + +### Deploying the blueprint + +Once the configuration is complete, run the project factory by running + +```bash +terraform init +terraform apply +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [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#L128) | Organization details. | object({…}) | ✓ | | +| [prefix](variables.tf#L136) | Prefix used for resources that need unique names. | string | ✓ | | +| [project_config](variables.tf#L141) | 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#L86) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | +| [log_sinks](variables.tf#L103) | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | +| [vpc_sc_access_levels](variables.tf#L161) | VPC SC access level definitions. | map(object({…})) | | {} | +| [vpc_sc_egress_policies](variables.tf#L190) | VPC SC egress policy defnitions. | map(object({…})) | | {} | +| [vpc_sc_ingress_policies](variables.tf#L210) | VPC SC ingress policy defnitions. | map(object({…})) | | {} | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [folders](outputs.tf#L15) | Folders id. | | +| [folders_sink_writer_identities](outputs.tf#L23) | Folders id. | | + + diff --git a/blueprints/data-solutions/shielded-folder/data/firewall-policies/cidrs.yaml b/blueprints/data-solutions/shielded-folder/data/firewall-policies/cidrs.yaml new file mode 100644 index 000000000..90dabfb6a --- /dev/null +++ b/blueprints/data-solutions/shielded-folder/data/firewall-policies/cidrs.yaml @@ -0,0 +1,15 @@ +# skip boilerplate check + +healthchecks: + - 35.191.0.0/16 + - 130.211.0.0/22 + - 209.85.152.0/22 + - 209.85.204.0/22 + +rfc1918: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + +onprem_probes: + - 10.255.255.254/32 \ No newline at end of file diff --git a/blueprints/data-solutions/shielded-folder/data/firewall-policies/hierarchical-policy-rules.yaml b/blueprints/data-solutions/shielded-folder/data/firewall-policies/hierarchical-policy-rules.yaml new file mode 100644 index 000000000..6a3b31335 --- /dev/null +++ b/blueprints/data-solutions/shielded-folder/data/firewall-policies/hierarchical-policy-rules.yaml @@ -0,0 +1,50 @@ +# skip boilerplate check + +allow-admins: + description: Access from the admin subnet to all subnets + direction: INGRESS + action: allow + priority: 1000 + ranges: + - $rfc1918 + ports: + all: [] + target_resources: null + enable_logging: false + +allow-healthchecks: + description: Enable HTTP and HTTPS healthchecks + direction: INGRESS + action: allow + priority: 1001 + ranges: + - $healthchecks + ports: + tcp: ["80", "443"] + target_resources: null + enable_logging: false + +allow-ssh-from-iap: + description: Enable SSH from IAP + direction: INGRESS + action: allow + priority: 1002 + ranges: + - 35.235.240.0/20 + ports: + tcp: ["22"] + target_resources: null + enable_logging: false + +allow-icmp: + description: Enable ICMP + direction: INGRESS + action: allow + priority: 1003 + ranges: + - 0.0.0.0/0 + ports: + icmp: [] + target_resources: null + enable_logging: false + \ No newline at end of file diff --git a/fast/stages/01-resman/data/org-policies/compute.yaml b/blueprints/data-solutions/shielded-folder/data/org-policies/compute.yaml similarity index 100% rename from fast/stages/01-resman/data/org-policies/compute.yaml rename to blueprints/data-solutions/shielded-folder/data/org-policies/compute.yaml diff --git a/fast/stages/01-resman/data/org-policies/iam.yaml b/blueprints/data-solutions/shielded-folder/data/org-policies/iam.yaml similarity index 100% rename from fast/stages/01-resman/data/org-policies/iam.yaml rename to blueprints/data-solutions/shielded-folder/data/org-policies/iam.yaml diff --git a/fast/stages/01-resman/data/org-policies/serverless.yaml b/blueprints/data-solutions/shielded-folder/data/org-policies/serverless.yaml similarity index 100% rename from fast/stages/01-resman/data/org-policies/serverless.yaml rename to blueprints/data-solutions/shielded-folder/data/org-policies/serverless.yaml diff --git a/fast/stages/01-resman/data/org-policies/sql.yaml b/blueprints/data-solutions/shielded-folder/data/org-policies/sql.yaml similarity index 100% rename from fast/stages/01-resman/data/org-policies/sql.yaml rename to blueprints/data-solutions/shielded-folder/data/org-policies/sql.yaml diff --git a/fast/stages/01-resman/data/org-policies/storage.yaml b/blueprints/data-solutions/shielded-folder/data/org-policies/storage.yaml similarity index 100% rename from fast/stages/01-resman/data/org-policies/storage.yaml rename to blueprints/data-solutions/shielded-folder/data/org-policies/storage.yaml diff --git a/blueprints/data-solutions/shielded-folder/data/vpc-sc/accessible-services.yaml b/blueprints/data-solutions/shielded-folder/data/vpc-sc/accessible-services.yaml new file mode 100644 index 000000000..2107d2ff1 --- /dev/null +++ b/blueprints/data-solutions/shielded-folder/data/vpc-sc/accessible-services.yaml @@ -0,0 +1,119 @@ +# skip boilerplate check + +- accessapproval.googleapis.com +- adsdatahub.googleapis.com +- aiplatform.googleapis.com +- alloydb.googleapis.com +- alpha-documentai.googleapis.com +- analyticshub.googleapis.com +- apigee.googleapis.com +- apigeeconnect.googleapis.com +- artifactregistry.googleapis.com +- assuredworkloads.googleapis.com +- automl.googleapis.com +- baremetalsolution.googleapis.com +- batch.googleapis.com +- beyondcorp.googleapis.com +- bigquery.googleapis.com +- bigquerydatapolicy.googleapis.com +- bigquerydatatransfer.googleapis.com +- bigquerymigration.googleapis.com +- bigqueryreservation.googleapis.com +- bigtable.googleapis.com +- binaryauthorization.googleapis.com +- cloudasset.googleapis.com +- cloudbuild.googleapis.com +- clouddebugger.googleapis.com +- clouderrorreporting.googleapis.com +- cloudfunctions.googleapis.com +- cloudkms.googleapis.com +- cloudprofiler.googleapis.com +- cloudresourcemanager.googleapis.com +- cloudsearch.googleapis.com +- cloudtrace.googleapis.com +- composer.googleapis.com +- compute.googleapis.com +- connectgateway.googleapis.com +- contactcenterinsights.googleapis.com +- container.googleapis.com +- containeranalysis.googleapis.com +- containerfilesystem.googleapis.com +- containerregistry.googleapis.com +- containerthreatdetection.googleapis.com +- contentwarehouse.googleapis.com +- datacatalog.googleapis.com +- dataflow.googleapis.com +- datafusion.googleapis.com +- datalineage.googleapis.com +- datamigration.googleapis.com +- datapipelines.googleapis.com +- dataplex.googleapis.com +- dataproc.googleapis.com +- datastream.googleapis.com +- dialogflow.googleapis.com +- dlp.googleapis.com +- dns.googleapis.com +- documentai.googleapis.com +- domains.googleapis.com +- essentialcontacts.googleapis.com +- eventarc.googleapis.com +- file.googleapis.com +- firebaseappcheck.googleapis.com +- firebaserules.googleapis.com +- firestore.googleapis.com +- gameservices.googleapis.com +- gkebackup.googleapis.com +- gkeconnect.googleapis.com +- gkehub.googleapis.com +- gkemulticloud.googleapis.com +- healthcare.googleapis.com +- iam.googleapis.com +- iamcredentials.googleapis.com +- iaptunnel.googleapis.com +- ids.googleapis.com +- integrations.googleapis.com +- language.googleapis.com +- lifesciences.googleapis.com +- logging.googleapis.com +- managedidentities.googleapis.com +- memcache.googleapis.com +- meshca.googleapis.com +- metastore.googleapis.com +- ml.googleapis.com +- monitoring.googleapis.com +- networkconnectivity.googleapis.com +- networkmanagement.googleapis.com +- networksecurity.googleapis.com +- networkservices.googleapis.com +- notebooks.googleapis.com +- opsconfigmonitoring.googleapis.com +- osconfig.googleapis.com +- oslogin.googleapis.com +- policytroubleshooter.googleapis.com +- privateca.googleapis.com +- pubsub.googleapis.com +- pubsublite.googleapis.com +- recaptchaenterprise.googleapis.com +- recommender.googleapis.com +- redis.googleapis.com +- retail.googleapis.com +- run.googleapis.com +- secretmanager.googleapis.com +- servicecontrol.googleapis.com +- servicedirectory.googleapis.com +- spanner.googleapis.com +- speakerid.googleapis.com +- speech.googleapis.com +- sqladmin.googleapis.com +- storage.googleapis.com +- storagetransfer.googleapis.com +- texttospeech.googleapis.com +- tpu.googleapis.com +- trafficdirector.googleapis.com +- transcoder.googleapis.com +- translate.googleapis.com +- videointelligence.googleapis.com +- vision.googleapis.com +- visionai.googleapis.com +- vpcaccess.googleapis.com +- workstations.googleapis.com \ No newline at end of file diff --git a/blueprints/data-solutions/shielded-folder/data/vpc-sc/restricted-services.yaml b/blueprints/data-solutions/shielded-folder/data/vpc-sc/restricted-services.yaml new file mode 100644 index 000000000..2107d2ff1 --- /dev/null +++ b/blueprints/data-solutions/shielded-folder/data/vpc-sc/restricted-services.yaml @@ -0,0 +1,119 @@ +# skip boilerplate check + +- accessapproval.googleapis.com +- adsdatahub.googleapis.com +- aiplatform.googleapis.com +- alloydb.googleapis.com +- alpha-documentai.googleapis.com +- analyticshub.googleapis.com +- apigee.googleapis.com +- apigeeconnect.googleapis.com +- artifactregistry.googleapis.com +- assuredworkloads.googleapis.com +- automl.googleapis.com +- baremetalsolution.googleapis.com +- batch.googleapis.com +- beyondcorp.googleapis.com +- bigquery.googleapis.com +- bigquerydatapolicy.googleapis.com +- bigquerydatatransfer.googleapis.com +- bigquerymigration.googleapis.com +- bigqueryreservation.googleapis.com +- bigtable.googleapis.com +- binaryauthorization.googleapis.com +- cloudasset.googleapis.com +- cloudbuild.googleapis.com +- clouddebugger.googleapis.com +- clouderrorreporting.googleapis.com +- cloudfunctions.googleapis.com +- cloudkms.googleapis.com +- cloudprofiler.googleapis.com +- cloudresourcemanager.googleapis.com +- cloudsearch.googleapis.com +- cloudtrace.googleapis.com +- composer.googleapis.com +- compute.googleapis.com +- connectgateway.googleapis.com +- contactcenterinsights.googleapis.com +- container.googleapis.com +- containeranalysis.googleapis.com +- containerfilesystem.googleapis.com +- containerregistry.googleapis.com +- containerthreatdetection.googleapis.com +- contentwarehouse.googleapis.com +- datacatalog.googleapis.com +- dataflow.googleapis.com +- datafusion.googleapis.com +- datalineage.googleapis.com +- datamigration.googleapis.com +- datapipelines.googleapis.com +- dataplex.googleapis.com +- dataproc.googleapis.com +- datastream.googleapis.com +- dialogflow.googleapis.com +- dlp.googleapis.com +- dns.googleapis.com +- documentai.googleapis.com +- domains.googleapis.com +- essentialcontacts.googleapis.com +- eventarc.googleapis.com +- file.googleapis.com +- firebaseappcheck.googleapis.com +- firebaserules.googleapis.com +- firestore.googleapis.com +- gameservices.googleapis.com +- gkebackup.googleapis.com +- gkeconnect.googleapis.com +- gkehub.googleapis.com +- gkemulticloud.googleapis.com +- healthcare.googleapis.com +- iam.googleapis.com +- iamcredentials.googleapis.com +- iaptunnel.googleapis.com +- ids.googleapis.com +- integrations.googleapis.com +- language.googleapis.com +- lifesciences.googleapis.com +- logging.googleapis.com +- managedidentities.googleapis.com +- memcache.googleapis.com +- meshca.googleapis.com +- metastore.googleapis.com +- ml.googleapis.com +- monitoring.googleapis.com +- networkconnectivity.googleapis.com +- networkmanagement.googleapis.com +- networksecurity.googleapis.com +- networkservices.googleapis.com +- notebooks.googleapis.com +- opsconfigmonitoring.googleapis.com +- osconfig.googleapis.com +- oslogin.googleapis.com +- policytroubleshooter.googleapis.com +- privateca.googleapis.com +- pubsub.googleapis.com +- pubsublite.googleapis.com +- recaptchaenterprise.googleapis.com +- recommender.googleapis.com +- redis.googleapis.com +- retail.googleapis.com +- run.googleapis.com +- secretmanager.googleapis.com +- servicecontrol.googleapis.com +- servicedirectory.googleapis.com +- spanner.googleapis.com +- speakerid.googleapis.com +- speech.googleapis.com +- sqladmin.googleapis.com +- storage.googleapis.com +- storagetransfer.googleapis.com +- texttospeech.googleapis.com +- tpu.googleapis.com +- trafficdirector.googleapis.com +- transcoder.googleapis.com +- translate.googleapis.com +- videointelligence.googleapis.com +- vision.googleapis.com +- visionai.googleapis.com +- vpcaccess.googleapis.com +- workstations.googleapis.com \ No newline at end of file diff --git a/blueprints/data-solutions/shielded-folder/images/overview_diagram.png b/blueprints/data-solutions/shielded-folder/images/overview_diagram.png new file mode 100644 index 000000000..abc8d56e7 Binary files /dev/null and b/blueprints/data-solutions/shielded-folder/images/overview_diagram.png differ diff --git a/blueprints/data-solutions/shielded-folder/kms.tf b/blueprints/data-solutions/shielded-folder/kms.tf new file mode 100644 index 000000000..7a3b42b53 --- /dev/null +++ b/blueprints/data-solutions/shielded-folder/kms.tf @@ -0,0 +1,102 @@ +/** + * 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. + */ + +# tfdoc:file:description Security project, Cloud KMS and Secret Manager resources. + +locals { + kms_locations = distinct(flatten([ + for k, v in var.kms_keys : v.locations + ])) + kms_locations_keys = { + 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([ + for k, v in local.kms_log_sink_keys : compact(v.locations) + ])) + + # Log sink keys + 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" + } + } + kms_log_locations_keys = { + for loc in local.kms_log_locations : loc => { + for k, v in local.kms_log_sink_keys : k => v if contains(v.locations, loc) + } + } +} + +module "sec-project" { + count = var.enable_features.encryption ? 1 : 0 + source = "../../../modules/project" + name = var.project_config.project_ids["sec-core"] + parent = module.folder.id + billing_account = var.project_config.billing_account_id + project_create = var.project_config.billing_account_id != null && var.enable_features.encryption + prefix = var.project_config.billing_account_id == null ? null : var.prefix + group_iam = { + (local.groups.workload-security) = [ + "roles/editor" + ] + } + services = [ + "cloudkms.googleapis.com", + "secretmanager.googleapis.com", + "stackdriver.googleapis.com" + ] +} + +module "sec-kms" { + for_each = var.enable_features.encryption ? toset(local.kms_locations) : toset([]) + source = "../../../modules/kms" + project_id = module.sec-project[0].project_id + keyring = { + location = each.key + name = "${each.key}" + } + # rename to `key_iam` to switch to authoritative bindings + key_iam_additive = { + for k, v in local.kms_locations_keys[each.key] : k => v.iam + } + keys = local.kms_locations_keys[each.key] +} + +module "log-kms" { + for_each = var.enable_features.encryption ? toset(local.kms_log_locations) : toset([]) + source = "../../../modules/kms" + project_id = module.sec-project[0].project_id + keyring = { + location = each.key + name = "${each.key}" + } + keys = local.kms_log_locations_keys[each.key] +} diff --git a/blueprints/data-solutions/shielded-folder/log-export.tf b/blueprints/data-solutions/shielded-folder/log-export.tf new file mode 100644 index 000000000..9eff32d20 --- /dev/null +++ b/blueprints/data-solutions/shielded-folder/log-export.tf @@ -0,0 +1,107 @@ +/** + * 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. + */ + +# tfdoc:file:description Audit log project and sink. + +locals { + gcs_storage_class = ( + length(split("-", var.log_locations.storage)) < 2 + ? "MULTI_REGIONAL" + : "REGIONAL" + ) + log_types = toset([for k, v in var.log_sinks : v.type]) + + _log_keys = var.enable_features.encryption ? { + bq = var.enable_features.log_sink ? ["projects/${module.sec-project.0.project_id}/locations/${var.log_locations.bq}/keyRings/${var.log_locations.bq}/cryptoKeys/bq"] : null + pubsub = var.enable_features.log_sink ? ["projects/${module.sec-project.0.project_id}/locations/${var.log_locations.pubsub}/keyRings/${var.log_locations.pubsub}/cryptoKeys/pubsub"] : null + storage = var.enable_features.log_sink ? ["projects/${module.sec-project.0.project_id}/locations/${var.log_locations.storage}/keyRings/${var.log_locations.storage}/cryptoKeys/storage"] : null + } : {} + + log_keys = { + for service, key in local._log_keys : service => key if key != null + } +} + +module "log-export-project" { + count = var.enable_features.log_sink ? 1 : 0 + source = "../../../modules/project" + name = var.project_config.project_ids["audit-logs"] + parent = module.folder.id + billing_account = var.project_config.billing_account_id + project_create = var.project_config.billing_account_id != null + prefix = var.project_config.billing_account_id == null ? null : var.prefix + group_iam = { + (local.groups.workload-security) = [ + "roles/editor" + ] + } + iam = { + # "roles/owner" = [module.automation-tf-bootstrap-sa.iam_email] + } + services = [ + "bigquery.googleapis.com", + "pubsub.googleapis.com", + "storage.googleapis.com", + "stackdriver.googleapis.com" + ] + service_encryption_key_ids = var.enable_features.encryption ? local.log_keys : {} + + depends_on = [ + module.log-kms + ] +} + +# one log export per type, with conditionals to skip those not needed + +module "log-export-dataset" { + source = "../../../modules/bigquery-dataset" + count = var.enable_features.log_sink && contains(local.log_types, "bigquery") ? 1 : 0 + project_id = module.log-export-project[0].project_id + id = "${var.prefix}_audit_export" + friendly_name = "Audit logs export." + location = replace(var.log_locations.bq, "europe", "EU") + encryption_key = var.enable_features.encryption ? module.log-kms[var.log_locations.bq].keys["bq"].id : false +} + +module "log-export-gcs" { + source = "../../../modules/gcs" + count = var.enable_features.log_sink && contains(local.log_types, "storage") ? 1 : 0 + project_id = module.log-export-project[0].project_id + name = "audit-logs" + prefix = var.prefix + location = replace(var.log_locations.storage, "europe", "EU") + storage_class = local.gcs_storage_class + encryption_key = var.enable_features.encryption ? module.log-kms[var.log_locations.storage].keys["storage"].id : null +} + +module "log-export-logbucket" { + source = "../../../modules/logging-bucket" + for_each = var.enable_features.log_sink ? toset([for k, v in var.log_sinks : k if v.type == "logging"]) : [] + parent_type = "project" + parent = module.log-export-project[0].project_id + id = "audit-logs-${each.key}" + location = var.log_locations.logging + #TODO check if logging bucket support encryption. +} + +module "log-export-pubsub" { + source = "../../../modules/pubsub" + for_each = toset([for k, v in var.log_sinks : k if v.type == "pubsub" && var.enable_features.log_sink]) + project_id = module.log-export-project[0].project_id + name = "audit-logs-${each.key}" + regions = [var.log_locations.pubsub] + kms_key = var.enable_features.encryption ? module.log-kms[var.log_locations.pubsub].keys["pubsub"].id : null +} diff --git a/blueprints/data-solutions/shielded-folder/main.tf b/blueprints/data-solutions/shielded-folder/main.tf new file mode 100644 index 000000000..52fa0db2e --- /dev/null +++ b/blueprints/data-solutions/shielded-folder/main.tf @@ -0,0 +1,141 @@ +# 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 +# +# https://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. + +# tfdoc:file:description Folder resources. + +locals { + # Create Log sink ingress policies + _sink_ingress_policies = var.enable_features.log_sink ? { + log_sink = { + from = { + access_levels = ["*"] + identities = values(module.folder.sink_writer_identities) + } + to = { + resources = ["projects/${module.log-export-project.0.number}"] + operations = [{ service_name = "*" }] + } } + } : null + + _vpc_sc_vpc_accessible_services = var.data_dir != null ? yamldecode( + file("${var.data_dir}/vpc-sc/restricted-services.yaml") + ) : null + _vpc_sc_restricted_services = var.data_dir != null ? yamldecode( + file("${var.data_dir}/vpc-sc/restricted-services.yaml") + ) : null + + access_policy_create = var.access_policy_config.access_policy_create != null ? { + parent = "organizations/${var.organization.id}" + title = "shielded-folder" + scopes = [module.folder.id] + } : null + + groups = { + for k, v in var.groups : k => "${v}@${var.organization.domain}" + } + groups_iam = { + for k, v in local.groups : k => "group:${v}" + } + group_iam = { + (local.groups.workload-engineers) = [ + "roles/editor", + "roles/iam.serviceAccountTokenCreator" + ] + } + + vpc_sc_resources = [ + for k, v in data.google_projects.folder-projects.projects : format("projects/%s", v.number) + ] + + log_sink_destinations = var.enable_features.log_sink ? merge( + # use the same dataset for all sinks with `bigquery` as destination + { for k, v in var.log_sinks : k => module.log-export-dataset.0 if v.type == "bigquery" }, + # use the same gcs bucket for all sinks with `storage` as destination + { for k, v in var.log_sinks : k => module.log-export-gcs.0 if v.type == "storage" }, + # use separate pubsub topics and logging buckets for sinks with + # destination `pubsub` and `logging` + module.log-export-pubsub, + module.log-export-logbucket + ) : null +} + +module "folder" { + source = "../../../modules/folder" + folder_create = var.folder_config.folder_create != null + parent = try(var.folder_config.folder_create.parent, null) + name = try(var.folder_config.folder_create.display_name, null) + id = var.folder_config.folder_create != null ? null : var.folder_config.folder_id + group_iam = local.group_iam + org_policies_data_path = var.data_dir != null ? "${var.data_dir}/org-policies" : null + firewall_policy_factory = var.data_dir != null ? { + cidr_file = "${var.data_dir}/firewall-policies/cidrs.yaml" + policy_name = "${var.prefix}-fw-policy" + rules_file = "${var.data_dir}/firewall-policies/hierarchical-policy-rules.yaml" + } : null + logging_sinks = var.enable_features.log_sink ? { + for name, attrs in var.log_sinks : name => { + bq_partitioned_table = attrs.type == "bigquery" + destination = local.log_sink_destinations[name].id + filter = attrs.filter + type = attrs.type + } + } : null +} + +module "folder-workload" { + source = "../../../modules/folder" + parent = module.folder.id + name = "${var.prefix}-workload" +} + + +#TODO VPCSC: Access levels +data "google_projects" "folder-projects" { + filter = "parent.id:${split("/", module.folder.id)[1]}" + + depends_on = [ + module.sec-project, + module.log-export-project + ] +} + +module "vpc-sc" { + count = var.enable_features.vpc_sc ? 1 : 0 + source = "../../../modules/vpc-sc" + access_policy = try(var.access_policy_config.policy_name, null) + access_policy_create = local.access_policy_create + access_levels = var.vpc_sc_access_levels + egress_policies = var.vpc_sc_egress_policies + ingress_policies = merge(var.vpc_sc_ingress_policies, local._sink_ingress_policies) + service_perimeters_regular = { + shielded = { + # Move `spec` definition to `status` and comment `use_explicit_dry_run_spec` variable to enforce VPC-SC configuration + # Before enforing configuration check logs and create Access Level, Ingress/Egress policy as needed + + status = null + spec = { + access_levels = keys(var.vpc_sc_access_levels) + resources = local.vpc_sc_resources + restricted_services = local._vpc_sc_restricted_services + egress_policies = keys(var.vpc_sc_egress_policies) + ingress_policies = keys(merge(var.vpc_sc_ingress_policies, local._sink_ingress_policies)) + vpc_accessible_services = { + allowed_services = local._vpc_sc_vpc_accessible_services + enable_restriction = true + } + } + use_explicit_dry_run_spec = true + } + } +} diff --git a/blueprints/data-solutions/shielded-folder/outputs.tf b/blueprints/data-solutions/shielded-folder/outputs.tf new file mode 100644 index 000000000..e1107fc61 --- /dev/null +++ b/blueprints/data-solutions/shielded-folder/outputs.tf @@ -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 +# +# https://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 "folders" { + description = "Folders id." + value = { + shielded-folder = module.folder.id + workload-folder = module.folder-workload.id + } +} + +output "folders_sink_writer_identities" { + description = "Folders id." + value = { + shielded-folder = module.folder.sink_writer_identities + workload-folder = module.folder-workload.sink_writer_identities + } +} + diff --git a/blueprints/data-solutions/shielded-folder/variables.tf b/blueprints/data-solutions/shielded-folder/variables.tf new file mode 100644 index 000000000..a9ecbb241 --- /dev/null +++ b/blueprints/data-solutions/shielded-folder/variables.tf @@ -0,0 +1,229 @@ +# 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 +# +# https://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. + +# tfdoc:file:description Variables definition. + +variable "access_policy_config" { + description = "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." + type = object({ + policy_name = optional(string, null) + access_policy_create = optional(object({ + parent = string + title = string + }), null) + }) + nullable = false +} + +variable "data_dir" { + description = "Relative path for the folder storing configuration data." + type = string + default = "data" +} + +variable "enable_features" { + description = "Flag to enable features on the solution." + type = object({ + encryption = optional(bool, false) + log_sink = optional(bool, true) + vpc_sc = optional(bool, true) + }) + default = { + encryption = false + log_sink = true + vpc_sc = true + } +} + +variable "folder_config" { + description = "Provide 'folder_create' values if folder creation is needed, uses existing 'folder_id' otherwise. Parent is in 'folders/nnn' or 'organizations/nnn' format." + type = object({ + folder_id = optional(string, null) + folder_create = optional(object({ + display_name = string + parent = string + }), null) + }) + validation { + condition = var.folder_config.folder_id != null || var.folder_config.folder_create != null + error_message = "At least one attribute should be set." + } + nullable = false +} + +variable "groups" { + description = "User groups." + type = object({ + workload-engineers = optional(string, "gcp-data-engineers") + workload-security = optional(string, "gcp-data-security") + }) + default = {} + nullable = false +} + +variable "kms_keys" { + description = "KMS keys to create, keyed by name." + type = map(object({ + iam = optional(map(list(string)), {}) + labels = optional(map(string), {}) + locations = optional(list(string), ["global", "europe", "europe-west1"]) + rotation_period = optional(string, "7776000s") + })) + default = {} +} + +variable "log_locations" { + description = "Optional locations for GCS, BigQuery, and logging buckets created here." + type = object({ + bq = optional(string, "europe") + storage = optional(string, "europe") + logging = optional(string, "global") + pubsub = optional(string, "global") + }) + default = { + bq = "europe" + storage = "europe" + logging = "global" + pubsub = null + } + nullable = false +} + +variable "log_sinks" { + description = "Org-level log sinks, in name => {type, filter} format." + type = map(object({ + filter = string + type = string + })) + default = { + audit-logs = { + filter = "logName:\"/logs/cloudaudit.googleapis.com%2Factivity\" OR logName:\"/logs/cloudaudit.googleapis.com%2Fsystem_event\"" + type = "bigquery" + } + vpc-sc = { + filter = "protoPayload.metadata.@type=\"type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata\"" + type = "bigquery" + } + } + validation { + condition = alltrue([ + for k, v in var.log_sinks : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } +} + +variable "organization" { + description = "Organization details." + type = object({ + domain = string + id = string + }) +} + +variable "prefix" { + description = "Prefix used for resources that need unique names." + type = string +} + +variable "project_config" { + description = "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." + type = object({ + billing_account_id = optional(string, null) + project_ids = optional(object({ + sec-core = string + audit-logs = string + }), { + sec-core = "sec-core" + audit-logs = "audit-logs" + } + ) + }) + nullable = false + validation { + condition = var.project_config.billing_account_id != null || var.project_config.project_ids != null + error_message = "At least one attribute should be set." + } +} + +variable "vpc_sc_access_levels" { + description = "VPC SC access level definitions." + type = map(object({ + combining_function = optional(string) + conditions = optional(list(object({ + device_policy = optional(object({ + allowed_device_management_levels = optional(list(string)) + allowed_encryption_statuses = optional(list(string)) + require_admin_approval = bool + require_corp_owned = bool + require_screen_lock = optional(bool) + os_constraints = optional(list(object({ + os_type = string + minimum_version = optional(string) + require_verified_chrome_os = optional(bool) + }))) + })) + ip_subnetworks = optional(list(string), []) + members = optional(list(string), []) + negate = optional(bool) + regions = optional(list(string), []) + required_access_levels = optional(list(string), []) + })), []) + description = optional(string) + })) + default = {} + nullable = false +} + +variable "vpc_sc_egress_policies" { + description = "VPC SC egress policy defnitions." + type = map(object({ + from = object({ + identity_type = optional(string, "ANY_IDENTITY") + identities = optional(list(string)) + }) + to = object({ + operations = optional(list(object({ + method_selectors = optional(list(string)) + service_name = string + })), []) + resources = optional(list(string)) + resource_type_external = optional(bool, false) + }) + })) + default = {} + nullable = false +} + +variable "vpc_sc_ingress_policies" { + description = "VPC SC ingress policy defnitions." + type = map(object({ + from = object({ + access_levels = optional(list(string), []) + identity_type = optional(string) + identities = optional(list(string)) + resources = optional(list(string), []) + }) + to = object({ + operations = optional(list(object({ + method_selectors = optional(list(string)) + service_name = string + })), []) + resources = optional(list(string)) + }) + })) + default = {} + nullable = false +} diff --git a/blueprints/data-solutions/sqlserver-alwayson/vpc.tf b/blueprints/data-solutions/sqlserver-alwayson/vpc.tf index ccc10e1c3..0f1e425e1 100644 --- a/blueprints/data-solutions/sqlserver-alwayson/vpc.tf +++ b/blueprints/data-solutions/sqlserver-alwayson/vpc.tf @@ -85,6 +85,7 @@ module "firewall" { ingress_rules = { "${var.prefix}-allow-all-between-wsfc-nodes" = { description = "Allow all between WSFC nodes" + source_ranges = [] sources = [module.compute-service-account.email] targets = [module.compute-service-account.email] use_service_accounts = true @@ -96,6 +97,7 @@ module "firewall" { } "${var.prefix}-allow-all-between-wsfc-witness" = { description = "Allow all between WSFC witness nodes" + source_ranges = [] sources = [module.compute-service-account.email] targets = [module.witness-service-account.email] use_service_accounts = true @@ -108,7 +110,7 @@ module "firewall" { "${var.prefix}-allow-sql-to-wsfc-nodes" = { description = "Allow SQL connections to WSFC nodes" targets = [module.compute-service-account.email] - ranges = var.sql_client_cidrs + source_ranges = var.sql_client_cidrs use_service_accounts = true rules = [ { protocol = "tcp", ports = [1433] }, @@ -117,7 +119,7 @@ module "firewall" { "${var.prefix}-allow-health-check-to-wsfc-nodes" = { description = "Allow health checks to WSFC nodes" targets = [module.compute-service-account.email] - ranges = var.health_check_ranges + source_ranges = var.health_check_ranges use_service_accounts = true rules = [ { protocol = "tcp" } diff --git a/blueprints/data-solutions/vertex-mlops/README.md b/blueprints/data-solutions/vertex-mlops/README.md new file mode 100644 index 000000000..d9f85fd83 --- /dev/null +++ b/blueprints/data-solutions/vertex-mlops/README.md @@ -0,0 +1,79 @@ +# MLOps with Vertex AI + +## Introduction +This example implements the infrastructure required to deploy an end-to-end [MLOps process](https://services.google.com/fh/files/misc/practitioners_guide_to_mlops_whitepaper.pdf) using [Vertex AI](https://cloud.google.com/vertex-ai) platform. + +## GCP resources +The blueprint will deploy all the required resources to have a fully functional MLOPs environment containing: +- Vertex Workbench (for the experimentation environment) +- GCP Project (optional) to host all the resources +- Isolated VPC network and a subnet to be used by Vertex and Dataflow. Alternatively, an external Shared VPC can be configured using the `network_config`variable. +- Firewall rule to allow the internal subnet communication required by Dataflow +- Cloud NAT required to reach the internet from the different computing resources (Vertex and Dataflow) +- GCS buckets to host Vertex AI and Cloud Build Artifacts. By default the buckets will be regional and should match the Vertex AI region for the different resources (i.e. Vertex Managed Dataset) and processes (i.e. Vertex trainining) +- BigQuery Dataset where the training data will be stored. This is optional, since the training data could be already hosted in an existing BigQuery dataset. +- Artifact Registry Docker repository to host the custom images. +- Service account (`mlops-[env]@`) with the minimum permissions required by Vertex AI and Dataflow (if this service is used inside of the Vertex AI Pipeline). +- Service account (`github@`) to be used by Workload Identity Federation, to federate Github identity (Optional). +- Secret to store the Github SSH key to get access the CICD code repo. + +![MLOps project description](./images/mlops_projects.png "MLOps project description") + +## Pre-requirements + +### User groups + +Assign roles relying on User groups is a way to decouple the final set of permissions from the stage where entities and resources are created, and their IAM bindings defined. You can configure the group names through the `groups` variable. These groups should be created before launching Terraform. + +We use the following groups to control access to resources: + +- *Data Scientits* (gcp-ml-ds@). They manage notebooks and create ML pipelines. +- *ML Engineers* (gcp-ml-eng@). They manage the different Vertex resources. +- *ML Viewer* (gcp-ml-eng@). Group with wiewer permission for the different resources. + +Please note that these groups are not suitable for production grade environments. Roles can be customized in the `main.tf`file. + +## Instructions +### Deploy the experimentation environment + +- Create a `terraform.tfvars` file and specify the variables to match your desired configuration. You can use the provided `terraform.tfvars.sample` as reference. +- Run `terraform init` and `terraform apply` + +## What's next? + +This blueprint can be used as a building block for setting up an end2end ML Ops solution. As next step, you can follow this [guide](https://cloud.google.com/architecture/architecture-for-mlops-using-tfx-kubeflow-pipelines-and-cloud-build) to setup a Vertex AI pipeline and run it on the deployed infraestructure. + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [project_id](variables.tf#L101) | Project id, references existing project if `project_create` is null. | string | ✓ | | +| [bucket_name](variables.tf#L18) | GCS bucket name to store the Vertex AI artifacts. | string | | null | +| [dataset_name](variables.tf#L24) | BigQuery Dataset to store the training data. | string | | null | +| [groups](variables.tf#L30) | Name of the groups (name@domain.org) to apply opinionated IAM permissions. | object({…}) | | {…} | +| [identity_pool_claims](variables.tf#L45) | Claims to be used by Workload Identity Federation (i.e.: attribute.repository/ORGANIZATION/REPO). If a not null value is provided, then google_iam_workload_identity_pool resource will be created. | string | | null | +| [labels](variables.tf#L51) | Labels to be assigned at project level. | map(string) | | {} | +| [location](variables.tf#L57) | Location used for multi-regional resources. | string | | "eu" | +| [network_config](variables.tf#L63) | Shared VPC network configurations to use. If null networks will be created in projects with preconfigured values. | object({…}) | | null | +| [notebooks](variables.tf#L73) | Vertex AI workbenchs to be deployed. | map(object({…})) | | {} | +| [prefix](variables.tf#L86) | Prefix used for the project id. | string | | null | +| [project_create](variables.tf#L92) | Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…}) | | null | +| [project_services](variables.tf#L106) | List of core services enabled on all projects. | list(string) | | […] | +| [region](variables.tf#L126) | Region used for regional resources. | string | | "europe-west4" | +| [repo_name](variables.tf#L132) | Cloud Source Repository name. null to avoid to create it. | string | | null | +| [sa_mlops_name](variables.tf#L138) | Name for the MLOPs Service Account. | string | | "sa-mlops" | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [github](outputs.tf#L33) | Github Configuration. | | +| [notebook](outputs.tf#L39) | Vertex AI managed notebook details. | | +| [project](outputs.tf#L44) | The project resource as return by the `project` module. | | +| [project_id](outputs.tf#L49) | Project ID. | | + + +# TODO +- Add support for User Managed Notebooks, SA permission option and non default SA for Single User mode. +- Improve default naming for local VPC and Cloud NAT \ No newline at end of file diff --git a/blueprints/data-solutions/vertex-mlops/ci-cd.tf b/blueprints/data-solutions/vertex-mlops/ci-cd.tf new file mode 100644 index 000000000..d73eacc83 --- /dev/null +++ b/blueprints/data-solutions/vertex-mlops/ci-cd.tf @@ -0,0 +1,74 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +resource "google_iam_workload_identity_pool" "github_pool" { + count = var.identity_pool_claims == null ? 0 : 1 + project = module.project.project_id + workload_identity_pool_id = "gh-pool" + display_name = "Github Actions Identity Pool" + description = "Identity pool for Github Actions" +} + +resource "google_iam_workload_identity_pool_provider" "github_provider" { + count = var.identity_pool_claims == null ? 0 : 1 + project = module.project.project_id + workload_identity_pool_id = google_iam_workload_identity_pool.github_pool[0].workload_identity_pool_id + workload_identity_pool_provider_id = "gh-provider" + display_name = "Github Actions provider" + description = "OIDC provider for Github Actions" + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.repository" = "assertion.repository" + } + oidc { + issuer_uri = "https://token.actions.githubusercontent.com" + } +} + +module "artifact_registry" { + source = "../../../modules/artifact-registry" + id = "docker-repo" + project_id = module.project.project_id + location = var.region + format = "DOCKER" + # iam = { + # "roles/artifactregistry.admin" = ["group:cicd@example.com"] + # } +} + +module "service-account-github" { + source = "../../../modules/iam-service-account" + name = "sa-github" + project_id = module.project.project_id + iam = var.identity_pool_claims == null ? {} : { "roles/iam.workloadIdentityUser" = ["principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github_pool[0].name}/${var.identity_pool_claims}"] } +} + +# NOTE: Secret manager module at the moment does not support CMEK +module "secret-manager" { + project_id = module.project.project_id + source = "../../../modules/secret-manager" + secrets = { + github-key = [var.region] + } + iam = { + github-key = { + "roles/secretmanager.secretAccessor" = [ + "serviceAccount:${module.project.service_accounts.robots.cloudbuild}", + module.service-account-mlops.iam_email + ] + } + } +} \ No newline at end of file diff --git a/blueprints/data-solutions/vertex-mlops/images/mlops_projects.png b/blueprints/data-solutions/vertex-mlops/images/mlops_projects.png new file mode 100644 index 000000000..24017bc9d Binary files /dev/null and b/blueprints/data-solutions/vertex-mlops/images/mlops_projects.png differ diff --git a/blueprints/data-solutions/vertex-mlops/main.tf b/blueprints/data-solutions/vertex-mlops/main.tf new file mode 100644 index 000000000..5f7fbc0c9 --- /dev/null +++ b/blueprints/data-solutions/vertex-mlops/main.tf @@ -0,0 +1,278 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +locals { + group_iam = merge( + var.groups.gcp-ml-viewer == null ? {} : { + (var.groups.gcp-ml-viewer) = [ + "roles/aiplatform.viewer", + "roles/artifactregistry.reader", + "roles/dataflow.viewer", + "roles/logging.viewer", + "roles/storage.objectViewer" + ] + }, + var.groups.gcp-ml-ds == null ? {} : { + (var.groups.gcp-ml-ds) = [ + "roles/aiplatform.admin", + "roles/artifactregistry.admin", + "roles/bigquery.dataEditor", + "roles/bigquery.jobUser", + "roles/bigquery.user", + "roles/cloudbuild.builds.editor", + "roles/cloudfunctions.developer", + "roles/dataflow.developer", + "roles/dataflow.worker", + "roles/iam.serviceAccountUser", + "roles/logging.logWriter", + "roles/logging.viewer", + "roles/notebooks.admin", + "roles/pubsub.editor", + "roles/serviceusage.serviceUsageConsumer", + "roles/storage.admin" + ] + }, + var.groups.gcp-ml-eng == null ? {} : { + (var.groups.gcp-ml-eng) = [ + "roles/aiplatform.admin", + "roles/artifactregistry.admin", + "roles/bigquery.dataEditor", + "roles/bigquery.jobUser", + "roles/bigquery.user", + "roles/dataflow.developer", + "roles/dataflow.worker", + "roles/iam.serviceAccountUser", + "roles/logging.logWriter", + "roles/logging.viewer", + "roles/serviceusage.serviceUsageConsumer", + "roles/storage.admin" + ] + } + ) + + service_encryption_keys = var.service_encryption_keys + shared_vpc_project = try(var.network_config.host_project, null) + + subnet = ( + local.use_shared_vpc + ? var.network_config.subnet_self_link + : values(module.vpc-local.0.subnet_self_links)[0] + ) + vpc = ( + local.use_shared_vpc + ? var.network_config.network_self_link + : module.vpc-local.0.self_link + ) + use_shared_vpc = var.network_config != null + + shared_vpc_bindings = { + "roles/compute.networkUser" = [ + "robot-df", "notebooks" + ] + } + + shared_vpc_role_members = { + robot-df = "serviceAccount:${module.project.service_accounts.robots.dataflow}" + notebooks = "serviceAccount:${module.project.service_accounts.robots.notebooks}" + } + + # reassemble in a format suitable for for_each + shared_vpc_bindings_map = { + for binding in flatten([ + for role, members in local.shared_vpc_bindings : [ + for member in members : { role = role, member = member } + ] + ]) : "${binding.role}-${binding.member}" => binding + } +} + +module "gcs-bucket" { + count = var.bucket_name == null ? 0 : 1 + source = "../../../modules/gcs" + project_id = module.project.project_id + name = var.bucket_name + prefix = var.prefix + location = var.region + storage_class = "REGIONAL" + versioning = false + encryption_key = try(local.service_encryption_keys.storage, null) +} + +# Default bucket for Cloud Build to prevent error: "'us' violates constraint ‘constraints/gcp.resourceLocations’" +# https://stackoverflow.com/questions/53206667/cloud-build-fails-with-resource-location-constraint +module "gcs-bucket-cloudbuild" { + source = "../../../modules/gcs" + project_id = module.project.project_id + name = "${var.project_id}_cloudbuild" + prefix = var.prefix + location = var.region + storage_class = "REGIONAL" + versioning = false + encryption_key = try(local.service_encryption_keys.storage, null) +} + +module "bq-dataset" { + count = var.dataset_name == null ? 0 : 1 + source = "../../../modules/bigquery-dataset" + project_id = module.project.project_id + id = var.dataset_name + location = var.region + encryption_key = try(local.service_encryption_keys.bq, null) +} + +module "vpc-local" { + count = local.use_shared_vpc ? 0 : 1 + source = "../../../modules/net-vpc" + project_id = module.project.project_id + name = "default" + subnets = [ + { + "name" : "default", + "region" : "${var.region}", + "ip_cidr_range" : "10.4.0.0/24", + "secondary_ip_range" : null + } + ] + psa_config = { + ranges = { + "vertex" : "10.13.0.0/18" + } + routes = null + } +} + +module "firewall" { + count = local.use_shared_vpc ? 0 : 1 + source = "../../../modules/net-vpc-firewall" + project_id = module.project.project_id + network = module.vpc-local[0].name + default_rules_config = { + disabled = true + } + ingress_rules = { + dataflow-ingress = { + description = "Dataflow service." + direction = "INGRESS" + action = "allow" + sources = ["dataflow"] + targets = ["dataflow"] + ranges = [] + use_service_accounts = false + rules = [{ protocol = "tcp", ports = ["12345-12346"] }] + extra_attributes = {} + } + } + +} + +module "cloudnat" { + count = local.use_shared_vpc ? 0 : 1 + source = "../../../modules/net-cloudnat" + project_id = module.project.project_id + region = var.region + name = "default" + router_network = module.vpc-local[0].self_link +} + +module "project" { + source = "../../../modules/project" + name = var.project_id + parent = try(var.project_create.parent, null) + billing_account = try(var.project_create.billing_account_id, null) + project_create = var.project_create != null + prefix = var.prefix + group_iam = local.group_iam + iam = { + "roles/aiplatform.user" = [module.service-account-mlops.iam_email] + "roles/artifactregistry.reader" = [module.service-account-mlops.iam_email] + "roles/artifactregistry.writer" = [module.service-account-github.iam_email] + "roles/bigquery.dataEditor" = [module.service-account-mlops.iam_email] + "roles/bigquery.jobUser" = [module.service-account-mlops.iam_email] + "roles/bigquery.user" = [module.service-account-mlops.iam_email] + "roles/cloudbuild.builds.editor" = [ + module.service-account-mlops.iam_email, + module.service-account-github.iam_email + ] + + "roles/cloudfunctions.invoker" = [module.service-account-mlops.iam_email] + "roles/dataflow.developer" = [module.service-account-mlops.iam_email] + "roles/dataflow.worker" = [module.service-account-mlops.iam_email] + "roles/iam.serviceAccountUser" = [ + module.service-account-mlops.iam_email, + "serviceAccount:${module.project.service_accounts.robots.cloudbuild}" + ] + "roles/monitoring.metricWriter" = [module.service-account-mlops.iam_email] + "roles/run.invoker" = [module.service-account-mlops.iam_email] + "roles/serviceusage.serviceUsageConsumer" = [ + module.service-account-mlops.iam_email, + module.service-account-github.iam_email + ] + "roles/storage.admin" = [ + module.service-account-mlops.iam_email, + module.service-account-github.iam_email + ] + } + labels = var.labels + + org_policies = { + # Example of applying a project wide policy + # "constraints/compute.requireOsLogin" = { + # enforce = false + # } + } + + service_encryption_key_ids = { + bq = [try(local.service_encryption_keys.bq, null)] + compute = [try(local.service_encryption_keys.compute, null)] + cloudbuild = [try(local.service_encryption_keys.storage, null)] + notebooks = [try(local.service_encryption_keys.compute, null)] + storage = [try(local.service_encryption_keys.storage, null)] + } + services = var.project_services + + + shared_vpc_service_config = local.shared_vpc_project == null ? null : { + attach = true + host_project = local.shared_vpc_project + } + +} + +module "service-account-mlops" { + source = "../../../modules/iam-service-account" + name = var.sa_mlops_name + project_id = module.project.project_id + iam = { + "roles/iam.serviceAccountUser" = [module.service-account-github.iam_email] + } +} + +resource "google_project_iam_member" "shared_vpc" { + count = local.use_shared_vpc ? 1 : 0 + project = var.network_config.host_project + role = "roles/compute.networkUser" + member = "serviceAccount:${module.project.service_accounts.robots.notebooks}" +} + + +resource "google_sourcerepo_repository" "code-repo" { + count = var.repo_name == null ? 0 : 1 + name = var.repo_name + project = module.project.project_id +} + + diff --git a/blueprints/data-solutions/vertex-mlops/notebooks.tf b/blueprints/data-solutions/vertex-mlops/notebooks.tf new file mode 100644 index 000000000..09d3e5a8b --- /dev/null +++ b/blueprints/data-solutions/vertex-mlops/notebooks.tf @@ -0,0 +1,60 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +resource "google_notebooks_runtime" "runtime" { + for_each = var.notebooks + name = each.key + + project = module.project.project_id + location = var.notebooks[each.key].region + access_config { + access_type = "SINGLE_USER" + runtime_owner = var.notebooks[each.key].owner + } + software_config { + enable_health_monitoring = true + idle_shutdown = var.notebooks[each.key].idle_shutdown + idle_shutdown_timeout = 1800 + } + virtual_machine { + virtual_machine_config { + machine_type = "n1-standard-4" + network = local.vpc + subnet = local.subnet + internal_ip_only = var.notebooks[each.key].internal_ip_only + dynamic "encryption_config" { + for_each = try(local.service_encryption_keys.compute, null) == null ? [] : [1] + content { + kms_key = local.service_encryption_keys.compute + } + } + metadata = { + notebook-disable-nbconvert = "false" + notebook-disable-downloads = "false" + notebook-disable-terminal = "false" + #notebook-disable-root = "true" + #notebook-upgrade-schedule = "48 4 * * MON" + } + data_disk { + initialize_params { + disk_size_gb = "100" + disk_type = "PD_STANDARD" + } + } + } + } +} + diff --git a/blueprints/data-solutions/vertex-mlops/outputs.tf b/blueprints/data-solutions/vertex-mlops/outputs.tf new file mode 100644 index 000000000..9cb390d62 --- /dev/null +++ b/blueprints/data-solutions/vertex-mlops/outputs.tf @@ -0,0 +1,52 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# TODO(): proper outputs + + +locals { + docker_split = try(split("/", module.artifact_registry.id), null) + docker_repo = try("${local.docker_split[3]}-docker.pkg.dev/${local.docker_split[1]}/${local.docker_split[5]}", null) + gh_config = { + WORKLOAD_ID_PROVIDER = try(google_iam_workload_identity_pool_provider.github_provider[0].name, null) + SERVICE_ACCOUNT = try(module.service-account-github.email, null) + PROJECT_ID = module.project.project_id + DOCKER_REPO = local.docker_repo + SA_MLOPS = module.service-account-mlops.email + SUBNETWORK = local.subnet + } +} + +output "github" { + + description = "Github Configuration." + value = local.gh_config +} + +output "notebook" { + description = "Vertex AI managed notebook details." + value = { for k, v in resource.google_notebooks_runtime.runtime : k => v.id } +} + +output "project" { + description = "The project resource as return by the `project` module." + value = module.project +} + +output "project_id" { + description = "Project ID." + value = module.project.project_id +} diff --git a/blueprints/data-solutions/vertex-mlops/terraform.tfvars.sample b/blueprints/data-solutions/vertex-mlops/terraform.tfvars.sample new file mode 100644 index 000000000..097bac3a8 --- /dev/null +++ b/blueprints/data-solutions/vertex-mlops/terraform.tfvars.sample @@ -0,0 +1,20 @@ +bucket_name = "creditcards-dev" +dataset_name = "creditcards" +identity_pool_claims = "attribute.repository/ORGANIZATION/REPO" +labels = { + "env" : "dev", + "team" : "ml" +} +notebooks = { + "myworkbench" : { + "owner" : "user@example.com", + "region" : "europe-west4", + "subnet" : "default", + } +} +prefix = "pref" +project_id = "creditcards-dev" +project_create = { + billing_account_id = "000000-123456-123456" + parent = "folders/111111111111" +} diff --git a/blueprints/data-solutions/vertex-mlops/variables.tf b/blueprints/data-solutions/vertex-mlops/variables.tf new file mode 100644 index 000000000..f3f6efad3 --- /dev/null +++ b/blueprints/data-solutions/vertex-mlops/variables.tf @@ -0,0 +1,152 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +variable "bucket_name" { + description = "GCS bucket name to store the Vertex AI artifacts." + type = string + default = null +} + +variable "dataset_name" { + description = "BigQuery Dataset to store the training data." + type = string + default = null +} + +variable "groups" { + description = "Name of the groups (name@domain.org) to apply opinionated IAM permissions." + type = object({ + gcp-ml-ds = string + gcp-ml-eng = string + gcp-ml-viewer = string + }) + default = { + gcp-ml-ds = null + gcp-ml-eng = null + gcp-ml-viewer = null + } + nullable = false +} + +variable "identity_pool_claims" { + description = "Claims to be used by Workload Identity Federation (i.e.: attribute.repository/ORGANIZATION/REPO). If a not null value is provided, then google_iam_workload_identity_pool resource will be created." + type = string + default = null +} + +variable "labels" { + description = "Labels to be assigned at project level." + type = map(string) + default = {} +} + +variable "location" { + description = "Location used for multi-regional resources." + type = string + default = "eu" +} + +variable "network_config" { + description = "Shared VPC network configurations to use. If null networks will be created in projects with preconfigured values." + type = object({ + host_project = string + network_self_link = string + subnet_self_link = string + }) + default = null +} + +variable "notebooks" { + description = "Vertex AI workbenchs to be deployed." + type = map(object({ + owner = string + region = string + subnet = string + internal_ip_only = optional(bool, false) + idle_shutdown = optional(bool) + })) + default = {} + nullable = false +} + +variable "prefix" { + description = "Prefix used for the project id." + type = string + default = null +} + +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 "project_services" { + description = "List of core services enabled on all projects." + type = list(string) + default = [ + "aiplatform.googleapis.com", + "artifactregistry.googleapis.com", + "bigquery.googleapis.com", + "cloudbuild.googleapis.com", + "compute.googleapis.com", + "datacatalog.googleapis.com", + "dataflow.googleapis.com", + "iam.googleapis.com", + "monitoring.googleapis.com", + "notebooks.googleapis.com", + "secretmanager.googleapis.com", + "servicenetworking.googleapis.com", + "serviceusage.googleapis.com" + ] +} + +variable "region" { + description = "Region used for regional resources." + type = string + default = "europe-west4" +} + +variable "repo_name" { + description = "Cloud Source Repository name. null to avoid to create it." + type = string + default = null +} + +variable "sa_mlops_name" { + description = "Name for the MLOPs Service Account." + type = string + default = "sa-mlops" +} + +variable "service_encryption_keys" { # service encription key + description = "Cloud KMS to use to encrypt different services. Key location should match service region." + type = object({ + bq = string + compute = string + storage = string + }) + default = null +} \ No newline at end of file diff --git a/blueprints/factories/bigquery-factory/README.md b/blueprints/factories/bigquery-factory/README.md index 05cabffb2..2cba6e01f 100644 --- a/blueprints/factories/bigquery-factory/README.md +++ b/blueprints/factories/bigquery-factory/README.md @@ -1,36 +1,18 @@ # Google Cloud BQ Factory -This module allows creation and management of BigQuery datasets and views as well as tables by defining them in well formatted `yaml` files. +This module allows creation and management of BigQuery datasets tables and views by defining them in well-formatted YAML files. YAML abstraction for BQ can simplify users onboarding and also makes creation of tables easier compared to HCL. -Yaml abstraction for BQ can simplify users onboarding and also makes creation of tables easier compared to HCL. +This factory is based on the [BQ dataset module](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/tree/master/modules/bigquery-dataset) which currently only supports tables and views. As soon as external table and materialized view support is added, this factory will be enhanced accordingly. -Subfolders distinguish between views and tables and ensures easier navigation for users. - -This factory is based on the [BQ dataset module](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/tree/master/modules/bigquery-dataset) which currently only supports tables and views. As soon as external table and materialized view support is added, factory will be enhanced accordingly. - -You can create as many files as you like, the code will loop through it and create the required variables in order to execute everything accordingly. +You can create as many files as you like, the code will loop through it and create everything accordingly. ## Example ### Terraform code -```hcl -module "bq" { - source = "github.com/GoogleCloudPlatform/cloud-foundation-fabric/modules/bigquery-dataset" - - for_each = local.output - project_id = var.project_id - id = each.key - views = try(each.value.views, null) - tables = try(each.value.tables, null) -} -# tftest skip -``` - -### Configuration Structure - +In this section we show how to create tables and views from a file structure simlar to the one shown below. ```bash -base_folder +bigquery │ ├── tables │ ├── table_a.yaml @@ -40,32 +22,43 @@ base_folder │ ├── view_b.yaml ``` -## YAML structure and definition formatting - -### Tables - -Table definition to be placed in a set of yaml files in the corresponding subfolder. Structure should look as following: +First we create the table definition in `bigquery/tables/countries.yaml`. ```yaml - -dataset: # required name of the dataset the table is to be placed in -table: # required descriptive name of the table -schema: # required schema in JSON FORMAT Example: [{name: "test", type: "STRING"},{name: "test2", type: "INT64"}] -labels: # not required, defaults to {}, Example: {"a":"thisislabela","b":"thisislabelb"} -use_legacy_sql: boolean # not required, defaults to false -deletion_protection: boolean # not required, defaults to false +# tftest-file id=table path=bigquery/tables/countries.yaml +dataset: my_dataset +table: countries +deletion_protection: true +labels: + env: prod +schema: + - name: country + type: STRING + - name: population + type: INT64 ``` -### Views -View definition to be placed in a set of yaml files in the corresponding subfolder. Structure should look as following: +And a view in `bigquery/views/population.yaml`. ```yaml -dataset: # required, name of the dataset the view is to be placed in -view: # required, descriptive name of the view -query: # required, SQL Query for the view in quotes -labels: # not required, defaults to {}, Example: {"a":"thisislabela","b":"thisislabelb"} -use_legacy_sql: bool # not required, defaults to false -deletion_protection: bool # not required, defaults to false +# tftest-file id=view path=bigquery/views/population.yaml +dataset: my_dataset +view: department +query: SELECT SUM(population) from my_dataset.countries +labels: + env: prod +``` + +With this file structure, we can use the factory as follows: + +```hcl +module "bq" { + source = "./fabric/blueprints/factories/bigquery-factory" + project_id = var.project_id + tables_path = "bigquery/tables" + views_path = "bigquery/views" +} +# tftest modules=2 resources=3 files=table,view inventory=simple.yaml ``` @@ -74,8 +67,8 @@ deletion_protection: bool # not required, defaults to false | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [project_id](variables.tf#L17) | Project ID. | string | ✓ | | -| [tables_dir](variables.tf#L22) | Relative path for the folder storing table data. | string | ✓ | | -| [views_dir](variables.tf#L27) | Relative path for the folder storing view data. | string | ✓ | | +| [tables_path](variables.tf#L22) | Relative path for the folder storing table data. | string | ✓ | | +| [views_path](variables.tf#L27) | Relative path for the folder storing view data. | string | ✓ | | ## TODO diff --git a/blueprints/factories/bigquery-factory/main.tf b/blueprints/factories/bigquery-factory/main.tf index 5995ea191..8c26f7479 100644 --- a/blueprints/factories/bigquery-factory/main.tf +++ b/blueprints/factories/bigquery-factory/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. @@ -16,17 +16,22 @@ locals { views = { - for f in fileset("${var.views_dir}", "**/*.yaml") : - trimsuffix(f, ".yaml") => yamldecode(file("${var.views_dir}/${f}")) + for f in fileset(var.views_path, "**/*.yaml") : + trimsuffix(f, ".yaml") => yamldecode(file("${var.views_path}/${f}")) } tables = { - for f in fileset("${var.tables_dir}", "**/*.yaml") : - trimsuffix(f, ".yaml") => yamldecode(file("${var.tables_dir}/${f}")) + for f in fileset(var.tables_path, "**/*.yaml") : + trimsuffix(f, ".yaml") => yamldecode(file("${var.tables_path}/${f}")) } - output = { - for dataset in distinct([for v in values(merge(local.views, local.tables)) : v.dataset]) : + all_datasets = distinct(concat( + [for x in values(local.tables) : x.dataset], + [for x in values(local.views) : x.dataset] + )) + + datasets = { + for dataset in local.all_datasets : dataset => { "views" = { for k, v in local.views : @@ -57,9 +62,8 @@ locals { } module "bq" { - source = "../../../modules/bigquery-dataset" - - for_each = local.output + source = "../../../modules/bigquery-dataset" + for_each = local.datasets project_id = var.project_id id = each.key views = try(each.value.views, null) diff --git a/blueprints/factories/bigquery-factory/variables.tf b/blueprints/factories/bigquery-factory/variables.tf index 774ec86e1..57025f629 100644 --- a/blueprints/factories/bigquery-factory/variables.tf +++ b/blueprints/factories/bigquery-factory/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. @@ -19,12 +19,12 @@ variable "project_id" { type = string } -variable "tables_dir" { +variable "tables_path" { description = "Relative path for the folder storing table data." type = string } -variable "views_dir" { +variable "views_path" { description = "Relative path for the folder storing view data." type = string } diff --git a/blueprints/factories/cloud-identity-group-factory/README.md b/blueprints/factories/cloud-identity-group-factory/README.md index 052b9a4af..b833304eb 100644 --- a/blueprints/factories/cloud-identity-group-factory/README.md +++ b/blueprints/factories/cloud-identity-group-factory/README.md @@ -11,9 +11,9 @@ Yaml abstraction for Groups can simplify groups creation and members management. ```hcl module "prod-firewall" { source = "./fabric/blueprints/factories/cloud-identity-group-factory" - - customer_id = "customers/C0xxxxxxx" - data_dir = "data" + + customer_id = "customers/C0xxxxxxx" + data_dir = "data" } # tftest skip ``` diff --git a/blueprints/factories/net-vpc-firewall-yaml/README.md b/blueprints/factories/net-vpc-firewall-yaml/README.md index 26e85c5d8..5e7260e94 100644 --- a/blueprints/factories/net-vpc-firewall-yaml/README.md +++ b/blueprints/factories/net-vpc-firewall-yaml/README.md @@ -14,14 +14,14 @@ Nested folder structure for yaml configurations is optionally supported, which a module "prod-firewall" { source = "./fabric/blueprints/factories/net-vpc-firewall-yaml" - project_id = "my-prod-project" - network = "my-prod-network" + project_id = "my-prod-project" + network = "my-prod-network" config_directories = [ "./prod", "./common" ] - log_config = { + log_config = { metadata = "INCLUDE_ALL_METADATA" } } @@ -29,8 +29,8 @@ module "prod-firewall" { module "dev-firewall" { source = "./fabric/blueprints/factories/net-vpc-firewall-yaml" - project_id = "my-dev-project" - network = "my-dev-network" + project_id = "my-dev-project" + network = "my-dev-network" config_directories = [ "./dev", "./common" diff --git a/blueprints/factories/net-vpc-firewall-yaml/versions.tf b/blueprints/factories/net-vpc-firewall-yaml/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/factories/net-vpc-firewall-yaml/versions.tf +++ b/blueprints/factories/net-vpc-firewall-yaml/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/factories/project-factory/README.md b/blueprints/factories/project-factory/README.md index a5680781a..2b8c3874e 100644 --- a/blueprints/factories/project-factory/README.md +++ b/blueprints/factories/project-factory/README.md @@ -49,8 +49,8 @@ locals { trimsuffix(f, ".yaml") => yamldecode(file("${local._data_dir}/${f}")) } # these are usually set via variables - _base_dir = "./fabric/blueprints/factories/project-factory" - _data_dir = "${local._base_dir}/sample-data/projects/" + _base_dir = "./fabric/blueprints/factories/project-factory" + _data_dir = "${local._base_dir}/sample-data/projects/" _defaults_file = "${local._base_dir}/sample-data/defaults.yaml" } @@ -59,6 +59,7 @@ module "projects" { for_each = local.projects defaults = local.defaults project_id = each.key + descriptive_name = try(each.value.descriptive_name, null) billing_account_id = try(each.value.billing_account_id, null) billing_alert = try(each.value.billing_alert, null) dns_zones = try(each.value.dns_zones, []) @@ -222,28 +223,29 @@ vpc: | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [billing_account_id](variables.tf#L17) | Billing account id. | string | ✓ | | -| [prefix](variables.tf#L151) | Prefix used for resource names. | string | ✓ | | -| [project_id](variables.tf#L160) | Project id. | string | ✓ | | +| [prefix](variables.tf#L157) | Prefix used for resource names. | string | ✓ | | +| [project_id](variables.tf#L166) | Project id. | string | ✓ | | | [billing_alert](variables.tf#L22) | Billing alert configuration. | object({…}) | | null | | [defaults](variables.tf#L35) | Project factory default values. | object({…}) | | null | -| [dns_zones](variables.tf#L57) | DNS private zones to create as child of var.defaults.environment_dns_zone. | list(string) | | [] | -| [essential_contacts](variables.tf#L63) | Email contacts to be used for billing and GCP notifications. | list(string) | | [] | -| [folder_id](variables.tf#L69) | Folder ID for the folder where the project will be created. | string | | null | -| [group_iam](variables.tf#L75) | Custom IAM settings in group => [role] format. | map(list(string)) | | {} | -| [group_iam_additive](variables.tf#L81) | Custom additive IAM settings in group => [role] format. | map(list(string)) | | {} | -| [iam](variables.tf#L87) | Custom IAM settings in role => [principal] format. | map(list(string)) | | {} | -| [iam_additive](variables.tf#L93) | Custom additive IAM settings in role => [principal] format. | map(list(string)) | | {} | -| [kms_service_agents](variables.tf#L99) | KMS IAM configuration in as service => [key]. | map(list(string)) | | {} | -| [labels](variables.tf#L105) | Labels to be assigned at project level. | map(string) | | {} | -| [org_policies](variables.tf#L111) | Org-policy overrides at project level. | map(object({…})) | | {} | -| [service_accounts](variables.tf#L165) | Service accounts to be created, and roles assigned them on the project. | map(list(string)) | | {} | -| [service_accounts_additive](variables.tf#L171) | Service accounts to be created, and roles assigned them on the project additively. | map(list(string)) | | {} | -| [service_accounts_iam](variables.tf#L177) | IAM bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}. | map(map(list(string))) | | {} | -| [service_accounts_iam_additive](variables.tf#L184) | IAM additive bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}. | map(map(list(string))) | | {} | -| [service_identities_iam](variables.tf#L191) | Custom IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | -| [service_identities_iam_additive](variables.tf#L198) | Custom additive IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | -| [services](variables.tf#L205) | Services to be enabled for the project. | list(string) | | [] | -| [vpc](variables.tf#L212) | VPC configuration for the project. | object({…}) | | null | +| [descriptive_name](variables.tf#L57) | Name of the project name. Used for project name instead of `name` variable. | string | | null | +| [dns_zones](variables.tf#L63) | DNS private zones to create as child of var.defaults.environment_dns_zone. | list(string) | | [] | +| [essential_contacts](variables.tf#L69) | Email contacts to be used for billing and GCP notifications. | list(string) | | [] | +| [folder_id](variables.tf#L75) | Folder ID for the folder where the project will be created. | string | | null | +| [group_iam](variables.tf#L81) | Custom IAM settings in group => [role] format. | map(list(string)) | | {} | +| [group_iam_additive](variables.tf#L87) | Custom additive IAM settings in group => [role] format. | map(list(string)) | | {} | +| [iam](variables.tf#L93) | Custom IAM settings in role => [principal] format. | map(list(string)) | | {} | +| [iam_additive](variables.tf#L99) | Custom additive IAM settings in role => [principal] format. | map(list(string)) | | {} | +| [kms_service_agents](variables.tf#L105) | KMS IAM configuration in as service => [key]. | map(list(string)) | | {} | +| [labels](variables.tf#L111) | Labels to be assigned at project level. | map(string) | | {} | +| [org_policies](variables.tf#L117) | Org-policy overrides at project level. | map(object({…})) | | {} | +| [service_accounts](variables.tf#L171) | Service accounts to be created, and roles assigned them on the project. | map(list(string)) | | {} | +| [service_accounts_additive](variables.tf#L177) | Service accounts to be created, and roles assigned them on the project additively. | map(list(string)) | | {} | +| [service_accounts_iam](variables.tf#L183) | IAM bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}. | map(map(list(string))) | | {} | +| [service_accounts_iam_additive](variables.tf#L190) | IAM additive bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}. | map(map(list(string))) | | {} | +| [service_identities_iam](variables.tf#L197) | Custom IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | +| [service_identities_iam_additive](variables.tf#L204) | Custom additive IAM settings for service identities in service => [role] format. | map(list(string)) | | {} | +| [services](variables.tf#L211) | Services to be enabled for the project. | list(string) | | [] | +| [vpc](variables.tf#L218) | VPC configuration for the project. | object({…}) | | null | ## Outputs diff --git a/blueprints/factories/project-factory/main.tf b/blueprints/factories/project-factory/main.tf index f6b2a797c..518d5a69c 100644 --- a/blueprints/factories/project-factory/main.tf +++ b/blueprints/factories/project-factory/main.tf @@ -180,6 +180,7 @@ module "project" { source = "../../../modules/project" billing_account = local.billing_account_id name = var.project_id + descriptive_name = var.descriptive_name prefix = var.prefix contacts = { for c in local.essential_contacts : c => ["ALL"] } iam = local.iam diff --git a/blueprints/factories/project-factory/variables.tf b/blueprints/factories/project-factory/variables.tf index 0ece0f042..3aa3fa36b 100644 --- a/blueprints/factories/project-factory/variables.tf +++ b/blueprints/factories/project-factory/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. @@ -54,6 +54,12 @@ variable "defaults" { default = null } +variable "descriptive_name" { + description = "Name of the project name. Used for project name instead of `name` variable." + type = string + default = null +} + variable "dns_zones" { description = "DNS private zones to create as child of var.defaults.environment_dns_zone." type = list(string) diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/install.yaml b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/install.yaml index b81c49622..f59f03e3d 100644 --- a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/install.yaml +++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/install.yaml @@ -23,25 +23,6 @@ set_fact: context: "gke_{{ project_id }}_{{ region }}_{{ cluster }}" -- name: Install ASM in cluster - shell: > - gcloud container fleet mesh update \ - --control-plane automatic \ - --memberships {{ cluster }} \ - --project {{ project_id }} - -- name: Wait until MCP is provisioned - shell: > - for i in $(seq 12); do - result=$(gcloud container fleet mesh describe --project {{ project_id }} --format json \ - | jq -r '.membershipStates | to_entries[] | select(.key | endswith("{{ cluster }}")) | .value.servicemesh.controlPlaneManagement.state') - if [ "$result" = "ACTIVE" ]; then - break - fi - echo "ASM control plane is not ready yet..." - sleep 60 - done - - name: Get endpoint IP shell: > gcloud container clusters describe "{{ cluster }}" \ diff --git a/blueprints/gke/multitenant-fleet/README.md b/blueprints/gke/multitenant-fleet/README.md index 80d09ac10..cadcf4109 100644 --- a/blueprints/gke/multitenant-fleet/README.md +++ b/blueprints/gke/multitenant-fleet/README.md @@ -4,7 +4,7 @@ This blueprint presents an opinionated architecture to handle multiple homogeneo The pattern used in this design is useful, for blueprint, in cases where multiple clusters host/support the same workloads, such as in the case of a multi-regional deployment. Furthermore, combined with Anthos Config Sync and proper RBAC, this architecture can be used to host multiple tenants (e.g. teams, applications) sharing the clusters. -This blueprint is used as part of the [FAST GKE stage](../../../fast/stages/03-gke-multitenant/) but it can also be used independently if desired. +This blueprint is used as part of the [FAST GKE stage](../../../fast/stages/3-gke-multitenant/) but it can also be used independently if desired.

GKE multitenant @@ -78,7 +78,7 @@ module "gke-fleet" { location = "europe-west1" private_cluster_config = local.cluster_defaults.private_cluster_config vpc_config = { - subnetwork = local.subnet_self_links.ew1 + subnetwork = local.subnet_self_links.ew1 master_ipv4_cidr_block = "172.16.10.0/28" } } @@ -86,7 +86,7 @@ module "gke-fleet" { location = "europe-west3" private_cluster_config = local.cluster_defaults.private_cluster_config vpc_config = { - subnetwork = local.subnet_self_links.ew3 + subnetwork = local.subnet_self_links.ew3 master_ipv4_cidr_block = "172.16.20.0/28" } } @@ -95,16 +95,16 @@ module "gke-fleet" { cluster-0 = { nodepool-0 = { node_config = { - disk_type = "pd-balanced" + disk_type = "pd-balanced" machine_type = "n2-standard-4" - spot = true + spot = true } } } cluster-1 = { nodepool-0 = { node_config = { - disk_type = "pd-balanced" + disk_type = "pd-balanced" machine_type = "n2-standard-4" } } @@ -115,7 +115,7 @@ module "gke-fleet" { vpc_self_link = "projects/prj-host/global/networks/prod-0" } } -# tftest modules=7 resources=26 +# tftest modules=7 resources=27 ``` ## GKE Fleet @@ -143,13 +143,13 @@ module "gke" { prefix = "myprefix" clusters = { cluster-0 = { - location = "europe-west1" + location = "europe-west1" vpc_config = { subnetwork = local.subnet_self_links.ew1 } } cluster-1 = { - location = "europe-west3" + location = "europe-west3" vpc_config = { subnetwork = local.subnet_self_links.ew3 } @@ -159,16 +159,16 @@ module "gke" { cluster-0 = { nodepool-0 = { node_config = { - disk_type = "pd-balanced" + disk_type = "pd-balanced" machine_type = "n2-standard-4" - spot = true + spot = true } } } cluster-1 = { nodepool-0 = { node_config = { - disk_type = "pd-balanced" + disk_type = "pd-balanced" machine_type = "n2-standard-4" } } @@ -205,14 +205,14 @@ module "gke" { enable_hierarchical_resource_quota = true enable_pod_tree_labels = true } - policy_controller = { + policy_controller = { audit_interval_seconds = 30 exemptable_namespaces = ["kube-system"] log_denies_enabled = true referential_rules_enabled = true template_library_installed = true } - version = "1.10.2" + version = "1.10.2" } } fleet_configmanagement_clusters = { @@ -224,7 +224,7 @@ module "gke" { } } -# tftest modules=8 resources=35 +# tftest modules=8 resources=38 ``` diff --git a/blueprints/networking/README.md b/blueprints/networking/README.md index ef3932689..ec510d564 100644 --- a/blueprints/networking/README.md +++ b/blueprints/networking/README.md @@ -49,19 +49,20 @@ The blueprint shows how to implement spoke transitivity via BGP advertisements, ### DNS and Private Access for On-premises - This [blueprint](./onprem-google-access-dns/) uses an emulated on-premises environment running in Docker containers inside a GCE instance, to allow testing specific features like DNS policies, DNS forwarding zones across VPN, and Private Access for On-premises hosts. + This [blueprint](./onprem-google-access-dns/) uses an emulated on-premises environment running in Docker containers inside a GCE instance, to allow testing specific features like DNS policies, DNS forwarding zones across VPN, and Private Access for On-premises hosts. The emulated on-premises environment can be used to test access to different services from outside Google Cloud, by implementing a VPN connection and BGP to Google CLoud via Strongswan and Bird.
+--> + ### Calling a private Cloud Function from on-premises This [blueprint](./private-cloud-function-from-onprem/) shows how to invoke a [private Google Cloud Function](https://cloud.google.com/functions/docs/networking/network-settings) from the on-prem environment via a [Private Service Connect endpoint](https://cloud.google.com/vpc/docs/private-service-connect#benefits-apis). diff --git a/blueprints/networking/_deprecated/README.md b/blueprints/networking/__need_fixing/README.md similarity index 100% rename from blueprints/networking/_deprecated/README.md rename to blueprints/networking/__need_fixing/README.md diff --git a/blueprints/networking/_deprecated/nginx-reverse-proxy-cluster/Dockerfile b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/Dockerfile similarity index 100% rename from blueprints/networking/_deprecated/nginx-reverse-proxy-cluster/Dockerfile rename to blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/Dockerfile diff --git a/blueprints/networking/_deprecated/nginx-reverse-proxy-cluster/README.md b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/README.md similarity index 100% rename from blueprints/networking/_deprecated/nginx-reverse-proxy-cluster/README.md rename to blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/README.md diff --git a/blueprints/networking/_deprecated/nginx-reverse-proxy-cluster/main.tf b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/main.tf similarity index 100% rename from blueprints/networking/_deprecated/nginx-reverse-proxy-cluster/main.tf rename to blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/main.tf diff --git a/blueprints/networking/_deprecated/nginx-reverse-proxy-cluster/outputs.tf b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/outputs.tf similarity index 100% rename from blueprints/networking/_deprecated/nginx-reverse-proxy-cluster/outputs.tf rename to blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/outputs.tf diff --git a/blueprints/networking/_deprecated/nginx-reverse-proxy-cluster/reverse-proxy.png b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/reverse-proxy.png similarity index 100% rename from blueprints/networking/_deprecated/nginx-reverse-proxy-cluster/reverse-proxy.png rename to blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/reverse-proxy.png diff --git a/blueprints/networking/_deprecated/nginx-reverse-proxy-cluster/variables.tf b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/variables.tf similarity index 100% rename from blueprints/networking/_deprecated/nginx-reverse-proxy-cluster/variables.tf rename to blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/variables.tf diff --git a/blueprints/networking/_deprecated/nginx-reverse-proxy-cluster/versions.tf b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/versions.tf similarity index 91% rename from blueprints/networking/_deprecated/nginx-reverse-proxy-cluster/versions.tf rename to blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/networking/_deprecated/nginx-reverse-proxy-cluster/versions.tf +++ b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/networking/onprem-google-access-dns/README.md b/blueprints/networking/__need_fixing/onprem-google-access-dns/README.md similarity index 95% rename from blueprints/networking/onprem-google-access-dns/README.md rename to blueprints/networking/__need_fixing/onprem-google-access-dns/README.md index fae563986..deb3593f7 100644 --- a/blueprints/networking/onprem-google-access-dns/README.md +++ b/blueprints/networking/__need_fixing/onprem-google-access-dns/README.md @@ -1,6 +1,6 @@ # On-prem DNS and Google Private Access -This blueprint leverages the [on prem in a box](../../../modules/cloud-config-container/onprem) module to bootstrap an emulated on-premises environment on GCP, then connects it via VPN and sets up BGP and DNS so that several specific features can be tested: +This blueprint leverages the on prem in a box module to bootstrap an emulated on-premises environment on GCP, then connects it via VPN and sets up BGP and DNS so that several specific features can be tested: - [Cloud DNS forwarding zone](https://cloud.google.com/dns/docs/overview#fz-targets) to on-prem - DNS forwarding from on-prem via a [Cloud DNS inbound policy](https://cloud.google.com/dns/docs/policies#create-in) @@ -30,7 +30,7 @@ The Cloud DNS inbound policy reserves an IP address in the VPC, which is used by ### Find out the forwarder entry point address -Run this gcloud command to (find out the address assigned to the inbound forwarder)[https://cloud.google.com/dns/docs/policies#list-in-entrypoints]: +Run this gcloud command to [find out the address assigned to the inbound forwarder](https://cloud.google.com/dns/docs/policies#list-in-entrypoints): ```bash gcloud compute addresses list --project [your project id] @@ -199,7 +199,7 @@ curl www.onprem.example.org -s |grep h1 A single pre-existing project is used in this blueprint to keep variables and complexity to a minimum, in a real world scenarios each spoke would probably use a separate project. -The VPN-s used to connect to the on-premises environment do not account for HA, upgrading to use HA VPN is reasonably simple by using the relevant [module](../../../modules/net-vpn-ha). +The VPN-s used to connect to the on-premises environment do not account for HA, upgrading to use HA VPN is reasonably simple by using the relevant [module](../../../../modules/net-vpn-ha). ## Variables diff --git a/blueprints/networking/onprem-google-access-dns/assets/Corefile b/blueprints/networking/__need_fixing/onprem-google-access-dns/assets/Corefile similarity index 100% rename from blueprints/networking/onprem-google-access-dns/assets/Corefile rename to blueprints/networking/__need_fixing/onprem-google-access-dns/assets/Corefile diff --git a/blueprints/networking/onprem-google-access-dns/backend.tf.sample b/blueprints/networking/__need_fixing/onprem-google-access-dns/backend.tf.sample similarity index 100% rename from blueprints/networking/onprem-google-access-dns/backend.tf.sample rename to blueprints/networking/__need_fixing/onprem-google-access-dns/backend.tf.sample diff --git a/blueprints/networking/onprem-google-access-dns/diagram.png b/blueprints/networking/__need_fixing/onprem-google-access-dns/diagram.png similarity index 100% rename from blueprints/networking/onprem-google-access-dns/diagram.png rename to blueprints/networking/__need_fixing/onprem-google-access-dns/diagram.png diff --git a/blueprints/networking/onprem-google-access-dns/main.tf b/blueprints/networking/__need_fixing/onprem-google-access-dns/main.tf similarity index 100% rename from blueprints/networking/onprem-google-access-dns/main.tf rename to blueprints/networking/__need_fixing/onprem-google-access-dns/main.tf diff --git a/blueprints/networking/onprem-google-access-dns/outputs.tf b/blueprints/networking/__need_fixing/onprem-google-access-dns/outputs.tf similarity index 100% rename from blueprints/networking/onprem-google-access-dns/outputs.tf rename to blueprints/networking/__need_fixing/onprem-google-access-dns/outputs.tf diff --git a/blueprints/networking/onprem-google-access-dns/variables.tf b/blueprints/networking/__need_fixing/onprem-google-access-dns/variables.tf similarity index 100% rename from blueprints/networking/onprem-google-access-dns/variables.tf rename to blueprints/networking/__need_fixing/onprem-google-access-dns/variables.tf diff --git a/modules/cloud-config-container/onprem/versions.tf b/blueprints/networking/__need_fixing/onprem-google-access-dns/versions.tf similarity index 91% rename from modules/cloud-config-container/onprem/versions.tf rename to blueprints/networking/__need_fixing/onprem-google-access-dns/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/cloud-config-container/onprem/versions.tf +++ b/blueprints/networking/__need_fixing/onprem-google-access-dns/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/networking/decentralized-firewall/versions.tf b/blueprints/networking/decentralized-firewall/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/networking/decentralized-firewall/versions.tf +++ b/blueprints/networking/decentralized-firewall/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/networking/filtering-proxy-psc/versions.tf b/blueprints/networking/filtering-proxy-psc/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/networking/filtering-proxy-psc/versions.tf +++ b/blueprints/networking/filtering-proxy-psc/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/networking/filtering-proxy/versions.tf b/blueprints/networking/filtering-proxy/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/networking/filtering-proxy/versions.tf +++ b/blueprints/networking/filtering-proxy/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/networking/hub-and-spoke-peering/versions.tf b/blueprints/networking/hub-and-spoke-peering/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/networking/hub-and-spoke-peering/versions.tf +++ b/blueprints/networking/hub-and-spoke-peering/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/networking/hub-and-spoke-vpn/README.md b/blueprints/networking/hub-and-spoke-vpn/README.md index 4f580ed82..bdf877c73 100644 --- a/blueprints/networking/hub-and-spoke-vpn/README.md +++ b/blueprints/networking/hub-and-spoke-vpn/README.md @@ -7,7 +7,7 @@ A few additional features are also shown: - [custom BGP advertisements](https://cloud.google.com/router/docs/how-to/advertising-overview) to implement transitivity between spokes - [VPC Global Routing](https://cloud.google.com/network-connectivity/docs/router/how-to/configuring-routing-mode) to leverage a regional set of VPN gateways in different regions as next hops (used here for illustrative/study purpose, not usually done in real life) -The blueprint has been purposefully kept simple to show how to use and wire the VPC and VPN-HA modules together, and so that it can be used as a basis for experimentation. For a more complex scenario that better reflects real-life usage, including [Shared VPC](https://cloud.google.com/vpc/docs/shared-vpc) and [DNS cross-project binding](https://cloud.google.com/dns/docs/zones/cross-project-binding) please refer to the [FAST network stage](../../../fast/stages/02-networking-vpn/). +The blueprint has been purposefully kept simple to show how to use and wire the VPC and VPN-HA modules together, and so that it can be used as a basis for experimentation. For a more complex scenario that better reflects real-life usage, including [Shared VPC](https://cloud.google.com/vpc/docs/shared-vpc) and [DNS cross-project binding](https://cloud.google.com/dns/docs/zones/cross-project-binding) please refer to the [FAST network stage](../../../fast/stages/2-networking-b-vpn/). This is the high level diagram of this blueprint: @@ -35,12 +35,12 @@ You can easily create such a project by commenting turning on project creation i ```hcl module "project" { - source = "../../../modules/project" - name = var.project_id + source = "../../../modules/project" + name = var.project_id # comment or remove this line to enable project creation # project_create = false # add the following line with your billing account id value - billing_account = "12345-ABCD-12345" + billing_account = "12345-ABCD-12345" services = [ "compute.googleapis.com", "dns.googleapis.com" diff --git a/blueprints/networking/hub-and-spoke-vpn/versions.tf b/blueprints/networking/hub-and-spoke-vpn/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/networking/hub-and-spoke-vpn/versions.tf +++ b/blueprints/networking/hub-and-spoke-vpn/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/networking/ilb-next-hop/versions.tf b/blueprints/networking/ilb-next-hop/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/networking/ilb-next-hop/versions.tf +++ b/blueprints/networking/ilb-next-hop/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.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 286536a65..08492c6f9 100644 --- a/blueprints/networking/private-cloud-function-from-onprem/versions.tf +++ b/blueprints/networking/private-cloud-function-from-onprem/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/networking/shared-vpc-gke/main.tf b/blueprints/networking/shared-vpc-gke/main.tf index 2e770377f..97bf45d24 100644 --- a/blueprints/networking/shared-vpc-gke/main.tf +++ b/blueprints/networking/shared-vpc-gke/main.tf @@ -227,6 +227,7 @@ module "cluster-1-nodepool-1" { project_id = module.project-svc-gke.project_id location = module.cluster-1.0.location cluster_name = module.cluster-1.0.name + cluster_id = module.cluster-1.0.id service_account = { create = true } diff --git a/blueprints/networking/shared-vpc-gke/versions.tf b/blueprints/networking/shared-vpc-gke/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/networking/shared-vpc-gke/versions.tf +++ b/blueprints/networking/shared-vpc-gke/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/blueprints/serverless/cloud-run-explore/README.md b/blueprints/serverless/cloud-run-explore/README.md new file mode 100644 index 000000000..1002e817e --- /dev/null +++ b/blueprints/serverless/cloud-run-explore/README.md @@ -0,0 +1,214 @@ +# Cloud Run Explore + +## Introduction + +This blueprint contains all the necessary Terraform modules to build and publicly expose a Cloud Run service in a variety of use cases. + +The content of this blueprint corresponds to the chapter '_My serverless "Hello, World! - Exploring Cloud Run_' of the __Serverless Networking Guide__ (to be released soon). This guide is an easy to follow introduction to Cloud Run, where a couple of friendly characters will guide you from the basics to more advanced topics with a very practical approach and in record time! The code here complements this learning and allows you to test the scenarios presented and your knowledge. + +## Architecture + +The following diagram depicts the main components that this blueprint will set up: + +

+ +The following products or features are used to fulfill the different use cases covered in this blueprint (to learn more about them click on the hyperlinks): + +* [Cloud Run](https://cloud.google.com/run/docs/overview/what-is-cloud-run) - Cloud Run is a managed compute platform that lets you run containers directly on top of Google's scalable infrastructure. +* [Cloud Run Ingress Settings](https://cloud.google.com/run/docs/securing/ingress) - feature that restricts network access to your Cloud Run service. At a network level, by default, any resource on the Internet can reach your Cloud Run service on its run.app URL or at a custom domain set up in Cloud Run. You can change this default by specifying a different setting for its ingress. All ingress paths, including the default run.app URL, are subject to your ingress setting. Ingress is set at the service level. The following settings are available: + * __Internal__: Allows requests from VPC networks that are in the same project or VPC Service Controls perimeter as your Cloud Run service. + * __Internal and Cloud Load Balancing__: Allows requests from resources allowed by the more restrictive Internal setting and an External HTTP(S) load balancer. + * __All__ (default): Allows all requests, including requests directly from the Internet to the default run.app URL. +* [Google Cloud Load Balancer](https://cloud.google.com/run/docs/mapping-custom-domains#https-load-balancer) - When an HTTP(S) load balancer is enabled for Cloud Run, you can reach your serverless app through a custom domain mapped to a single dedicated global Anycast IP address that is not shared with other services. +* [Cloud Armor](https://cloud.google.com/armor) - Google Cloud Armor is the web-application firewall (WAF) and DDoS mitigation service that helps users defend their web apps and services at Google scale at the edge of Google’s network. +* [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. [External Load Balancing with IAP](https://cloud.google.com/iap/docs/load-balancer-howto) is supported for Cloud Run with Serverless NEGs. +* [Cloud CDN](https://cloud.google.com/cdn) - Configure fast, reliable web and video content delivery with global scale and reach. __Note__: Cloud CDN is not part of this blueprint yet. + +## Prerequisites + +You will need an existing [project](https://cloud.google.com/resource-manager/docs/creating-managing-projects) with [billing enabled](https://cloud.google.com/billing/docs/how-to/modify-project) and a user with the “Project owner” [IAM](https://cloud.google.com/iam) role on that project. __Note__: to grant a user a role, take a look at the [Granting and Revoking Access](https://cloud.google.com/iam/docs/granting-changing-revoking-access#grant-single-role) documentation. + +## Spinning up the architecture + +### General steps + +1. Clone the repo to your local machine or Cloud Shell: +```bash +git clone https://github.com/GoogleCloudPlatform/cloud-foundation-fabric +``` + +2. Change to the directory of the blueprint: +```bash +cd cloud-foundation-fabric/blueprints/serverless/cloud-run-explore +``` +You should see this README and some terraform files. + +3. To deploy a specific use case, you will need to create a file in this directory called `terraform.tfvars` and follow the corresponding instructions to set variables. Sometimes values that are meant to be substituted will be shown inside brackets but you need to omit these brackets. E.g.: +```tfvars +project_id = "[your-project_id]" +``` +may become +```tfvars +project_id = "spiritual-hour-331417" +``` + +Although each use case is somehow built around the previous one they are self-contained so you can deploy any of them at will. + +4. The usual terraform commands will do the work: +```bash +terraform init +terraform plan +terraform apply +``` + +The resource creation will take a few minutes but when it’s complete, you should see an output stating the command completed successfully with a list of the created resources, and some output variables with information to access your service. + +__Congratulations!__ You have successfully deployed the use case you chose based on the variables configuration. + +### Use case 1: Cloud Run service with default URL + +This is the simplest case, the "Hello World" for Cloud Run. A Cloud Run service is deployed with a default URL based in your project, service name and cloud region where it is deployed: + +

+ +In this case the only variable that you need to set in `terraform.tfvars` is the project ID: +```tfvars +project_id = "[your-project-id]" +``` +Alternatively you can pass this value on the command line: +```bash +terraform apply -var project_id="[your-project-id]" +``` + +The default URL is automatically created and shown as a terraform output variable. It will be similar to the one shown in the picture above. Now use your browser to visit it, you should see the following: + +

+ +### Use case 2: Cloud Run service with custom domain + +If you want to use your own custom domain you need a GCLB in front of your Cloud Run app: + +

+ +The following values will need to be set in `terraform.tfvars`, replacing the custom_domain value with your own domain: +```tfvars +project_id = "[your-project-id]" +custom_domain = "cloud-run-explore.example.org" +``` +Since it is an HTTPS connection a Google managed certificate is created, but for it to be provisioned correctly you will need to point to the load balancer IP address with an A DNS record at your registrar: [Use Google-managed SSL certificates | Load Balancing](https://cloud.google.com/load-balancing/docs/ssl-certificates/google-managed-certs#update-dns). The LB IP is shown as a terraform output. + +Be aware that in this case the Cloud Run service can also be reached through the default URL. To limit access only through the custom domain see the next use case. + +### Use case 3: Cloud Run service exposed only via custom domain + +To block access to the default URL, you can configure Ingress Settings so that Internet requests will be accepted only if they come through the Load Balancer: + +

+ +You only need to set one more value in the previous `terraform.tfvars` file: +```tfvars +project_id = "[your-project-id]" +custom_domain = "cloud-run-explore.example.org" +ingress_settings = "internal-and-cloud-load-balancing" +``` + +The default URL is still created but if you try to visit it, you should see a forbidden error: +

+ +### Use case 4: Cloud Run service protected by Cloud Armor + +To use Cloud Armor to protect the Cloud Run service, you need to create a security policy to enforce in the load balancer: +

+ +The code allows to block a list of IPs and a specific URL path. For example, you may want to block access to a login page to external users. To test its behavior, by default all IPs and the path `"/login.html"` are blocked, but you can override any of these settings with your own values: +```tfvars +project_id = "[your-project-id]" +custom_domain = "cloud-run-explore.example.org" +ingress_settings = "internal-and-cloud-load-balancing" +security_policy = { + enabled = true + ip_blacklist = ["79.149.0.0/16"] + path_blocked = "/admin.html" +} +``` + +Note that to avoid users to bypass the Cloud Armor policy you need to block access through the default URL. Ingress settings is configured to do that. + +### Use case 5: Cloud Run service protected by Cloud Armor and Identity-Aware Proxy + +You can enable IAP at the load balancer to control access using identity and context: +

+Use your own email as identity to access the Cloud Run service: + +```tfvars +project_id = "[your-project-id]" +custom_domain = "cloud-run-explore.example.org" +ingress_settings = "internal-and-cloud-load-balancing" +security_policy = { + enabled = true + ip_blacklist = ["79.149.0.0/16"] +} +iap = { + enabled = true + email = "user@example.org" +} +``` +When visiting it you may be redirected to login with Google. You can use an incognito window to test this behavior. + +## Cleaning up your environment + +The easiest way to remove all the deployed resources is to run the following command: +```bash +terraform destroy +``` +The above command will delete the associated resources so there will be no billable charges made afterwards. IAP Brands, though, can only be created once per project and not deleted. Destroying a Terraform-managed IAP Brand will remove it from state but will not delete it from Google Cloud. + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [project_id](variables.tf#L55) | Project ID. | string | ✓ | | +| [custom_domain](variables.tf#L17) | Custom domain for the Load Balancer. | string | | null | +| [iap](variables.tf#L23) | Identity-Aware Proxy for Cloud Run in the LB. | object({…}) | | {} | +| [image](variables.tf#L34) | Container image to deploy. | string | | "us-docker.pkg.dev/cloudrun/container/hello" | +| [ingress_settings](variables.tf#L40) | Ingress traffic sources allowed to call the service. | string | | "all" | +| [project_create](variables.tf#L46) | Parameters for the creation of a new project. | object({…}) | | null | +| [region](variables.tf#L60) | Cloud region where resource will be deployed. | string | | "europe-west1" | +| [run_svc_name](variables.tf#L66) | Cloud Run service name. | string | | "hello" | +| [security_policy](variables.tf#L72) | Security policy (Cloud Armor) to enforce in the LB. | object({…}) | | {} | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [custom_domain](outputs.tf#L19) | Custom domain for the Load Balancer. | | +| [default_URL](outputs.tf#L24) | Cloud Run service default URL. | | +| [load_balancer_ip](outputs.tf#L29) | LB IP that forwards to Cloud Run service. | | + + + +## Tests + +```hcl +module "test" { + source = "./fabric/blueprints/serverless/cloud-run-explore" + project_create = { + billing_account_id = "ABCDE-12345-ABCDE" + parent = "organizations/0123456789" + } + project_id = "myproject" + custom_domain = "cloud-run-explore.example.org" + ingress_settings = "internal-and-cloud-load-balancing" + security_policy = { + enabled = true + ip_blacklist = ["79.149.0.0/16"] + } + iap = { + enabled = true + email = "user@example.org" + } +} + +# tftest modules=4 resources=17 +``` \ No newline at end of file diff --git a/blueprints/serverless/cloud-run-explore/images/architecture.png b/blueprints/serverless/cloud-run-explore/images/architecture.png new file mode 100644 index 000000000..46e3a41df Binary files /dev/null and b/blueprints/serverless/cloud-run-explore/images/architecture.png differ diff --git a/blueprints/serverless/cloud-run-explore/images/forbidden.png b/blueprints/serverless/cloud-run-explore/images/forbidden.png new file mode 100644 index 000000000..67d313b85 Binary files /dev/null and b/blueprints/serverless/cloud-run-explore/images/forbidden.png differ diff --git a/blueprints/serverless/cloud-run-explore/images/service-running.png b/blueprints/serverless/cloud-run-explore/images/service-running.png new file mode 100644 index 000000000..2c517a0ac Binary files /dev/null and b/blueprints/serverless/cloud-run-explore/images/service-running.png differ diff --git a/blueprints/serverless/cloud-run-explore/images/use-case-1.png b/blueprints/serverless/cloud-run-explore/images/use-case-1.png new file mode 100644 index 000000000..8028c65da Binary files /dev/null and b/blueprints/serverless/cloud-run-explore/images/use-case-1.png differ diff --git a/blueprints/serverless/cloud-run-explore/images/use-case-2.png b/blueprints/serverless/cloud-run-explore/images/use-case-2.png new file mode 100644 index 000000000..a0fafb994 Binary files /dev/null and b/blueprints/serverless/cloud-run-explore/images/use-case-2.png differ diff --git a/blueprints/serverless/cloud-run-explore/images/use-case-3.png b/blueprints/serverless/cloud-run-explore/images/use-case-3.png new file mode 100644 index 000000000..af834a3d1 Binary files /dev/null and b/blueprints/serverless/cloud-run-explore/images/use-case-3.png differ diff --git a/blueprints/serverless/cloud-run-explore/images/use-case-4.png b/blueprints/serverless/cloud-run-explore/images/use-case-4.png new file mode 100644 index 000000000..113e9206e Binary files /dev/null and b/blueprints/serverless/cloud-run-explore/images/use-case-4.png differ diff --git a/blueprints/serverless/cloud-run-explore/images/use-case-5.png b/blueprints/serverless/cloud-run-explore/images/use-case-5.png new file mode 100644 index 000000000..d4f918782 Binary files /dev/null and b/blueprints/serverless/cloud-run-explore/images/use-case-5.png differ diff --git a/blueprints/serverless/cloud-run-explore/main.tf b/blueprints/serverless/cloud-run-explore/main.tf new file mode 100644 index 000000000..04bb3cc8b --- /dev/null +++ b/blueprints/serverless/cloud-run-explore/main.tf @@ -0,0 +1,187 @@ +/** + * 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 { + gclb_create = var.custom_domain == null ? false : true +} + +module "project" { + source = "../../../modules/project" + billing_account = (var.project_create != null + ? var.project_create.billing_account_id + : null + ) + parent = (var.project_create != null + ? var.project_create.parent + : null + ) + name = var.project_id + services = [ + "run.googleapis.com", + "compute.googleapis.com", + "iap.googleapis.com" + ] + project_create = var.project_create != null +} + +# Cloud Run service +module "cloud_run" { + source = "../../../modules/cloud-run" + project_id = module.project.project_id + name = var.run_svc_name + region = var.region + containers = [{ + image = var.image + options = null + ports = null + resources = null + volume_mounts = null + }] + iam = { + "roles/run.invoker" = ["allUsers"] + } + ingress_settings = var.ingress_settings +} + +# Reserved static IP for the Load Balancer +resource "google_compute_global_address" "default" { + count = local.gclb_create ? 1 : 0 + project = module.project.project_id + name = "glb-ip" +} + +# Global L7 HTTPS Load Balancer in front of Cloud Run +module "glb" { + source = "../../../modules/net-glb" + count = local.gclb_create ? 1 : 0 + project_id = module.project.project_id + name = "glb" + address = google_compute_global_address.default[0].address + backend_service_configs = { + default = { + backends = [ + { backend = "neg-0" } + ] + 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 = { + neg-0 = { + cloudrun = { + region = var.region + target_service = { + name = var.run_svc_name + } + } + } + } + protocol = "HTTPS" + ssl_certificates = { + managed_configs = { + default = { + domains = [var.custom_domain] + } + } + } +} + +# Cloud Armor configuration +resource "google_compute_security_policy" "policy" { + count = local.gclb_create && var.security_policy.enabled ? 1 : 0 + name = "cloud-run-policy" + project = module.project.project_id + 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.gclb_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.gclb_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.gclb_create && var.iap.enabled ? 1 : 0 + project = module.project.project_id + role = "roles/iap.httpsResourceAccessor" + member = "user:${var.iap.email}" +} diff --git a/blueprints/serverless/cloud-run-explore/outputs.tf b/blueprints/serverless/cloud-run-explore/outputs.tf new file mode 100644 index 000000000..2005b13d8 --- /dev/null +++ b/blueprints/serverless/cloud-run-explore/outputs.tf @@ -0,0 +1,32 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# Custom domain for the Load Balancer. I'd prefer getting the value from the +# SSL certificate but it is not exported as output +output "custom_domain" { + description = "Custom domain for the Load Balancer." + value = local.gclb_create ? var.custom_domain : "none" +} + +output "default_URL" { + description = "Cloud Run service default URL." + value = module.cloud_run.service.status[0].url +} + +output "load_balancer_ip" { + description = "LB IP that forwards to Cloud Run service." + value = local.gclb_create ? module.glb[0].address : "none" +} diff --git a/blueprints/serverless/cloud-run-explore/variables.tf b/blueprints/serverless/cloud-run-explore/variables.tf new file mode 100644 index 000000000..1c3b04a1a --- /dev/null +++ b/blueprints/serverless/cloud-run-explore/variables.tf @@ -0,0 +1,80 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "custom_domain" { + description = "Custom domain for the Load Balancer." + 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 = {} +} + +variable "image" { + description = "Container image to deploy." + type = string + default = "us-docker.pkg.dev/cloudrun/container/hello" +} + +variable "ingress_settings" { + description = "Ingress traffic sources allowed to call the service." + type = string + default = "all" +} + +variable "project_create" { + description = "Parameters for the creation of a new project." + type = object({ + billing_account_id = string + parent = string + }) + default = null +} + +variable "project_id" { + description = "Project ID." + type = string +} + +variable "region" { + description = "Cloud region where resource will be deployed." + type = string + default = "europe-west1" +} + +variable "run_svc_name" { + description = "Cloud Run service name." + type = string + default = "hello" +} + +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 = {} +} diff --git a/blueprints/third-party-solutions/openshift/tf/versions.tf b/blueprints/third-party-solutions/openshift/tf/versions.tf index 286536a65..08492c6f9 100644 --- a/blueprints/third-party-solutions/openshift/tf/versions.tf +++ b/blueprints/third-party-solutions/openshift/tf/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/default-versions.tf b/default-versions.tf index 286536a65..08492c6f9 100644 --- a/default-versions.tf +++ b/default-versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/diagram.svg b/diagram.svg new file mode 100644 index 000000000..689adf24e --- /dev/null +++ b/diagram.svg @@ -0,0 +1,293 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+ + + +
+ +
+
+
+
+ + + +
+ +
+
+
+
+ + + +
+ +
+
+
+
+
+ + + + + + + +
+ +
+
+ +
+ Organization +
+
+ +
+ tag value [tenant] +
+
+ +
+ IAM bindings() +
+
+ +
+ organization policies() +
+
+
+
+ + + + + + +
+ «folder» +
+
+ +
+ Tenant0 +
+
+ +
+ IAM bindings() +
+
+ +
+ organization policies() +
+
+ +
+ tag bindings() +
+
+
+
+ + + + + + +
+ «folder» +
+
+ +
+ Tenant1 +
+
+ +
+ IAM bindings() +
+
+ +
+ organization policies() +
+
+ +
+ tag bindings() +
+
+
+
+ + + + + + +
+ «project» +
+
+ +
+ Tenant0_IaC +
+
+ +
+ service accounts [all stages] +
+
+ +
+ storage buckets [stage 0+1] +
+
+ +
+ optional CI/CD [stage 0+1] +
+
+ +
+ IAM bindings() +
+
+
+
+ + + + + + +
+ «project» +
+
+ +
+ Tenant1_IaC +
+
+ +
+ service accounts [all stages] +
+
+ +
+ storage buckets [stage 0+1] +
+
+ +
+ optional CI/CD [stage 0+1] +
+
+ +
+ IAM bindings() +
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/fast/README.md b/fast/README.md index e35a48372..7459c870c 100644 --- a/fast/README.md +++ b/fast/README.md @@ -12,7 +12,7 @@ Fabric FAST was initially conceived to help enterprises quickly set up a GCP org ### Contracts and stages -FAST uses the concept of stages, which individually perform precise tasks but, taken together, build a functional, ready-to-use GCP organization. More importantly, stages are modeled around the security boundaries that typically appear in mature organizations. This arrangement allows delegating ownership of each stage to the team responsible for the types of resources it manages. For example, as its name suggests, the networking stage sets up all the networking elements and is usually the responsibility of a dedicated networking team within the organization. +FAST uses the concept of stages, which individually perform precise tasks but taken together build a functional, ready-to-use GCP organization. More importantly, stages are modeled around the security boundaries that typically appear in mature organizations. This arrangement allows delegating ownership of each stage to the team responsible for the types of resources it manages. For example, as its name suggests, the networking stage sets up all the networking elements and is usually the responsibility of a dedicated networking team within the organization. From the perspective of FAST's overall design, stages also work as contacts or interfaces, defining a set of pre-requisites and inputs required to perform their designed task and generating outputs needed by other stages lower in the chain. The diagram below shows the relationships between stages. @@ -20,7 +20,7 @@ From the perspective of FAST's overall design, stages also work as contacts or i Stages diagram

-Please refer to the [stages](./stages/) section for further details on each stage. +Please refer to the [stages](./stages/) section for further details on each stage. For details on tenant-level stages which introduce a deeper level of autonomy via nested FAST setups rooted in a top-level folder, refer to the [multitenant stages](#multitenant-organizations) section below. ### Security-first design @@ -32,11 +32,21 @@ FAST also aims to minimize the number of permissions granted to principals accor A resource factory consumes a simple representation of a resource (e.g., in YAML) and deploys it (e.g., using Terraform). Used correctly, factories can help decrease the management overhead of large-scale infrastructure deployments. See "[Resource Factories: A descriptive approach to Terraform](https://medium.com/google-cloud/resource-factories-a-descriptive-approach-to-terraform-581b3ebb59c)" for more details and the rationale behind factories. -FAST uses YAML-based factories to deploy subnets and firewall rules and, as its name suggests, in the [project factory](./stages/03-project-factory/) stage. +FAST uses YAML-based factories to deploy subnets and firewall rules and, as its name suggests, in the [project factory](./stages/3-project-factory/) stage. ### CI/CD -One of our objectives with FAST is to provide a lightweight reference design for the IaC repositories, and a built-in implementation for running our code in automated pipelines. Our CI/CD approach leverages [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation), and provides sample workflow configurations for several major providers. Refer to the [CI/CD section in the bootstrap stage](stages/00-bootstrap/README.md#cicd) for more details. We also provide separate [optional small stages](./extras/) to help you configure your CI/CD provider. +One of our objectives with FAST is to provide a lightweight reference design for the IaC repositories, and a built-in implementation for running our code in automated pipelines. Our CI/CD approach leverages [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation), and provides sample workflow configurations for several major providers. Refer to the [CI/CD section in the bootstrap stage](./stages/0-bootstrap/README.md#cicd) for more details. We also provide separate [optional small stages](./extras/) to help you configure your CI/CD provider. + +### Multitenant organizations + +FAST has built-in support for complex multitenant organizations, where each tenant has complete control over a separate hierarchy rooted in a top-level folder. This approach is particularly suited for large enterprises or governments, where country-level subsidiaries or government agencies have a wide degree of autonomy within a shared GCP organization managed by a central entity. + +FAST implements multitenancy via [dedicated stages](stages-multitenant) for tenant-level bootstrap and resource management, which configure separate hierarchies within the organization rooted in top-level folders, so that subsequent FAST stages (networking, security, data, etc.) can be used directly for each tenant. The diagram below shows the relationships between organization-level and tenant-level stages. + +

+ Stages diagram +

## Implementation @@ -57,9 +67,9 @@ Those familiar with Python will note that FAST follows many of the maxims in the ## Roadmap -Besides the features already described, FAST roadmap includes: +Besides the features already described, FAST also includes: - Stage to deploy environment-specific multitenant GKE clusters following Google's best practices - Stage to deploy a fully featured data platform -- Reference implementation to use FAST in CI/CD pipelines (in progress) -- Static policy enforcement +- Reference implementation to use FAST in CI/CD pipelines +- Static policy enforcement (planned) diff --git a/fast/extras/0-cicd-github/README.md b/fast/extras/0-cicd-github/README.md new file mode 100644 index 000000000..58407b5e4 --- /dev/null +++ b/fast/extras/0-cicd-github/README.md @@ -0,0 +1,139 @@ +# FAST GitHub repository management + +This small extra stage allows creating and populating GitHub repositories used to host FAST stage code, including rewriting of module sources and secrets used for private modules repository access. + +It is designed for use in a GitHub organization, and is only meant as a one-shot solution with perishable state especially when used for initial population, as you don't want Terraform to keep overwriting your changes with initial versions of files. + +Initial population is only meant to be used with actual stage, while populating the modules repository should be done by hand to avoid hitting the GitHub hourly limit for their API. + +Once initial population is done, you need to manually push to the repository + +- the `.tfvars` file with custom variable values for your stages +- the workflow configuration file generated by FAST stages + +## GitHub provider credentials + +A [GitHub token](https://github.com/settings/tokens) is needed to authenticate against their API. The token needs organization-level permissions, like shown in this screenshot: + +

+ GitHub token scopes. +

+ +Once a token is available set it in the `GITHUB_TOKEN` environment variable before running Terraform. + +## Variable configuration + +The `organization` required variable sets the GitHub organization where repositories will be created, and is used to configure the Terraform provider. + +### Modules repository and sources + +The `modules_config` variable controls creation and management of the key and secret used to access the private modules repository, and indirectly control population of initial files: if the `modules_config` variable is not specified no module repository is know to the code, so module source paths cannot be replaced, and initial population of files cannot happen. If the variable is specified, an optional `source_ref` attribute can be set to the reference used to pin modules versions. + +This is an example that configures the modules repository name and an optional reference, enabling initial population of repositories where the feature has been turned on: + +```hcl +modules_config = { + repository_name = "GoogleCloudPlatform/cloud-foundation-fabric" + source_ref = "v19.0.0" +} +# tftest skip +``` + +In the above example, no key options are set so it's assumed modules will be fetched from a public repository. If modules repository authentication is needed the `key_config` attribute also needs to be set. + +If no keypair path is specified an internally generated key will be stored as an access key in the modules repository, and as secrets in the stage repositories: + +```hcl +modules_config = { + repository_name = "GoogleCloudPlatform/cloud-foundation-fabric" + key_config = { + create_key = true + create_secrets = true + } +} +# tftest skip +``` + +To use an existing keypair pass the path to the private key, the public key name is assumed to have the same name ending with the `.pub` suffix. This is useful in cases where the access key has already been set in the modules repository, and new repositories need to be created and their corresponding secret set: + +```hcl +modules_config = { + repository_name = "GoogleCloudPlatform/cloud-foundation-fabric" + key_config = { + create_secrets = true + keypair_path = "~/modules-repository-key" + } +} +# tftest skip +``` + +### Repositories + +The `repositories` variable is where you configure which repositories to create and whether initial population of files is desired. + +This is an example that creates repositories for stages 00 and 01, and populates initial files for stages 00, 01, and 02: + +```tfvars +repositories = { + fast_00_bootstrap = { + create_options = { + description = "FAST bootstrap." + features = { + issues = true + } + } + populate_from = "../../stages/0-bootstrap" + } + fast_01_resman = { + create_options = { + description = "FAST resource management." + features = { + issues = true + } + } + populate_from = "../../stages/1-resman" + } + fast_02_networking = { + populate_from = "../../stages/2-networking-peering" + } +} +# tftest skip +``` + +The `create_options` repository attribute controls creation: if the attribute is not present, the repository is assumed to be already existing. + +Initial population depends on a modules repository being configured in the `modules_config` variable described in the preceding section and on the`populate_from` attributes in each repository where population is required, which point to the folder holding the files to be committed. + +### Commit configuration + +Finally, a `commit_config` variable is optional: it can be used to configure author, email and message used in commits for initial population of files, its defaults are probably fine for most use cases. + + + + +## Files + +| name | description | resources | +|---|---|---| +| [cicd-versions.tf](./cicd-versions.tf) | Provider version. | | +| [main.tf](./main.tf) | Module-level locals and resources. | github_actions_secret · github_repository · github_repository_deploy_key · github_repository_file · tls_private_key | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [providers.tf](./providers.tf) | Provider configuration. | | +| [variables.tf](./variables.tf) | Module variables. | | + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [organization](variables.tf#L50) | GitHub organization. | string | ✓ | | +| [commmit_config](variables.tf#L17) | Configure commit metadata. | object({…}) | | {} | +| [modules_config](variables.tf#L28) | Configure access to repository module via key, and replacement for modules sources in stage repositories. | object({…}) | | null | +| [repositories](variables.tf#L55) | Repositories to create. | map(object({…})) | | {} | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [clone](outputs.tf#L17) | Clone repository commands. | | + + diff --git a/fast/extras/00-cicd-github/cicd-versions.tf b/fast/extras/0-cicd-github/cicd-versions.tf similarity index 96% rename from fast/extras/00-cicd-github/cicd-versions.tf rename to fast/extras/0-cicd-github/cicd-versions.tf index 09f544cba..830f1e48a 100644 --- a/fast/extras/00-cicd-github/cicd-versions.tf +++ b/fast/extras/0-cicd-github/cicd-versions.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/fast/extras/00-cicd-github/github_token.png b/fast/extras/0-cicd-github/github_token.png similarity index 100% rename from fast/extras/00-cicd-github/github_token.png rename to fast/extras/0-cicd-github/github_token.png diff --git a/fast/extras/00-cicd-github/main.tf b/fast/extras/0-cicd-github/main.tf similarity index 72% rename from fast/extras/00-cicd-github/main.tf rename to fast/extras/0-cicd-github/main.tf index ac6028c17..d91ab970c 100644 --- a/fast/extras/00-cicd-github/main.tf +++ b/fast/extras/0-cicd-github/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,9 +15,6 @@ */ locals { - _modules_repository = [ - for k, v in var.repositories : local.repositories[k] if v.has_modules - ] _repository_files = flatten([ for k, v in var.repositories : [ for f in concat( @@ -30,12 +27,12 @@ locals { } ] if v.populate_from != null ]) - modules_ref = var.modules_ref == null ? "" : "?ref=${var.modules_ref}" - modules_repository = ( - length(local._modules_repository) > 0 - ? local._modules_repository.0 - : null + modules_ref = ( + try(var.modules_config.source_ref, null) == null + ? "" + : "?ref=${var.modules_config.source_ref}" ) + modules_repo = try(var.modules_config.repository_name, null) repositories = { for k, v in var.repositories : k => v.create_options == null ? k : github_repository.default[k].name @@ -56,6 +53,15 @@ locals { name = "templates/providers.tf.tpl" } if v.populate_from != null + }, + { + for k, v in var.repositories : + "${k}/templates/workflow-github.yaml" => { + repository = k + file = "../../assets/templates/workflow-github.yaml" + name = "templates/workflow-github.yaml" + } + if v.populate_from != null } ) } @@ -96,41 +102,49 @@ resource "github_repository" "default" { } resource "tls_private_key" "default" { - count = local.modules_repository != null ? 1 : 0 algorithm = "ED25519" } resource "github_repository_deploy_key" "default" { - count = local.modules_repository == null ? 0 : 1 + count = ( + try(var.modules_config.key_config.create_key, null) == true ? 1 : 0 + ) title = "Modules repository access" - repository = local.modules_repository - key = tls_private_key.default.0.public_key_openssh - read_only = true + repository = local.modules_repo + key = ( + try(var.modules_config.key_config.keypair_path, null) == null + ? tls_private_key.default.public_key_openssh + : file(pathexpand("${var.modules_config.key_config.keypair_path}.pub")) + ) + read_only = true } resource "github_actions_secret" "default" { - for_each = local.modules_repository == null ? {} : { - for k, v in local.repositories : - k => v if k != local.modules_repository - } - repository = each.key - secret_name = "CICD_MODULES_KEY" - plaintext_value = tls_private_key.default.0.private_key_openssh + for_each = ( + try(var.modules_config.key_config.create_secrets, null) == true + ? local.repositories + : {} + ) + repository = each.key + secret_name = "CICD_MODULES_KEY" + plaintext_value = ( + try(var.modules_config.key_config.keypair_path, null) == null + ? tls_private_key.default.private_key_openssh + : file(pathexpand("${var.modules_config.key_config.keypair_path}")) + ) } resource "github_repository_file" "default" { - for_each = ( - local.modules_repository == null ? {} : local.repository_files - ) + for_each = local.modules_repo == null ? {} : local.repository_files repository = local.repositories[each.value.repository] branch = "main" file = each.value.name content = ( - endswith(each.value.name, ".tf") && local.modules_repository != null + endswith(each.value.name, ".tf") && local.modules_repo != null ? replace( file(each.value.file), "/source\\s*=\\s*\"../../../modules/([^/\"]+)\"/", - "source = \"git@github.com:${var.organization}/${local.modules_repository}.git//$1${local.modules_ref}\"" # " + "source = \"git@github.com:${local.modules_repo}.git//$1${local.modules_ref}\"" # " ) : file(each.value.file) ) diff --git a/fast/extras/00-cicd-github/outputs.tf b/fast/extras/0-cicd-github/outputs.tf similarity index 96% rename from fast/extras/00-cicd-github/outputs.tf rename to fast/extras/0-cicd-github/outputs.tf index cb580e1fe..61b5ffbc7 100644 --- a/fast/extras/00-cicd-github/outputs.tf +++ b/fast/extras/0-cicd-github/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. diff --git a/fast/extras/00-cicd-github/providers.tf b/fast/extras/0-cicd-github/providers.tf similarity index 95% rename from fast/extras/00-cicd-github/providers.tf rename to fast/extras/0-cicd-github/providers.tf index 29be30ae9..a7ccb32d4 100644 --- a/fast/extras/00-cicd-github/providers.tf +++ b/fast/extras/0-cicd-github/providers.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/fast/extras/00-cicd-github/variables.tf b/fast/extras/0-cicd-github/variables.tf similarity index 75% rename from fast/extras/00-cicd-github/variables.tf rename to fast/extras/0-cicd-github/variables.tf index 0d9cb7fd6..8e5d0832f 100644 --- a/fast/extras/00-cicd-github/variables.tf +++ b/fast/extras/0-cicd-github/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. @@ -25,10 +25,26 @@ variable "commmit_config" { nullable = false } -variable "modules_ref" { - description = "Optional git ref used in module sources." - type = string - default = null +variable "modules_config" { + description = "Configure access to repository module via key, and replacement for modules sources in stage repositories." + type = object({ + repository_name = string + source_ref = optional(string) + key_config = optional(object({ + create_key = optional(bool, false) + create_secrets = optional(bool, false) + keypair_path = optional(string) + }), {}) + }) + default = null + validation { + condition = ( + var.modules_config == null + || + try(var.modules_config.repository_name, null) != null + ) + error_message = "Modules configuration requires a modules repository name." + } } variable "organization" { @@ -63,7 +79,6 @@ variable "repositories" { }), {}) visibility = optional(string, "private") })) - has_modules = optional(bool, false) populate_from = optional(string) })) default = {} diff --git a/fast/extras/00-cicd-github/README.md b/fast/extras/00-cicd-github/README.md deleted file mode 100644 index 52c322a33..000000000 --- a/fast/extras/00-cicd-github/README.md +++ /dev/null @@ -1,105 +0,0 @@ -# FAST GitHub repository management - -This small extra stage allows creation and management of GitHub repositories used to host FAST stage code, including initial population of files and rewriting of module sources. - -This stage is designed for quick repository creation in a GitHub organization, and is not suited for medium or long-term repository management especially if you enable initial population of files. - -## Initial population caveats - -Initial file population of repositories is controlled via the `populate_from` attribute, and needs a bit of care: - -- never run this stage with the same variables used for population once the repository starts being used, as **Terraform will manage file state and revert any changes at each apply**, which is probably not what you want. -- initial population of the modules repository is discouraged, as the number of resulting files Terraform needs to manage is very close to the GitHub hourly limit for their API, it's much easier to populate modules via regular git commands - -The scenario for which this stage has been designed is one-shot creation and/or population of stage repositories, running it multiple times with different variables and Terraform states if incremental creation is needed for subsequent FAST stages (e.g. GKE, data platform, etc.). - -Once initial population is done, you need to manually push to the repository - -- the `.tfvars` file with custom variable values for your stages -- the workflow configuration file generated by FAST stages - -## GitHub provider credentials - -A [GitHub token](https://github.com/settings/tokens) is needed to authenticate against their API. The token needs organization-level permissions, like shown in this screenshot: - -

- GitHub token scopes. -

- -## Variable configuration - -The `organization` required variable sets the GitHub organization where repositories will be created, and is used to configure the Terraform provider. - -The `repositories` variable is where you configure which repositories to create, whether initial population of files is desired, and which repository is used to host modules. - -This is an example that creates repositories for stages 00 and 01, defines an existing repositories as the source for modules, and populates initial files for stages 00, 01, and 02: - -```hcl -organization = "ludomagno" -repositories = { - fast_00_bootstrap = { - create_options = { - description = "FAST bootstrap." - features = { - issues = true - } - } - populate_from = "../../stages/00-bootstrap" - } - fast_01_resman = { - create_options = { - description = "FAST resource management." - features = { - issues = true - } - } - populate_from = "../../stages/01-resman" - } - fast_02_networking = { - populate_from = "../../stages/02-networking-peering" - } - fast_modules = { - has_modules = true - } -} -``` - -The `create_options` repository attribute controls creation: if the attribute is not present, the repository is assumed to be already existing. - -Initial population depends on a modules repository being configured, identified by the `has_modules` attribute, and on `populate_from` attributes in each repository where population is required, pointing to the folder holding the files to be committed. - -Finally, a `commit_config` variable is optional: it can be used to configure author, email and message used in commits for initial population of files, its defaults are probably fine for most use cases. - -## Modules secret - -When initial population is configured for a repository, this stage also adds a secret with the private key used to authenticate against the modules repository. This matches the configuration of the GitHub workflow files created for each FAST stage when CI/CD is enabled. - - - - -## Files - -| name | description | resources | -|---|---|---| -| [cicd-versions.tf](./cicd-versions.tf) | Provider version. | | -| [main.tf](./main.tf) | Module-level locals and resources. | github_actions_secret · github_repository · github_repository_deploy_key · github_repository_file · tls_private_key | -| [outputs.tf](./outputs.tf) | Module outputs. | | -| [providers.tf](./providers.tf) | Provider configuration. | | -| [variables.tf](./variables.tf) | Module variables. | | - -## Variables - -| name | description | type | required | default | -|---|---|:---:|:---:|:---:| -| [organization](variables.tf#L34) | GitHub organization. | string | ✓ | | -| [commmit_config](variables.tf#L17) | Configure commit metadata. | object({…}) | | {} | -| [modules_ref](variables.tf#L28) | Optional git ref used in module sources. | string | | null | -| [repositories](variables.tf#L39) | Repositories to create. | map(object({…})) | | {} | - -## Outputs - -| name | description | sensitive | -|---|---|:---:| -| [clone](outputs.tf#L17) | Clone repository commands. | | - - diff --git a/fast/extras/README.md b/fast/extras/README.md index 121fa4b04..9213224cd 100644 --- a/fast/extras/README.md +++ b/fast/extras/README.md @@ -2,4 +2,4 @@ This folder contains additional helper stages for FAST, which can be used to simplify specific operational tasks: -- [GitHub repository management](./00-cicd-github/) +- [GitHub repository management](./0-cicd-github/) diff --git a/fast/stage-links.sh b/fast/stage-links.sh new file mode 100755 index 000000000..52c9e5ae6 --- /dev/null +++ b/fast/stage-links.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# 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. + +if [ $# -eq 0 ]; then + echo "Error: no folder or GCS bucket specified. Use -h or --help for usage." + exit 1 +fi + +if [[ "$1" == "-h" || "$1" == "--help" ]]; then + cat < $MESSAGE <---" +fi diff --git a/fast/stages-multitenant/0-bootstrap-tenant/IAM.md b/fast/stages-multitenant/0-bootstrap-tenant/IAM.md new file mode 100644 index 000000000..543a82ab3 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/IAM.md @@ -0,0 +1,49 @@ +# IAM bindings reference + +Legend: + additive, conditional. + +## Organization [org_id #0] + +| members | roles | +|---|---| +|tn0-admins
group|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +
[roles/resourcemanager.organizationViewer](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.organizationViewer) +| +|tn0-gke-dev-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| +|tn0-gke-prod-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| +|tn0-networking-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| +|tn0-pf-dev-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| +|tn0-pf-prod-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| +|tn0-resman-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| +|tn0-sandbox-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| +|tn0-security-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| +|tn0-teams-0
serviceAccount|[roles/orgpolicy.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#orgpolicy.policyAdmin) +| + +## Folder test tenant 0 [#1] + +| members | roles | +|---|---| +|tn0-admins
group|[roles/compute.xpnAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.xpnAdmin)
[roles/logging.admin](https://cloud.google.com/iam/docs/understanding-roles#logging.admin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/resourcemanager.folderAdmin](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.folderAdmin)
[roles/resourcemanager.projectCreator](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.projectCreator) | +|tn0-networking-0
serviceAccount|[roles/compute.xpnAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.xpnAdmin) | +|tn0-resman-0
serviceAccount|[roles/compute.xpnAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.xpnAdmin)
[roles/logging.admin](https://cloud.google.com/iam/docs/understanding-roles#logging.admin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/resourcemanager.folderAdmin](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.folderAdmin)
[roles/resourcemanager.projectCreator](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.projectCreator) | + +## Project prod-iac-core-0 + +| members | roles | +|---|---| +|tn0-bootstrap-1
serviceAccount|[roles/logging.logWriter](https://cloud.google.com/iam/docs/understanding-roles#logging.logWriter) +| + +## Project tn0-audit-logs-0 + +| members | roles | +|---|---| +|f260055713332-284719
serviceAccount|[roles/logging.bucketWriter](https://cloud.google.com/iam/docs/understanding-roles#logging.bucketWriter) +| +|prod-resman-0
serviceAccount|[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner) | +|tn0-resman-0
serviceAccount|[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner) | + +## Project tn0-iac-core-0 + +| members | roles | +|---|---| +|tn0-admins
group|[roles/iam.serviceAccountTokenCreator](https://cloud.google.com/iam/docs/understanding-roles#iam.serviceAccountTokenCreator)
[roles/iam.workloadIdentityPoolAdmin](https://cloud.google.com/iam/docs/understanding-roles#iam.workloadIdentityPoolAdmin) | +|SERVICE_IDENTITY_service-networking
serviceAccount|[roles/servicenetworking.serviceAgent](https://cloud.google.com/iam/docs/understanding-roles#servicenetworking.serviceAgent) +| +|prod-resman-0
serviceAccount|[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner) | +|tn0-resman-0
serviceAccount|[roles/cloudbuild.builds.editor](https://cloud.google.com/iam/docs/understanding-roles#cloudbuild.builds.editor)
[roles/iam.serviceAccountAdmin](https://cloud.google.com/iam/docs/understanding-roles#iam.serviceAccountAdmin)
[roles/iam.workloadIdentityPoolAdmin](https://cloud.google.com/iam/docs/understanding-roles#iam.workloadIdentityPoolAdmin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/source.admin](https://cloud.google.com/iam/docs/understanding-roles#source.admin)
[roles/storage.admin](https://cloud.google.com/iam/docs/understanding-roles#storage.admin) | diff --git a/fast/stages-multitenant/0-bootstrap-tenant/README.md b/fast/stages-multitenant/0-bootstrap-tenant/README.md new file mode 100644 index 000000000..bbeaf9f69 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/README.md @@ -0,0 +1,210 @@ +# Tenant bootstrap + +The primary purpose of this stage is to decouple a single tenant from centrally managed resources in the organization, so that subsequent management of the tenant's own hierarchy and resources can be implemented with a high degree of autonomy. + +It is logically equivalent to organization-level bootstrap as it's concerned with setting up IAM bindings on a root node and creating supporting projects attached to it, but it depends on the organization-level resource management stage and uses the same service account and permissions since it operates at the hierarchy level (folders, tags, organization policies). + +The resources and policies managed here are: + +- the tag value in the `tenant` key used in IAM conditions +- the billing IAM bindings for the tenant-specific automation service accounts +- the organization-level IAM binding that allows conditional managing of org policies on the tenant folder +- the top-level tenant folder which acts as the root of the tenant's hierarchy +- any organization policy that needs to be set for the tenant on its root folder +- the tenant automation and logging projects +- service accounts for all tenant stages +- GCS buckets for bootstrap and resource management state +- optional CI/CD setup for this and the resource management tenant stages +- tenant-specific Workload Identity Federation pool and providers (planned) + +One notable difference compared to organization-level bootstrap is the creation of service accounts for all tenant stages: this is done here so that Billing and Organization Policy Admin bindings can be set, leveraging permissions of the org-level resman service account which is used to run this stage. Doing this here avoids the need to grant broad scoped permissions on the organization to tenant-level service accounts, and effectively decouples the tenant from the organization. + +The following diagram is a high level reference of what this stage manages, showing one hypothetical tenant (additional tenants require additional instances of this stage being deployed): + +```mermaid +%%{init: {'theme':'base'}}%% +classDiagram + Organization~🏢~ -- Tenant 0~📁~ + Tenant 0~📁~ -- tn0_automation + Tenant 0~📁~ -- tn0_logging + class Organization~🏢~ { + - tag value + - IAM bindings() + - org policies() + } + class Tenant 0~📁~ { + - log sinks + - IAM bindings() + - tag bindings() + } + class tn0_automation { + - GCS buckets + - service accounts + - optional CI/CD + - IAM bindings() + } + class tn0_logging { + - log sink destinations + } +``` + +As most of the features of this stage follow the same design and configurations of the [organization-level bootstrap stage](../../stages/0-bootstrap/), we will only focus on the tenant-specific configuration in this document. + +## Naming + +This stage sets the prefix used to name tenant resources, and passes it downstream to the other tenant stages together with the other globals needed by the tenant. The default is to append the tenant short name (a 3 or 4 letter acronym or abbreviation) to the organization-level prefix, if that is not desired this can be changed by editing local definitions in the `main.tf` file. Just be aware that some resources have name length constraints. + +## How to run this stage + +The tenant bootstrap stage is the effective boundary between organization and tenant-level resources: it uses the same inputs as the organization-level resource management stage, and produces outputs which provide the needed context to all other tenant stages. + +### Output files and cross-stage variables + +As mentioned above, the organization-level set of output files are used here with one exception: the provider file is different since state is specific to this stage. The `stage-links.sh` script can be used to get the commands needed for the provider and output files, just pass a single argument with your FAST output files folder path, or GCS bucket URI: + +```bash +../../stage-links.sh ~/fast-config +``` + +The script output can be copy/pasted to a terminal: + +```bash +# copy and paste the following commands for '0-bootstrap-tenant' + +cp ~/fast-config/providers/0-bootstrap-tenant-providers.tf ./ +ln -s ~/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json ./ + +# ---> remember to set the prefix in the provider file <--- +``` + +As shown in the script output above, the provider file is a template used as a source for potentially multiple tenant installations, so it needs to be specifically configured for this tenant by setting the backend `prefix` to a unique string so that the Terraform state file will not overlap with other tenants. Open it in an editor and perform the change before proceeding. + +### Global overrides + +The globals variable file linekd above contains definition which were set for the organization, for example the locations used for log sink destinations. These might not be correct for each tenant, so this stage allows overriding them via the tenant configuration variable described in the next section. + +### Tenant-level configuration + +The tenant configuration resides in the `tenant_config` variable, this is an example configuration for a tenant with comments explaining the different choices that need to be made: + +```hcl +tenant_config = { + # used for the top-level folder name + descriptive_name = "My First Tenant" + # tenant-specific groups, only the admin group is required + # the organization domain is automatically added after the group name + groups = { + gcp-admins = "tn01-admins" + # gcp-devops = "tn01-devops" + # gcp-network-admins = "tn01-networking" + # gcp-security-admins = "tn01-security" + } + # the 3 or 4 letter acronym or abbreviation used in resource names + short_name = "tn01" + # optional CI/CD configuration, refer to the org-level stages for information + # cicd = { + # branch = null + # identity_provider = "foo-provider" + # name = "myorg/tn01-bootstrap" + # type = "github" + # } + # optional group-level IAM bindings to add to the top-level folder + # group_iam = { + # tn01-support = ["roles/viewer"] + # } + # optional IAM bindings to add to the top-level folder + # iam = { + # "roles/logging.admin" = [ + # "serviceAccount:foo@myprj.iam.gserviceaccount.com" + # ] + # } + # optional location overrides to global locations + # locations = { + # bq = null + # gcs = null + # logging = null + # pubsub = null + # } + # optional folder ids for automation and logging project folders, typically + # added in later stages and entered here once created + # project_parent_ids = { + # automation = "folders/012345678" + # logging = "folders/0123456789" + # } +} +# tftest skip +``` + +Configure the tenant variable in a tfvars file for this stage. A few minor points worth noting: + +- the administrator group is the only one required here, specifying other groups only has the effect of populating the output file with group names for reuse in later stages +- the `iam` variable is merged with the IAM bindings for service accounts in the `main.tf` file, which take precedence; if a role specified in the variable is ignored, that's probably the case +- locations can be overridden at the attribute level, there's no need to specify those that are equal to the ones in the organization globals file + +### Running the stage + +Once the configuration is done just go through the usual `init/apply` cycle. On successful apply, a tfvars file specific for this tenant and a set of provider files will be created. + +### TODO + +- [ ] tenant-level Workload Identity Federation pool and providers configuration +- [ ] tenant-level logging project and sinks + + + + +## Files + +| name | description | modules | resources | +|---|---|---|---| +| [automation-sas.tf](./automation-sas.tf) | Tenant automation stage 2 and 3 service accounts. | iam-service-account | google_organization_iam_member | +| [automation.tf](./automation.tf) | Tenant automation project and resources. | gcs · iam-service-account · project | | +| [billing.tf](./billing.tf) | Billing roles for standalone billing accounts. | | google_billing_account_iam_member | +| [cicd.tf](./cicd.tf) | Workload Identity Federation configurations for CI/CD. | iam-service-account · source-repository | | +| [identity-providers.tf](./identity-providers.tf) | Workload Identity Federation provider definitions. | | google_iam_workload_identity_pool · google_iam_workload_identity_pool_provider | +| [log-export.tf](./log-export.tf) | Audit log project and sink. | bigquery-dataset · gcs · logging-bucket · project · pubsub | | +| [main.tf](./main.tf) | Module-level locals and resources. | folder | | +| [organization.tf](./organization.tf) | Organization tag and conditional IAM grant. | organization | google_organization_iam_member · google_tags_tag_value_iam_member | +| [outputs-files.tf](./outputs-files.tf) | Output files persistence to local filesystem. | | local_file | +| [outputs-gcs.tf](./outputs-gcs.tf) | Output files persistence to automation GCS bucket. | | google_storage_bucket_object | +| [outputs.tf](./outputs.tf) | Module outputs. | | | +| [variables.tf](./variables.tf) | Module variables. | | | + +## Variables + +| name | description | type | required | default | producer | +|---|---|:---:|:---:|:---:|:---:| +| [automation](variables.tf#L20) | Automation resources created by the organization-level bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | +| [billing_account](variables.tf#L38) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap | +| [organization](variables.tf#L194) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L210) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [tag_keys](variables.tf#L233) | Organization tag keys. | object({…}) | ✓ | | 1-resman | +| [tag_names](variables.tf#L244) | Customized names for resource management tags. | object({…}) | ✓ | | 1-resman | +| [tag_values](variables.tf#L255) | Organization resource management tag values. | map(string) | ✓ | | 1-resman | +| [tenant_config](variables.tf#L262) | Tenant configuration. Short name must be 4 characters or less. | object({…}) | ✓ | | | +| [cicd_repositories](variables.tf#L51) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | +| [custom_roles](variables.tf#L97) | Custom roles defined at the organization level, in key => id format. | object({…}) | | null | 0-bootstrap | +| [fast_features](variables.tf#L107) | Selective control for top-level FAST features. | object({…}) | | {} | 0-bootstrap | +| [federated_identity_providers](variables.tf#L121) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…})) | | {} | | +| [group_iam](variables.tf#L135) | Tenant-level custom group IAM settings in group => [roles] format. | map(list(string)) | | {} | | +| [iam](variables.tf#L141) | Tenant-level custom IAM settings in role => [principal] format. | map(list(string)) | | {} | | +| [iam_additive](variables.tf#L147) | Tenant-level custom IAM settings in role => [principal] format for non-authoritative bindings. | map(list(string)) | | {} | | +| [locations](variables.tf#L153) | Optional locations for GCS, BigQuery, and logging buckets created here. These are the defaults set at the organization level, and can be overridden via the tenant config variable. | object({…}) | | {…} | 0-bootstrap | +| [log_sinks](variables.tf#L173) | Tenant-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | | +| [outputs_location](variables.tf#L204) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | +| [project_parent_ids](variables.tf#L220) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the tenant folder as parent. | object({…}) | | {…} | | +| [test_principal](variables.tf#L302) | Used when testing to bypass the data source returning the current identity. | string | | null | | + +## Outputs + +| name | description | sensitive | consumers | +|---|---|:---:|---| +| [cicd_workflows](outputs.tf#L102) | CI/CD workflows for tenant bootstrap and resource management stages. | ✓ | | +| [federated_identity](outputs.tf#L108) | Workload Identity Federation pool and providers. | | | +| [provider](outputs.tf#L118) | Terraform provider file for tenant resource management stage. | ✓ | stage-01 | +| [tenant_resources](outputs.tf#L125) | Tenant-level resources. | | | +| [tfvars](outputs.tf#L136) | Terraform variable files for the following tenant stages. | ✓ | | + + diff --git a/fast/stages-multitenant/0-bootstrap-tenant/automation-sas.tf b/fast/stages-multitenant/0-bootstrap-tenant/automation-sas.tf new file mode 100644 index 000000000..cae14093e --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/automation-sas.tf @@ -0,0 +1,135 @@ +/** + * 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. + */ + +# tfdoc:file:description Tenant automation stage 2 and 3 service accounts. + +locals { + branch_sas = { + dp-dev = { + condition = join(" && ", [ + "resource.matchTag('${local.tag_keys.context}', 'data')", + "resource.matchTag('${local.tag_keys.environment}', 'development')" + ]) + description = "data platform dev" + flag = "data_platform" + } + dp-prod = { + condition = join(" && ", [ + "resource.matchTag('${local.tag_keys.context}', 'data')", + "resource.matchTag('${local.tag_keys.environment}', 'production')" + ]) + description = "data platform prod" + flag = "data_platform" + } + gke-dev = { + condition = join(" && ", [ + "resource.matchTag('${local.tag_keys.context}', 'gke')", + "resource.matchTag('${local.tag_keys.environment}', 'development')" + ]) + description = "GKE dev" + flag = "gke" + } + gke-prod = { + condition = join(" && ", [ + "resource.matchTag('${local.tag_keys.context}', 'gke')", + "resource.matchTag('${local.tag_keys.environment}', 'production')" + ]) + description = "GKE prod" + flag = "gke" + } + networking = { + condition = "resource.matchTag('${local.tag_keys.context}', 'networking')" + description = "networking" + flag = "-" + } + pf-dev = { + condition = "resource.matchTag('${local.tag_keys.environment}', 'development')" + description = "project factory dev" + flag = "project_factory" + } + pf-prod = { + condition = "resource.matchTag('${local.tag_keys.environment}', 'production')" + description = "project factory prod" + flag = "project_factory" + } + sandbox = { + condition = "resource.matchTag('${local.tag_keys.context}', 'sandbox')" + description = "sandbox" + flag = "sandbox" + } + security = { + condition = "resource.matchTag('${local.tag_keys.context}', 'security')" + description = "security" + flag = "-" + } + teams = { + condition = "resource.matchTag('${local.tag_keys.context}', 'teams')" + description = "teams" + flag = "teams" + } + } +} + +module "automation-tf-resman-sa-stage2-3" { + source = "../../../modules/iam-service-account" + for_each = { + for k, v in local.branch_sas : + k => v if lookup(local.fast_features, v.flag, true) + } + project_id = module.automation-project.project_id + name = "${each.key}-0" + display_name = "Terraform ${each.value.description} service account." + prefix = local.prefix + iam_billing_roles = !var.billing_account.is_org_level ? { + (var.billing_account.id) = [ + "roles/billing.user", "roles/billing.costsManager" + ] + } : {} + iam_organization_roles = var.billing_account.is_org_level ? { + (var.organization.id) = [ + "roles/billing.user", "roles/billing.costsManager" + ] + } : {} +} + +# assign org policy admin with a tag-based condition to stage 2 and 3 SAs + +resource "google_organization_iam_member" "org_policy_admin_stage2_3" { + for_each = { + for k, v in module.automation-tf-resman-sa-stage2-3 : k => v.iam_email + } + org_id = var.organization.id + role = "roles/orgpolicy.policyAdmin" + member = each.value + condition { + title = "org_policy_tag_${var.tenant_config.short_name}_${each.key}_scoped" + description = join("", [ + "Org policy tag scoped grant for tenant ${var.tenant_config.short_name} ", + local.branch_sas[each.key].description + ]) + expression = join(" && ", [ + local.iam_tenant_condition, local.branch_sas[each.key].condition + ]) + } +} + +# assign custom tenant network admin role to networking SA + +resource "google_organization_iam_member" "tenant_network_admin" { + org_id = var.organization.id + role = var.custom_roles.tenant_network_admin + member = module.automation-tf-resman-sa-stage2-3["networking"].iam_email +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/automation.tf b/fast/stages-multitenant/0-bootstrap-tenant/automation.tf new file mode 100644 index 000000000..9684e7ca3 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/automation.tf @@ -0,0 +1,141 @@ +/** + * 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. + */ + +# tfdoc:file:description Tenant automation project and resources. + +module "automation-project" { + source = "../../../modules/project" + billing_account = var.billing_account.id + name = "iac-core-0" + parent = coalesce( + var.project_parent_ids.automation, + module.tenant-folder.id + ) + prefix = local.prefix + # human (groups) IAM bindings + group_iam = { + (local.groups.gcp-admins) = [ + "roles/iam.serviceAccountAdmin", + "roles/iam.serviceAccountTokenCreator", + ] + (local.groups.gcp-admins) = [ + "roles/iam.serviceAccountTokenCreator", + "roles/iam.workloadIdentityPoolAdmin" + ] + } + # machine (service accounts) IAM bindings + iam = { + "roles/owner" = [ + module.automation-tf-resman-sa.iam_email, + "serviceAccount:${local.resman_sa}" + ] + "roles/cloudbuild.builds.editor" = [ + module.automation-tf-resman-sa.iam_email + ] + "roles/iam.serviceAccountAdmin" = [ + module.automation-tf-resman-sa.iam_email + ] + "roles/iam.workloadIdentityPoolAdmin" = [ + module.automation-tf-resman-sa.iam_email + ] + "roles/source.admin" = [ + module.automation-tf-resman-sa.iam_email + ] + "roles/storage.admin" = [ + module.automation-tf-resman-sa.iam_email + ] + } + services = [ + "accesscontextmanager.googleapis.com", + "bigquery.googleapis.com", + "bigqueryreservation.googleapis.com", + "bigquerystorage.googleapis.com", + "billingbudgets.googleapis.com", + "cloudbilling.googleapis.com", + "cloudbuild.googleapis.com", + "cloudkms.googleapis.com", + "cloudresourcemanager.googleapis.com", + "container.googleapis.com", + "compute.googleapis.com", + "container.googleapis.com", + "essentialcontacts.googleapis.com", + "iam.googleapis.com", + "iamcredentials.googleapis.com", + "orgpolicy.googleapis.com", + "pubsub.googleapis.com", + "servicenetworking.googleapis.com", + "serviceusage.googleapis.com", + "sourcerepo.googleapis.com", + "stackdriver.googleapis.com", + "storage-component.googleapis.com", + "storage.googleapis.com", + "sts.googleapis.com" + ] +} + +# output files bucket + +module "automation-tf-output-gcs" { + source = "../../../modules/gcs" + project_id = module.automation-project.project_id + name = "iac-core-outputs-0" + prefix = local.prefix + location = local.locations.gcs + storage_class = local.gcs_storage_class + versioning = true +} + +# resource management stage bucket and service account + +module "automation-tf-resman-gcs" { + source = "../../../modules/gcs" + project_id = module.automation-project.project_id + name = "iac-core-resman-0" + prefix = local.prefix + location = local.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [module.automation-tf-resman-sa.iam_email] + } +} + +module "automation-tf-resman-sa" { + source = "../../../modules/iam-service-account" + project_id = module.automation-project.project_id + name = "resman-0" + display_name = "Terraform stage 1 resman service account." + prefix = local.prefix + # allow SA used by CI/CD workflow to impersonate this SA + iam = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.automation-tf-cicd-sa-resman["0"].iam_email, null) + ]) + } + iam_billing_roles = !var.billing_account.is_org_level ? { + (var.billing_account.id) = [ + "roles/billing.admin", "roles/billing.costsManager" + ] + } : {} + iam_organization_roles = var.billing_account.is_org_level ? { + (var.organization.id) = [ + "roles/billing.admin", "roles/billing.costsManager" + ] + } : {} + iam_storage_roles = { + (module.automation-tf-output-gcs.name) = ["roles/storage.admin"] + } +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/billing.tf b/fast/stages-multitenant/0-bootstrap-tenant/billing.tf new file mode 100644 index 000000000..77c26b919 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/billing.tf @@ -0,0 +1,39 @@ +/** + * 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. + */ + +# tfdoc:file:description Billing roles for standalone billing accounts. + +# service account billing roles are in the SA module in automation.tf + +resource "google_billing_account_iam_member" "billing_ext_admin" { + for_each = toset(var.billing_account.is_org_level ? [] : [ + "group:${local.groups.gcp-admins}", + module.automation-tf-resman-sa.iam_email + ]) + billing_account_id = var.billing_account.id + role = "roles/billing.admin" + member = each.key +} + +resource "google_billing_account_iam_member" "billing_ext_cost_manager" { + for_each = toset(var.billing_account.is_org_level ? [] : [ + "group:${local.groups.gcp-admins}", + module.automation-tf-resman-sa.iam_email + ]) + billing_account_id = var.billing_account.id + role = "roles/billing.costsManager" + member = each.key +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/cicd.tf b/fast/stages-multitenant/0-bootstrap-tenant/cicd.tf new file mode 100644 index 000000000..a25215af2 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/cicd.tf @@ -0,0 +1,223 @@ +/** + * 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. + */ + +# tfdoc:file:description Workload Identity Federation configurations for CI/CD. + +locals { + _file_prefix = "tenants/${var.tenant_config.short_name}" + # derive identity pool names from identity providers for easy reference + cicd_identity_pools = { + for k, v in local.cicd_identity_providers : + k => split("/providers/", v.name)[0] + } + # merge org-level and tenant-level identity providers + cicd_identity_providers = merge( + var.automation.federated_identity_providers, + { + for k, v in google_iam_workload_identity_pool_provider.default : + k => { + issuer = local.identity_providers[k].issuer + issuer_uri = local.identity_providers[k].issuer_uri + name = v.name + principal_tpl = local.identity_providers[k].principal_tpl + principalset_tpl = local.identity_providers[k].principalset_tpl + } + }) + # filter CI/CD repositories to only keep valid ones + cicd_repositories = { + for k, v in coalesce(var.cicd_repositories, {}) : k => v + if( + v != null + && + ( + try(v.type, null) == "sourcerepo" + || + contains( + keys(local.cicd_identity_providers), + coalesce(try(v.identity_provider, null), ":") + ) + ) + && + fileexists( + format("${path.module}/templates/workflow-%s.yaml", try(v.type, "")) + ) + ) + } +} + +# tenant bootstrap runs in the org scope and uses top-level automation project + +module "automation-tf-cicd-repo-bootstrap" { + source = "../../../modules/source-repository" + for_each = { + for k, v in local.cicd_repositories : 0 => v + if k == "bootstrap" && try(v.type, null) == "sourcerepo" + } + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = [ + local.resman_sa + ] + "roles/source.reader" = [ + module.automation-tf-cicd-sa-bootstrap["0"].iam_email + ] + } + triggers = { + "fast-${var.tenant_config.short_name}-0-bootstrap" = { + filename = ".cloudbuild/workflow.yaml" + included_files = ["**/*tf", ".cloudbuild/workflow.yaml"] + service_account = module.automation-tf-cicd-sa-bootstrap["0"].id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } +} + +module "automation-tf-cicd-sa-bootstrap" { + source = "../../../modules/iam-service-account" + for_each = { + for k, v in local.cicd_repositories : 0 => v + if k == "bootstrap" && try(v.type, null) != null + } + project_id = var.automation.project_id + name = "bootstrap-1" + display_name = "Terraform CI/CD ${var.tenant_config.short_name} bootstrap." + prefix = local.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? {} + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} + +module "automation-tf-org-resman-sa" { + source = "../../../modules/iam-service-account" + for_each = { + for k, v in local.cicd_repositories : 0 => v + if k == "bootstrap" && try(v.type, null) != null + } + project_id = var.automation.project_id + name = local.resman_sa + service_account_create = false + iam_additive = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.automation-tf-cicd-sa-bootstrap["0"].iam_email, null) + ]) + } +} + +# tenant resman runs in the tenant scope and uses its own automation project + +module "automation-tf-cicd-repo-resman" { + source = "../../../modules/source-repository" + for_each = { + for k, v in local.cicd_repositories : 0 => v + if k == "resman" && try(v.type, null) == "sourcerepo" + } + project_id = module.automation-project.project_id + name = each.value.name + iam = { + "roles/source.admin" = [ + module.automation-tf-resman-sa.iam_email + ] + "roles/source.reader" = [ + module.automation-tf-cicd-sa-resman["0"].iam_email + ] + } + triggers = { + fast-1-resman = { + filename = ".cloudbuild/workflow.yaml" + included_files = ["**/*tf", ".cloudbuild/workflow.yaml"] + service_account = module.automation-tf-cicd-sa-resman["0"].id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } +} + +module "automation-tf-cicd-sa-resman" { + source = "../../../modules/iam-service-account" + for_each = { + for k, v in local.cicd_repositories : 0 => v + if k == "resman" && try(v.type, null) != null + } + project_id = module.automation-project.project_id + name = "resman-1" + display_name = "Terraform CI/CD resman." + prefix = local.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? {} + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (module.automation-project.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (module.automation-tf-output-gcs.name) = ["roles/storage.objectViewer"] + } +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/diagram.svg b/fast/stages-multitenant/0-bootstrap-tenant/diagram.svg new file mode 100644 index 000000000..4090c7b09 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/diagram.svg @@ -0,0 +1,597 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fast/stages-multitenant/0-bootstrap-tenant/identity-providers.tf b/fast/stages-multitenant/0-bootstrap-tenant/identity-providers.tf new file mode 100644 index 000000000..3f8499b75 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/identity-providers.tf @@ -0,0 +1,96 @@ +/** + * 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. + */ + +# tfdoc:file:description Workload Identity Federation provider definitions. + +locals { + identity_providers = { + for k, v in var.federated_identity_providers : k => merge( + v, + lookup(local.identity_providers_defs, v.issuer, {}) + ) + } + identity_providers_defs = { + # https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect + github = { + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.sub" = "assertion.sub" + "attribute.actor" = "assertion.actor" + "attribute.repository" = "assertion.repository" + "attribute.repository_owner" = "assertion.repository_owner" + "attribute.ref" = "assertion.ref" + } + issuer_uri = "https://token.actions.githubusercontent.com" + principal_tpl = "principal://iam.googleapis.com/%s/subject/repo:%s:ref:refs/heads/%s" + principalset_tpl = "principalSet://iam.googleapis.com/%s/attribute.repository/%s" + } + # https://docs.gitlab.com/ee/ci/cloud_services/index.html#how-it-works + gitlab = { + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.sub" = "assertion.sub" + "attribute.environment" = "assertion.environment" + "attribute.environment_protected" = "assertion.environment_protected" + "attribute.namespace_id" = "assertion.namespace_id" + "attribute.namespace_path" = "assertion.namespace_path" + "attribute.pipeline_id" = "assertion.pipeline_id" + "attribute.pipeline_source" = "assertion.pipeline_source" + "attribute.project_id" = "assertion.project_id" + "attribute.project_path" = "assertion.project_path" + "attribute.repository" = "assertion.project_path" + "attribute.ref" = "assertion.ref" + "attribute.ref_protected" = "assertion.ref_protected" + "attribute.ref_type" = "assertion.ref_type" + } + allowed_audiences = ["https://gitlab.com"] + issuer_uri = "https://gitlab.com" + principal_tpl = "principalSet://iam.googleapis.com/%s/attribute.sub/project_path:%s:ref_type:branch:ref:%s" + principalset_tpl = "principalSet://iam.googleapis.com/%s/attribute.repository/%s" + } + } +} + +resource "google_iam_workload_identity_pool" "default" { + provider = google-beta + count = length(local.identity_providers) > 0 ? 1 : 0 + project = module.automation-project.project_id + workload_identity_pool_id = "${var.prefix}-bootstrap" +} + +resource "google_iam_workload_identity_pool_provider" "default" { + provider = google-beta + for_each = local.identity_providers + project = module.automation-project.project_id + workload_identity_pool_id = ( + google_iam_workload_identity_pool.default.0.workload_identity_pool_id + ) + workload_identity_pool_provider_id = "${var.prefix}-bootstrap-${each.key}" + attribute_condition = each.value.attribute_condition + attribute_mapping = each.value.attribute_mapping + oidc { + allowed_audiences = ( + try(each.value.custom_settings.allowed_audiences, null) != null + ? each.value.custom_settings.allowed_audiences + : try(each.value.allowed_audiences, null) + ) + issuer_uri = ( + try(each.value.custom_settings.issuer_uri, null) != null + ? each.value.custom_settings.issuer_uri + : try(each.value.issuer_uri, null) + ) + } +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/log-export.tf b/fast/stages-multitenant/0-bootstrap-tenant/log-export.tf new file mode 100644 index 000000000..b0bf115a2 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/log-export.tf @@ -0,0 +1,94 @@ +/** + * 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. + */ + +# tfdoc:file:description Audit log project and sink. + +locals { + log_sink_destinations = merge( + # use the same dataset for all sinks with `bigquery` as destination + { for k, v in var.log_sinks : k => module.log-export-dataset.0 if v.type == "bigquery" }, + # use the same gcs bucket for all sinks with `storage` as destination + { for k, v in var.log_sinks : k => module.log-export-gcs.0 if v.type == "storage" }, + # use separate pubsub topics and logging buckets for sinks with + # destination `pubsub` and `logging` + module.log-export-pubsub, + module.log-export-logbucket + ) + log_types = toset([for k, v in var.log_sinks : v.type]) +} + +module "log-export-project" { + source = "../../../modules/project" + billing_account = var.billing_account.id + name = "audit-logs-0" + parent = coalesce( + var.project_parent_ids.logging, + module.tenant-folder.id + ) + prefix = local.prefix + iam = { + "roles/owner" = [ + module.automation-tf-resman-sa.iam_email, + "serviceAccount:${local.resman_sa}" + ] + } + services = [ + # "cloudresourcemanager.googleapis.com", + # "iam.googleapis.com", + # "serviceusage.googleapis.com", + "bigquery.googleapis.com", + "storage.googleapis.com", + "stackdriver.googleapis.com" + ] +} + +# one log export per type, with conditionals to skip those not needed + +module "log-export-dataset" { + source = "../../../modules/bigquery-dataset" + count = contains(local.log_types, "bigquery") ? 1 : 0 + project_id = module.log-export-project.project_id + id = "audit_export" + friendly_name = "Audit logs export." + location = var.locations.bq +} + +module "log-export-gcs" { + source = "../../../modules/gcs" + count = contains(local.log_types, "storage") ? 1 : 0 + project_id = module.log-export-project.project_id + name = "audit-logs-0" + prefix = local.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class +} + +module "log-export-logbucket" { + source = "../../../modules/logging-bucket" + for_each = toset([for k, v in var.log_sinks : k if v.type == "logging"]) + parent_type = "project" + parent = module.log-export-project.project_id + id = "audit-logs-${each.key}" + location = var.locations.logging +} + +module "log-export-pubsub" { + source = "../../../modules/pubsub" + for_each = toset([for k, v in var.log_sinks : k if v.type == "pubsub"]) + project_id = module.log-export-project.project_id + name = "audit-logs-${each.key}" + regions = var.locations.pubsub +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/main.tf b/fast/stages-multitenant/0-bootstrap-tenant/main.tf new file mode 100644 index 000000000..3a1505949 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/main.tf @@ -0,0 +1,100 @@ +/** + * 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 { + gcs_storage_class = ( + length(split("-", local.locations.gcs)) < 2 + ? "MULTI_REGIONAL" + : "REGIONAL" + ) + groups = { + for k, v in var.tenant_config.groups : + k => v == null ? null : "${v}@${var.organization.domain}" + } + fast_features = { + for k, v in var.tenant_config.fast_features : + k => v == null ? var.fast_features[k] : v + } + locations = { + for k, v in var.tenant_config.locations : + k => v == null || v == [] ? var.locations[k] : v + } + prefix = join("-", compact([var.prefix, var.tenant_config.short_name])) + resman_sa = ( + var.test_principal == null + ? data.google_client_openid_userinfo.resman-sa.0.email + : var.test_principal + ) +} + +data "google_client_openid_userinfo" "resman-sa" { + count = var.test_principal == null ? 1 : 0 +} + +module "tenant-folder" { + source = "../../../modules/folder" + parent = "organizations/${var.organization.id}" + name = var.tenant_config.descriptive_name + logging_sinks = { + for name, attrs in var.log_sinks : name => { + bq_partitioned_table = attrs.type == "bigquery" + destination = local.log_sink_destinations[name].id + filter = attrs.filter + type = attrs.type + } + } + tag_bindings = { + tenant = try( + module.organization.tag_values["${var.tag_names.tenant}/${var.tenant_config.short_name}"].id, + null + ) + } +} + +module "tenant-folder-iam" { + source = "../../../modules/folder" + id = module.tenant-folder.id + folder_create = false + group_iam = merge(var.group_iam, { + (local.groups.gcp-admins) = [ + "roles/logging.admin", + "roles/owner", + "roles/resourcemanager.folderAdmin", + "roles/resourcemanager.projectCreator", + "roles/compute.xpnAdmin" + ] + }) + iam = merge(var.iam, { + "roles/compute.xpnAdmin" = [ + module.automation-tf-resman-sa.iam_email, + module.automation-tf-resman-sa-stage2-3["networking"].iam_email + ] + "roles/logging.admin" = [ + module.automation-tf-resman-sa.iam_email + ] + "roles/resourcemanager.folderAdmin" = [ + module.automation-tf-resman-sa.iam_email + ] + "roles/resourcemanager.projectCreator" = [ + module.automation-tf-resman-sa.iam_email + ] + "roles/owner" = [ + module.automation-tf-resman-sa.iam_email + ] + }) + iam_additive = var.iam_additive + depends_on = [module.automation-project] +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/organization.tf b/fast/stages-multitenant/0-bootstrap-tenant/organization.tf new file mode 100644 index 000000000..46f8c0d4f --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/organization.tf @@ -0,0 +1,84 @@ +/** + * 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. + */ + +# tfdoc:file:description Organization tag and conditional IAM grant. + +locals { + iam_tenant_condition = "resource.matchTag('${local.tag_keys.tenant}', '${var.tenant_config.short_name}')" + tag_keys = { + for k, v in var.tag_names : k => "${var.organization.id}/${v}" + } +} + +module "organization" { + source = "../../../modules/organization" + organization_id = "organizations/${var.organization.id}" + iam_additive = merge( + { + "roles/resourcemanager.organizationViewer" = [ + "group:${local.groups.gcp-admins}" + ] + }, + var.billing_account.is_org_level ? { + "roles/billing.admin" = [ + "group:${local.groups.gcp-admins}", + module.automation-tf-resman-sa.iam_email + ] + "roles/billing.costsManager" = ["group:${local.groups.gcp-admins}"] + } : {} + ) + tags = { + tenant = { + id = var.tag_keys.tenant + values = { + (var.tenant_config.short_name) = {} + } + } + } +} + +resource "google_tags_tag_value_iam_member" "resman_tag_user" { + for_each = var.tag_values + tag_value = each.value + role = "roles/resourcemanager.tagUser" + member = module.automation-tf-resman-sa.iam_email +} + +resource "google_tags_tag_value_iam_member" "admins_tag_viewer" { + for_each = var.tag_values + tag_value = each.value + role = "roles/resourcemanager.tagViewer" + member = "group:${local.groups.gcp-admins}" +} + +# assign org policy admin with a tag-based condition to admin group and stage 1 SA + +resource "google_organization_iam_member" "org_policy_admin_stage0" { + for_each = toset([ + "group:${local.groups.gcp-admins}", + module.automation-tf-resman-sa.iam_email + ]) + org_id = var.organization.id + role = "roles/orgpolicy.policyAdmin" + member = each.key + condition { + title = "org_policy_tag_${var.tenant_config.short_name}_scoped" + description = "Org policy tag scoped grant for tenant ${var.tenant_config.short_name}." + expression = local.iam_tenant_condition + } +} + +# tag-based condition for service accounts is in the automation-sa file diff --git a/fast/stages-multitenant/0-bootstrap-tenant/outputs-files.tf b/fast/stages-multitenant/0-bootstrap-tenant/outputs-files.tf new file mode 100644 index 000000000..28bec3276 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/outputs-files.tf @@ -0,0 +1,46 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Output files persistence to local filesystem. + +locals { + outputs_root = join("/", [ + try(pathexpand(var.outputs_location), ""), + "tenants", + var.tenant_config.short_name + ]) +} + +resource "local_file" "providers" { + count = var.outputs_location == null ? 0 : 1 + file_permission = "0644" + filename = "${local.outputs_root}/providers/1-resman-tenant-providers.tf" + content = try(local.provider, null) +} + +resource "local_file" "tfvars" { + count = var.outputs_location == null ? 0 : 1 + file_permission = "0644" + filename = "${local.outputs_root}/tfvars/0-bootstrap-tenant.auto.tfvars.json" + content = jsonencode(local.tfvars) +} + +resource "local_file" "workflows" { + for_each = var.outputs_location == null ? {} : local.cicd_workflows + file_permission = "0644" + filename = "${local.outputs_root}/workflows/${each.key}-${local.cicd_repositories[each.key].type}.yaml" + content = each.value +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/outputs-gcs.tf b/fast/stages-multitenant/0-bootstrap-tenant/outputs-gcs.tf new file mode 100644 index 000000000..5196bad7d --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/outputs-gcs.tf @@ -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. + */ + +# tfdoc:file:description Output files persistence to automation GCS bucket. + +resource "google_storage_bucket_object" "providers" { + bucket = module.automation-tf-output-gcs.name + # provider suffix allows excluding via .gitignore when linked from stages + name = "providers/1-resman-tenant-providers.tf" + content = local.provider +} + +resource "google_storage_bucket_object" "tfvars" { + bucket = module.automation-tf-output-gcs.name + name = "tfvars/0-bootstrap-tenant.auto.tfvars.json" + content = jsonencode(local.tfvars) +} + +resource "google_storage_bucket_object" "workflows" { + for_each = local.cicd_workflows + bucket = ( + each.key == "bootstrap" + ? var.automation.outputs_bucket + : module.automation-tf-output-gcs.name + ) + name = "workflows/${each.key}-${local.cicd_repositories[each.key].type}.yaml" + content = each.value +} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/outputs.tf b/fast/stages-multitenant/0-bootstrap-tenant/outputs.tf new file mode 100644 index 000000000..4f22ff636 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/outputs.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 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 { + cicd_workflows = { + for k, v in local.cicd_repositories : k => templatefile( + "${path.module}/templates/workflow-${v.type}.yaml", ( + k == "bootstrap" + ? { + identity_provider = try( + local.cicd_identity_providers[v["identity_provider"]].name, "" + ) + outputs_bucket = var.automation.outputs_bucket + service_account = try( + module.automation-tf-cicd-sa-bootstrap["0"].email, "" + ) + stage_name = k + tf_providers_file = "" + tf_var_files = [ + "0-bootstrap.auto.tfvars.json", + "1-resman.auto.tfvars.json", + "globals.auto.tfvars.json" + ] + } + : { + identity_provider = try( + local.cicd_identity_providers[v["identity_provider"]].name, "" + ) + outputs_bucket = module.automation-tf-output-gcs.name + service_account = try( + module.automation-tf-cicd-sa-resman["0"].email, "" + ) + stage_name = k + tf_providers_file = ( + "${local._file_prefix}/providers/1-resman-tenant-providers.tf" + ) + tf_var_files = [ + "${local._file_prefix}/tfvars/0-bootstrap-tenant.auto.tfvars.json" + ] + } + ) + ) + } + provider = templatefile( + "${path.module}/templates/providers.tf.tpl", { + bucket = module.automation-tf-resman-gcs.name + name = "resman" + sa = module.automation-tf-resman-sa.email + } + ) + tfvars = { + automation = { + outputs_bucket = module.automation-tf-output-gcs.name + project_id = module.automation-project.project_id + project_number = module.automation-project.number + federated_identity_pools = compact([ + try(google_iam_workload_identity_pool.default.0.name, null), + var.automation.federated_identity_pool, + ]) + federated_identity_providers = local.cicd_identity_providers + service_accounts = merge( + { resman = module.automation-tf-resman-sa.email }, + { + for k, v in local.branch_sas : k => try( + module.automation-tf-resman-sa-stage2-3[k].email, null + ) + } + ) + } + billing_account = var.billing_account + custom_roles = var.custom_roles + fast_features = local.fast_features + groups = var.tenant_config.groups + locations = local.locations + organization = var.organization + prefix = local.prefix + root_node = module.tenant-folder.id + short_name = var.tenant_config.short_name + tags = { + keys = var.tag_keys + names = var.tag_names + values = merge(var.tag_values, { + for k, v in module.organization.tag_values : k => v.id + }) + } + } +} + +output "cicd_workflows" { + description = "CI/CD workflows for tenant bootstrap and resource management stages." + sensitive = true + value = local.cicd_workflows +} + +output "federated_identity" { + description = "Workload Identity Federation pool and providers." + value = { + pool = try( + google_iam_workload_identity_pool.default.0.name, null + ) + providers = local.cicd_identity_providers + } +} + +output "provider" { + # tfdoc:output:consumers stage-01 + description = "Terraform provider file for tenant resource management stage." + sensitive = true + value = local.provider +} + +output "tenant_resources" { + description = "Tenant-level resources." + value = { + bucket = module.automation-tf-resman-gcs.name + folder = module.tenant-folder.id + project_id = module.automation-project.project_id + project_number = module.automation-project.number + service_account = module.automation-tf-resman-sa.email + } +} + +output "tfvars" { + description = "Terraform variable files for the following tenant stages." + sensitive = true + value = local.tfvars +} diff --git a/tests/modules/gke_cluster/fixture/main.tf b/fast/stages-multitenant/0-bootstrap-tenant/templates/providers.tf.tpl similarity index 63% rename from tests/modules/gke_cluster/fixture/main.tf rename to fast/stages-multitenant/0-bootstrap-tenant/templates/providers.tf.tpl index 4ac38e165..e11a51b80 100644 --- a/tests/modules/gke_cluster/fixture/main.tf +++ b/fast/stages-multitenant/0-bootstrap-tenant/templates/providers.tf.tpl @@ -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,15 +14,17 @@ * limitations under the License. */ -module "test" { - source = "../../../../modules/gke-cluster" - project_id = "my-project" - name = "cluster-1" - location = "europe-west1-b" - vpc_config = { - network = "mynetwork" - subnetwork = "mysubnet" +terraform { + backend "gcs" { + bucket = "${bucket}" + impersonate_service_account = "${sa}" } - enable_addons = var.enable_addons - enable_features = var.enable_features } +provider "google" { + impersonate_service_account = "${sa}" +} +provider "google-beta" { + impersonate_service_account = "${sa}" +} + +# end provider.tf for ${name} diff --git a/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-github.yaml b/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-github.yaml new file mode 100644 index 000000000..364ee8f46 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-github.yaml @@ -0,0 +1,202 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "FAST ${stage_name} stage" + +on: + pull_request: + branches: + - main + types: + - closed + - opened + - synchronize + +env: + FAST_OUTPUTS_BUCKET: ${outputs_bucket} + FAST_SERVICE_ACCOUNT: ${service_account} + FAST_WIF_PROVIDER: ${identity_provider} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + %{~ if tf_providers_file != "" ~} + TF_PROVIDERS_FILE: ${tf_providers_file} + %{~ endif ~} + TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + TF_VERSION: 1.3.2 + +jobs: + fast-pr: + permissions: + contents: read + id-token: write + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - id: checkout + name: Checkout repository + uses: actions/checkout@v3 + + # set up SSH key authentication to the modules repository + - id: ssh-config + name: Configure SSH authentication + run: | + ssh-agent -a "$SSH_AUTH_SOCK" > /dev/null + ssh-add - <<< "$${{ secrets.CICD_MODULES_KEY }}" + + # set up authentication via Workload identity Federation + - id: gcp-auth + name: Authenticate to Google Cloud + uses: google-github-actions/auth@v0 + with: + workload_identity_provider: $${{ env.FAST_WIF_PROVIDER }} + service_account: $${{ env.FAST_SERVICE_ACCOUNT }} + access_token_lifetime: 3600s + + - id: gcp-sdk + name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v0 + with: + install_components: alpha + + # copy provider and tfvars files + - id: tf-config + name: Copy Terraform output files + run: | + %{~ if tf_providers_file != "" ~} + gcloud alpha storage cp -r \ + "gs://$${{env.FAST_OUTPUTS_BUCKET}}/providers/$${{env.TF_PROVIDERS_FILE}}" ./ + %{~ endif ~} + gcloud alpha storage cp -r \ + "gs://$${{env.FAST_OUTPUTS_BUCKET}}/tfvars" ./ + for f in $${{env.TF_VAR_FILES}}; do + ln -s "tfvars/$f" ./ + done + + - id: tf-setup + name: Set up Terraform + uses: hashicorp/setup-terraform@v2.0.3 + with: + terraform_version: $${{ env.TF_VERSION }} + + # run Terraform init/validate/plan + - id: tf-init + name: Terraform init + continue-on-error: true + run: | + terraform init -no-color + + - id: tf-validate + name: Terraform validate + continue-on-error: true + run: terraform validate -no-color + + - id: tf-plan + name: Terraform plan + continue-on-error: true + run: | + terraform plan -input=false -out ../plan.out -no-color + + - id: tf-apply + if: github.event.pull_request.merged == true && success() + name: Terraform apply + continue-on-error: true + run: | + terraform apply -input=false -auto-approve -no-color ../plan.out + + - id: pr-comment + name: Post comment to Pull Request + continue-on-error: true + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' + env: + PLAN: $${{ steps.tf-plan.outputs.stdout }}\n$${{ steps.tf-plan.outputs.stderr }} + with: + script: | + const output = `### Terraform Initialization \`$${{ steps.tf-init.outcome }}\` + + ### Terraform Validation \`$${{ steps.tf-validate.outcome }}\` + +
Validation Output + + \`\`\`\n + $${{ steps.tf-validate.outputs.stdout }} + \`\`\` + +
+ + ### Terraform Plan \`$${{ steps.tf-plan.outcome }}\` + +
Show Plan + + \`\`\`\n + $${process.env.PLAN.split('\n').filter(l => l.match(/^([A-Z\s].*|)$$/)).join('\n')} + \`\`\` + +
+ + ### Terraform Apply \`$${{ steps.tf-apply.outcome }}\` + + *Pusher: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`, Working Directory: \`$${{ env.tf_actions_working_dir }}\`, Workflow: \`$${{ github.workflow }}\`*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - id: pr-short-comment + name: Post comment to Pull Request + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' && steps.pr-comment.outcome != 'success' + with: + script: | + const output = `### Terraform Initialization \`$${{ steps.tf-init.outcome }}\` + + ### Terraform Validation \`$${{ steps.tf-validate.outcome }}\` + + ### Terraform Plan \`$${{ steps.tf-plan.outcome }}\` + + Plan output is in the action log. + + ### Terraform Apply \`$${{ steps.tf-apply.outcome }}\` + + *Pusher: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`, Working Directory: \`$${{ env.tf_actions_working_dir }}\`, Workflow: \`$${{ github.workflow }}\`*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - id: check-init + name: Check init failure + if: steps.tf-init.outcome != 'success' + run: exit 1 + + - id: check-validate + name: Check validate failure + if: steps.tf-validate.outcome != 'success' + run: exit 1 + + - id: check-plan + name: Check plan failure + if: steps.tf-plan.outcome != 'success' + run: exit 1 + + - id: check-apply + name: Check apply failure + if: github.event.pull_request.merged == true && steps.tf-apply.outcome != 'success' + run: exit 1 diff --git a/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-gitlab.yaml b/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-gitlab.yaml new file mode 100644 index 000000000..739e74851 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-gitlab.yaml @@ -0,0 +1,124 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +default: + before_script: + - echo "$${CI_JOB_JWT_V2}" > token.txt + image: + name: hashicorp/terraform + entrypoint: + - "/usr/bin/env" + - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +variables: + GOOGLE_CREDENTIALS: cicd-sa-credentials.json + FAST_OUTPUTS_BUCKET: ${outputs_bucket} + FAST_SERVICE_ACCOUNT: ${service_account} + FAST_WIF_PROVIDER: ${identity_provider} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + %{~ if tf_providers_file != "" ~} + TF_PROVIDERS_FILE: ${tf_providers_file} + %{~ endif ~} + TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + +stages: + - gcp-auth + - tf-files + - tf-plan + - tf-apply + +cache: + key: gcp-auth + paths: + - cicd-sa-credentials.json + - .tf-setup + +gcp-auth: + image: + name: google/cloud-sdk:slim + stage: gcp-auth + script: + - | + gcloud iam workload-identity-pools create-cred-config \ + $${FAST_WIF_PROVIDER} \ + --service-account=$${FAST_SERVICE_ACCOUNT} \ + --service-account-token-lifetime-seconds=3600 \ + --output-file=$${GOOGLE_CREDENTIALS} \ + --credential-source-file=token.txt +tf-files: + dependencies: + - gcp-auth + image: + name: google/cloud-sdk:slim + stage: tf-files + script: + # - gcloud components install -q alpha + - gcloud config set auth/credential_file_override $${GOOGLE_CREDENTIALS} + - mkdir -p .tf-setup + %{~ if tf_providers_file != "" ~} + - | + gcloud alpha storage cp -r \ + "gs://$${FAST_OUTPUTS_BUCKET}/providers/$${TF_PROVIDERS_FILE}" .tf-setup/ + %{~ endif ~} + - | + gcloud alpha storage cp -r \ + "gs://$${FAST_OUTPUTS_BUCKET}/tfvars" .tf-setup/ + +tf-plan: + # uncomment the following lines and set the SSH key secret for private modules repo + # before_script: + # - | + # ssh-agent -a $SSH_AUTH_SOCK > /dev/null + # echo "$CICD_MODULES_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null + # mkdir -p ~/.ssh + # ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts + # ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts + stage: tf-plan + script: + - cp .tf-setup/$${TF_PROVIDERS_FILE} ./ + - | + for f in $${TF_VAR_FILES}; do + ln -s ".tf-setup/tfvars/$f" ./ + done + - terraform init + - terraform validate + - terraform plan + dependencies: + - tf-files + +tf-apply: + # uncomment the following lines and set the SSH key secret for private modules repo + # before_script: + # - | + # ssh-agent -a $SSH_AUTH_SOCK > /dev/null + # echo "$CICD_MODULES_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null + # mkdir -p ~/.ssh + # ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts + # ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts + stage: tf-apply + script: + - cp .tf-setup/$${TF_PROVIDERS_FILE} ./ + - | + for f in $${TF_VAR_FILES}; do + ln -s ".tf-setup/tfvars/$f" ./ + done + - terraform init + - terraform validate + - terraform apply -input=false -auto-approve + dependencies: + - tf-files + when: manual + only: + variables: + - $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH diff --git a/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-sourcerepo.yaml b/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-sourcerepo.yaml new file mode 100644 index 000000000..e171c45e7 --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/templates/workflow-sourcerepo.yaml @@ -0,0 +1,100 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +steps: + - name: alpine:3 + id: tf-download + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + mkdir -p /builder/home/.local/bin + wget https://releases.hashicorp.com/terraform/$${_TF_VERSION}/terraform_$${_TF_VERSION}_linux_amd64.zip + unzip terraform_$${_TF_VERSION}_linux_amd64.zip -d /builder/home/.local/bin + rm terraform_$${_TF_VERSION}_linux_amd64.zip + chmod 755 /builder/home/.local/bin/terraform + - name: alpine:3 + id: tf-check-format + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform fmt -recursive -check /workspace/ + - name: gcr.io/google.com/cloudsdktool/cloud-sdk:alpine + id: tf-files + entrypoint: bash + args: + - -eEuo + - pipefail + - -c + - |- + %{~ if tf_providers_file != "" ~} + /google-cloud-sdk/bin/gsutil cp \ + gs://$${_FAST_OUTPUTS_BUCKET}/providers/$${_TF_PROVIDERS_FILE} ./ + %{~ endif ~} + /google-cloud-sdk/bin/gsutil cp -r \ + gs://$${_FAST_OUTPUTS_BUCKET}/tfvars ./ + for f in $${_TF_VAR_FILES}; do + ln -s tfvars/$f ./ + done + - name: alpine:3 + id: tf-init + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform init -no-color + - name: alpine:3 + id: tf-check-validate + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform validate -no-color + - name: alpine:3 + id: tf-plan + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform plan -no-color -input=false -out plan.out + # store artifact and ask for approval here if needed + - name: alpine:3 + id: tf-apply + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform apply -no-color -input=false -auto-approve plan.out +options: + env: + - PATH=/usr/local/bin:/usr/bin:/bin:/builder/home/.local/bin + logging: CLOUD_LOGGING_ONLY +substitutions: + _FAST_OUTPUTS_BUCKET: ${outputs_bucket} + _TF_PROVIDERS_FILE: ${tf_providers_file} + _TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + _TF_VERSION: 1.3.2 diff --git a/fast/stages-multitenant/0-bootstrap-tenant/variables.tf b/fast/stages-multitenant/0-bootstrap-tenant/variables.tf new file mode 100644 index 000000000..d218986fd --- /dev/null +++ b/fast/stages-multitenant/0-bootstrap-tenant/variables.tf @@ -0,0 +1,306 @@ +/** + * 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. + */ + +# defaults for variables marked with global tfdoc annotations, can be set via +# the tfvars file generated in stage 00 and stored in its outputs + +variable "automation" { + # tfdoc:variable:source 0-bootstrap + description = "Automation resources created by the organization-level bootstrap stage." + type = object({ + outputs_bucket = string + project_id = string + project_number = string + federated_identity_pool = string + federated_identity_providers = map(object({ + issuer = string + issuer_uri = string + name = string + principal_tpl = string + principalset_tpl = string + })) + }) +} + +variable "billing_account" { + # tfdoc:variable:source 0-bootstrap + description = "Billing account id. If billing account is not part of the same org set `is_org_level` to false." + type = object({ + id = string + is_org_level = optional(bool, true) + }) + validation { + condition = var.billing_account.is_org_level != null + error_message = "Invalid `null` value for `billing_account.is_org_level`." + } +} + +variable "cicd_repositories" { + description = "CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed." + type = object({ + bootstrap = optional(object({ + branch = optional(string) + identity_provider = string + name = string + type = string + })) + resman = optional(object({ + branch = optional(string) + identity_provider = string + name = string + type = string + })) + }) + default = null + validation { + condition = alltrue([ + for k, v in coalesce(var.cicd_repositories, {}) : + v == null || try(v.name, null) != null + ]) + error_message = "Non-null repositories need a non-null name." + } + validation { + condition = alltrue([ + for k, v in coalesce(var.cicd_repositories, {}) : + v == null || ( + try(v.identity_provider, null) != null + || + try(v.type, null) == "sourcerepo" + ) + ]) + error_message = "Non-null repositories need a non-null provider unless type is 'sourcerepo'." + } + validation { + condition = alltrue([ + for k, v in coalesce(var.cicd_repositories, {}) : + v == null || ( + contains(["github", "gitlab", "sourcerepo"], coalesce(try(v.type, null), "null")) + ) + ]) + error_message = "Invalid repository type, supported types: 'github' 'gitlab' or 'sourcerepo'." + } +} + +variable "custom_roles" { + # tfdoc:variable:source 0-bootstrap + description = "Custom roles defined at the organization level, in key => id format." + type = object({ + service_project_network_admin = string + tenant_network_admin = string + }) + default = null +} + +variable "fast_features" { + # tfdoc:variable:source 0-bootstrap + description = "Selective control for top-level FAST features." + type = object({ + data_platform = optional(bool, true) + gke = optional(bool, true) + project_factory = optional(bool, true) + sandbox = optional(bool, true) + teams = optional(bool, true) + }) + default = {} + nullable = false +} + +variable "federated_identity_providers" { + description = "Workload Identity Federation pools. The `cicd_repositories` variable references keys here." + type = map(object({ + attribute_condition = string + issuer = string + custom_settings = object({ + issuer_uri = string + allowed_audiences = list(string) + }) + })) + default = {} + nullable = false +} + +variable "group_iam" { + description = "Tenant-level custom group IAM settings in group => [roles] format." + type = map(list(string)) + default = {} +} + +variable "iam" { + description = "Tenant-level custom IAM settings in role => [principal] format." + type = map(list(string)) + default = {} +} + +variable "iam_additive" { + description = "Tenant-level custom IAM settings in role => [principal] format for non-authoritative bindings." + type = map(list(string)) + default = {} +} + +variable "locations" { + # tfdoc:variable:source 0-bootstrap + description = "Optional locations for GCS, BigQuery, and logging buckets created here. These are the defaults set at the organization level, and can be overridden via the tenant config variable." + type = object({ + bq = string + gcs = string + logging = string + pubsub = list(string) + }) + default = { + bq = "EU" + gcs = "EU" + logging = "global" + pubsub = [] + } + nullable = false +} + +# See https://cloud.google.com/architecture/exporting-stackdriver-logging-for-security-and-access-analytics +# for additional logging filter examples +variable "log_sinks" { + description = "Tenant-level log sinks, in name => {type, filter} format." + type = map(object({ + filter = string + type = string + })) + default = { + audit-logs = { + filter = "logName:\"/logs/cloudaudit.googleapis.com%2Factivity\" OR logName:\"/logs/cloudaudit.googleapis.com%2Fsystem_event\"" + type = "logging" + } + } + validation { + condition = alltrue([ + for k, v in var.log_sinks : + contains(["bigquery", "logging", "pubsub", "storage"], v.type) + ]) + error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'." + } +} + +variable "organization" { + # tfdoc:variable:source 0-bootstrap + description = "Organization details." + type = object({ + domain = string + id = number + customer_id = string + }) +} + +variable "outputs_location" { + description = "Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable." + type = string + default = null +} + +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." + } +} + +variable "project_parent_ids" { + description = "Optional parents for projects created here in folders/nnnnnnn format. Null values will use the tenant folder as parent." + type = object({ + automation = string + logging = string + }) + default = { + automation = null + logging = null + } + nullable = false +} + +variable "tag_keys" { + # tfdoc:variable:source 1-resman + description = "Organization tag keys." + type = object({ + context = string + environment = string + tenant = string + }) + nullable = false +} + +variable "tag_names" { + # tfdoc:variable:source 1-resman + description = "Customized names for resource management tags." + type = object({ + context = string + environment = string + tenant = string + }) + nullable = false +} + +variable "tag_values" { + # tfdoc:variable:source 1-resman + description = "Organization resource management tag values." + type = map(string) + nullable = false +} + +variable "tenant_config" { + description = "Tenant configuration. Short name must be 4 characters or less." + type = object({ + descriptive_name = string + groups = object({ + gcp-admins = string + gcp-devops = optional(string) + gcp-network-admins = optional(string) + gcp-security-admins = optional(string) + }) + short_name = string + fast_features = optional(object({ + data_platform = optional(bool) + gke = optional(bool) + project_factory = optional(bool) + sandbox = optional(bool) + teams = optional(bool) + }), {}) + locations = optional(object({ + bq = optional(string) + gcs = optional(string) + logging = optional(string) + pubsub = optional(list(string)) + }), {}) + }) + nullable = false + validation { + condition = alltrue([ + for a in ["descriptive_name", "groups", "short_name"] : + var.tenant_config[a] != null + ]) + error_message = "Non-optional members must not be null." + } + validation { + condition = length(var.tenant_config.short_name) < 5 + error_message = "Short name must be a string of 4 characters or less." + } +} + + +variable "test_principal" { + description = "Used when testing to bypass the data source returning the current identity." + type = string + default = null +} diff --git a/fast/stages-multitenant/1-resman-tenant/IAM.md b/fast/stages-multitenant/1-resman-tenant/IAM.md new file mode 100644 index 000000000..16db4a6c5 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/IAM.md @@ -0,0 +1,60 @@ +# IAM bindings reference + +Legend: + additive, conditional. + +## Folder development [#0] + +| members | roles | +|---|---| +|tn0-gke-dev-0
serviceAccount|[roles/compute.xpnAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.xpnAdmin)
[roles/logging.admin](https://cloud.google.com/iam/docs/understanding-roles#logging.admin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/resourcemanager.folderAdmin](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.folderAdmin)
[roles/resourcemanager.projectCreator](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.projectCreator) | + +## Folder development [#1] + +| members | roles | +|---|---| +|tn0-gke-dev-0
serviceAccount|organizations/[org_id #0]/roles/serviceProjectNetworkAdmin | +|tn0-pf-dev-0
serviceAccount|organizations/[org_id #0]/roles/serviceProjectNetworkAdmin | + +## Folder networking + +| members | roles | +|---|---| +|tn0-networking-0
serviceAccount|[roles/compute.xpnAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.xpnAdmin)
[roles/logging.admin](https://cloud.google.com/iam/docs/understanding-roles#logging.admin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/resourcemanager.folderAdmin](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.folderAdmin)
[roles/resourcemanager.projectCreator](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.projectCreator) | + +## Folder production [#0] + +| members | roles | +|---|---| +|tn0-gke-prod-0
serviceAccount|[roles/compute.xpnAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.xpnAdmin)
[roles/logging.admin](https://cloud.google.com/iam/docs/understanding-roles#logging.admin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/resourcemanager.folderAdmin](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.folderAdmin)
[roles/resourcemanager.projectCreator](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.projectCreator) | + +## Folder production [#1] + +| members | roles | +|---|---| +|tn0-gke-prod-0
serviceAccount|organizations/[org_id #0]/roles/serviceProjectNetworkAdmin | +|tn0-pf-prod-0
serviceAccount|organizations/[org_id #0]/roles/serviceProjectNetworkAdmin | + +## Folder sandbox + +| members | roles | +|---|---| +|tn0-sandbox-0
serviceAccount|[roles/logging.admin](https://cloud.google.com/iam/docs/understanding-roles#logging.admin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/resourcemanager.folderAdmin](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.folderAdmin)
[roles/resourcemanager.projectCreator](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.projectCreator) | + +## Folder security + +| members | roles | +|---|---| +|tn0-security-0
serviceAccount|[roles/logging.admin](https://cloud.google.com/iam/docs/understanding-roles#logging.admin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/resourcemanager.folderAdmin](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.folderAdmin)
[roles/resourcemanager.projectCreator](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.projectCreator) | + +## Folder teams + +| members | roles | +|---|---| +|tn0-teams-0
serviceAccount|[roles/compute.xpnAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.xpnAdmin)
[roles/logging.admin](https://cloud.google.com/iam/docs/understanding-roles#logging.admin)
[roles/owner](https://cloud.google.com/iam/docs/understanding-roles#owner)
[roles/resourcemanager.folderAdmin](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.folderAdmin)
[roles/resourcemanager.projectCreator](https://cloud.google.com/iam/docs/understanding-roles#resourcemanager.projectCreator) | + +## Folder test tenant 0 + +| members | roles | +|---|---| +|tn0-networking-0
serviceAccount|[roles/compute.orgFirewallPolicyAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.orgFirewallPolicyAdmin) +
[roles/compute.xpnAdmin](https://cloud.google.com/iam/docs/understanding-roles#compute.xpnAdmin) +| +|tn0-security-0
serviceAccount|[roles/accesscontextmanager.policyAdmin](https://cloud.google.com/iam/docs/understanding-roles#accesscontextmanager.policyAdmin) +| diff --git a/fast/stages-multitenant/1-resman-tenant/README.md b/fast/stages-multitenant/1-resman-tenant/README.md new file mode 100644 index 000000000..ae42fc305 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/README.md @@ -0,0 +1,184 @@ +# Tenant resource management + +This stage is run for a specific tenant after [tenant bootstrap](../0-bootstrap-tenant/) has successfully created initial resources for the tenant, which is then decoupled from the organization. + +It is logically equivalent and almost identical in code to the corresponding [organization resource management stage](../../stages/1-resman/), with a few notable differences: + +- the hierarchy is rooted in the tenant top-level folder instead of the organization +- there's no management of tag values and keys since they organization-level resources (it could be implemented for tenant-specific tags if the need arises) +- automation service accounts for subsequent stages are configured but not created here (tenant-level bootstrap creates them and assigns organization-level permissions) + +The stage runs with a dedicated service account for the tenant, which has no permissions at the organization level except for billing and organization policies, constrained by a condition on the tenant tag. + +The following diagram is a high level reference of what this stage manages, showing one hypothetical tenant (additional tenants require additional instances of this stage being deployed): + +```mermaid +%%{init: {'theme':'base'}}%% +classDiagram + Tenant_root~📁~ -- tn0_automation + Tenant_root~📁~ -- Networking~📁~ + Tenant_root~📁~ -- Security~📁~ + Tenant_root~📁~ -- Data_Platform~📁~ + Data_Platform~📁~ -- DP_Dev~📁~ + Data_Platform~📁~ -- DP_Prod~📁~ + Tenant_root~📁~ -- GKE~📁~ + GKE~📁~ -- GKE_Dev~📁~ + GKE~📁~ -- GKE_Prod~📁~ + Tenant_root~📁~ -- Teams~📁~ + Teams~📁~ -- Team_0~📁~ + Team_0~📁~ -- Team_0_Dev~📁~ + Team_0~📁~ -- Team_0_Prod~📁~ + Tenant_root~📁~ -- Sandbox~📁~ + class Tenant_root~📁~ { + - IAM bindings() + - org policies() + } + class tn0_automation { + - GCS buckets + - IAM bindings() + } + class Data_Platform~📁~ { + - IAM bindings() + - tag bindings() + } + class DP_Dev~📁~ { + - IAM bindings() + - tag bindings() + } + class DP_Prod~📁~ { + - IAM bindings() + - tag bindings() + } + class GKE~📁~ { + - IAM bindings() + - tag bindings() + } + class GKE_Dev~📁~ { + - IAM bindings() + - tag bindings() + } + class GKE_Prod~📁~ { + - IAM bindings() + - tag bindings() + } + class Networking~📁~ { + - IAM bindings() + - tag bindings() + } + class Security~📁~ { + - IAM bindings() + - tag bindings() + } + class Sandbox~📁~ { + - IAM bindings() + - tag bindings() + } + class Teams~📁~ { + - IAM bindings() + - tag bindings() + } + class Team_0~📁~ { + - IAM bindings() + - tag bindings() + } + class Team_0_Dev~📁~ { + - IAM bindings() + - tag bindings() + } + class Team_0_Prod~📁~ { + - IAM bindings() + - tag bindings() + } +``` + +As most of the features of this stage follow the same design and configurations of the [organization-level resource management stage](../../stages/1-resman/), we will only focus on the tenant-specific configuration in this document. + +## How to run this stage + +As mentioned above this stage is decoupled from organization-level stages: it uses a service account and state bucket from the tenant-specific automation project, and its tfvars and provider files are also tenant-specific. + +The `stage-links.sh` script can be used to get the commands needed for the provider and output files, just set the variable for the tenant shortname (the same one specified in the tenant bootstrap stage) and pass a single argument with your FAST output files folder path, or GCS bucket URI: + +```bash +TENANT=tn0 ../../stage-links.sh ~/fast-config +``` + +The script output can be copy/pasted to a terminal: + +```bash +# copy and paste the following commands for '1-resman-tenant' + +ln -s ~/fast-config/tenants/tn0/providers/1-resman-tenant-providers.tf ./ +ln -s ~/fast-config/tenants/tn0/tfvars/0-bootstrap-tenant.auto.tfvars.json ./ +``` + +Once that is done, stage-level configuration variables are the same as the corresponding organization-level stage. + +### Running the stage + +Once the configuration is done just go through the usual `init/apply` cycle. On successful apply, a tfvars file specific for this tenant and a set of provider files will be created. + + + + +## Files + +| name | description | modules | resources | +|---|---|---|---| +| [branch-data-platform.tf](./branch-data-platform.tf) | Data Platform stages resources. | folder · gcs · iam-service-account | | +| [branch-gke.tf](./branch-gke.tf) | GKE multitenant stage resources. | folder · gcs · iam-service-account | | +| [branch-networking.tf](./branch-networking.tf) | Networking stage resources. | folder · gcs · iam-service-account | | +| [branch-project-factory.tf](./branch-project-factory.tf) | Project factory stage resources. | gcs · iam-service-account | | +| [branch-sandbox.tf](./branch-sandbox.tf) | Sandbox stage resources. | folder · gcs | | +| [branch-security.tf](./branch-security.tf) | Security stage resources. | folder · gcs · iam-service-account | | +| [branch-teams.tf](./branch-teams.tf) | Team stage resources. | folder · gcs · iam-service-account | | +| [cicd-data-platform.tf](./cicd-data-platform.tf) | CI/CD resources for the data platform branch. | iam-service-account · source-repository | | +| [cicd-gke.tf](./cicd-gke.tf) | CI/CD resources for the data platform branch. | iam-service-account · source-repository | | +| [cicd-networking.tf](./cicd-networking.tf) | CI/CD resources for the networking branch. | iam-service-account · source-repository | | +| [cicd-project-factory.tf](./cicd-project-factory.tf) | CI/CD resources for the teams branch. | iam-service-account · source-repository | | +| [cicd-security.tf](./cicd-security.tf) | CI/CD resources for the security branch. | iam-service-account · source-repository | | +| [main.tf](./main.tf) | Module-level locals and resources. | | | +| [outputs-files.tf](./outputs-files.tf) | Output files persistence to local filesystem. | | local_file | +| [outputs-gcs.tf](./outputs-gcs.tf) | Output files persistence to automation GCS bucket. | | google_storage_bucket_object | +| [outputs.tf](./outputs.tf) | Module outputs. | | | +| [root_node.tf](./root_node.tf) | Tenant root folder configuration. | folder | | +| [variables.tf](./variables.tf) | Module variables. | | | + +## Variables + +| name | description | type | required | default | producer | +|---|---|:---:|:---:|:---:|:---:| +| [automation](variables.tf#L20) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | +| [billing_account](variables.tf#L51) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap | +| [organization](variables.tf#L206) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L228) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [root_node](variables.tf#L239) | Root folder node for the tenant, in folders/nnnnnn format. | string | ✓ | | | +| [short_name](variables.tf#L244) | Short name used to identify the tenant. | string | ✓ | | | +| [tags](variables.tf#L249) | Resource management tags. | object({…}) | ✓ | | | +| [cicd_repositories](variables.tf#L64) | CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | +| [custom_roles](variables.tf#L146) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | +| [data_dir](variables.tf#L155) | Relative path for the folder storing configuration data. | string | | "data" | | +| [fast_features](variables.tf#L161) | Selective control for top-level FAST features. | object({…}) | | {} | 0-0-bootstrap | +| [groups](variables.tf#L175) | Group names to grant organization-level permissions. | object({…}) | | {} | 0-bootstrap | +| [locations](variables.tf#L188) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | 0-bootstrap | +| [organization_policy_data_path](variables.tf#L216) | Path for the data folder used by the organization policies factory. | string | | null | | +| [outputs_location](variables.tf#L222) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | +| [team_folders](variables.tf#L267) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | | +| [test_skip_data_sources](variables.tf#L277) | Used when testing to bypass data sources. | bool | | false | | + +## Outputs + +| name | description | sensitive | consumers | +|---|---|:---:|---| +| [cicd_repositories](outputs.tf#L189) | WIF configuration for CI/CD repositories. | | | +| [dataplatform](outputs.tf#L203) | Data for the Data Platform stage. | | | +| [gke_multitenant](outputs.tf#L219) | Data for the GKE multitenant stage. | | 03-gke-multitenant | +| [networking](outputs.tf#L240) | Data for the networking stage. | | | +| [project_factories](outputs.tf#L249) | Data for the project factories stage. | | | +| [providers](outputs.tf#L264) | Terraform provider files for this stage and dependent stages. | ✓ | 02-networking · 02-security · 03-dataplatform · xx-sandbox · xx-teams | +| [sandbox](outputs.tf#L271) | Data for the sandbox stage. | | xx-sandbox | +| [security](outputs.tf#L285) | Data for the networking stage. | | 02-security | +| [teams](outputs.tf#L295) | Data for the teams stage. | | | +| [tfvars](outputs.tf#L307) | Terraform variable files for the following stages. | ✓ | | + + diff --git a/fast/stages-multitenant/1-resman-tenant/branch-data-platform.tf b/fast/stages-multitenant/1-resman-tenant/branch-data-platform.tf new file mode 100644 index 000000000..3916d6358 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/branch-data-platform.tf @@ -0,0 +1,133 @@ +/** + * 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. + */ + +# tfdoc:file:description Data Platform stages resources. + +module "branch-dp-folder" { + source = "../../../modules/folder" + count = var.fast_features.data_platform ? 1 : 0 + parent = module.root-folder.id + name = "Data Platform" + tag_bindings = { + context = var.tags.values["${var.tags.names.context}/data"] + } +} + +module "branch-dp-dev-folder" { + source = "../../../modules/folder" + count = var.fast_features.data_platform ? 1 : 0 + parent = module.branch-dp-folder.0.id + name = "Development" + group_iam = {} + iam = { + (local.custom_roles.service_project_network_admin) = [ + local.automation_sas_iam.dp-dev + ] + # remove owner here and at project level if SA does not manage project resources + "roles/owner" = [local.automation_sas_iam.dp-dev] + "roles/logging.admin" = [local.automation_sas_iam.dp-dev] + "roles/resourcemanager.folderAdmin" = [local.automation_sas_iam.dp-dev] + "roles/resourcemanager.projectCreator" = [local.automation_sas_iam.dp-dev] + } + tag_bindings = { + context = var.tags.values["${var.tags.names.environment}/development"] + } +} + +module "branch-dp-prod-folder" { + source = "../../../modules/folder" + count = var.fast_features.data_platform ? 1 : 0 + parent = module.branch-dp-folder.0.id + name = "Production" + group_iam = {} + iam = { + (local.custom_roles.service_project_network_admin) = [ + local.automation_sas_iam.dp-prod + ] + # remove owner here and at project level if SA does not manage project resources + "roles/owner" = [local.automation_sas_iam.dp-prod] + "roles/logging.admin" = [local.automation_sas_iam.dp-prod] + "roles/resourcemanager.folderAdmin" = [local.automation_sas_iam.dp-prod] + "roles/resourcemanager.projectCreator" = [local.automation_sas_iam.dp-prod] + } + tag_bindings = { + context = var.tags.values["${var.tags.names.environment}/production"] + } +} + +# automation service accounts and buckets + +module "branch-dp-dev-sa" { + source = "../../../modules/iam-service-account" + count = var.fast_features.data_platform ? 1 : 0 + project_id = var.automation.project_id + name = "dp-dev-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.branch-dp-dev-sa-cicd.0.iam_email, null) + ]) + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-dp-prod-sa" { + source = "../../../modules/iam-service-account" + count = var.fast_features.data_platform ? 1 : 0 + project_id = var.automation.project_id + name = "dp-prod-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.branch-dp-prod-sa-cicd.0.iam_email, null) + ]) + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-dp-dev-gcs" { + source = "../../../modules/gcs" + count = var.fast_features.data_platform ? 1 : 0 + project_id = var.automation.project_id + name = "dev-resman-dp-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.dp-dev] + } +} + +module "branch-dp-prod-gcs" { + source = "../../../modules/gcs" + count = var.fast_features.data_platform ? 1 : 0 + project_id = var.automation.project_id + name = "prod-resman-dp-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.dp-prod] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/branch-gke.tf b/fast/stages-multitenant/1-resman-tenant/branch-gke.tf new file mode 100644 index 000000000..9ece810bb --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/branch-gke.tf @@ -0,0 +1,133 @@ +/** + * 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. + */ + +# tfdoc:file:description GKE multitenant stage resources. + +module "branch-gke-folder" { + source = "../../../modules/folder" + count = var.fast_features.gke ? 1 : 0 + parent = module.root-folder.id + name = "GKE" + tag_bindings = { + context = var.tags.values["${var.tags.names.context}/gke"] + } +} + +module "branch-gke-dev-folder" { + source = "../../../modules/folder" + count = var.fast_features.gke ? 1 : 0 + parent = module.branch-gke-folder.0.id + name = "Development" + iam = { + "roles/owner" = [local.automation_sas_iam.gke-dev] + "roles/logging.admin" = [local.automation_sas_iam.gke-dev] + "roles/resourcemanager.folderAdmin" = [local.automation_sas_iam.gke-dev] + "roles/resourcemanager.projectCreator" = [local.automation_sas_iam.gke-dev] + "roles/compute.xpnAdmin" = [local.automation_sas_iam.gke-dev] + } + tag_bindings = { + context = var.tags.values["${var.tags.names.environment}/development"] + } +} + +module "branch-gke-prod-folder" { + source = "../../../modules/folder" + count = var.fast_features.gke ? 1 : 0 + parent = module.branch-gke-folder.0.id + name = "Production" + iam = { + "roles/owner" = [local.automation_sas_iam.gke-prod] + "roles/logging.admin" = [local.automation_sas_iam.gke-prod] + "roles/resourcemanager.folderAdmin" = [local.automation_sas_iam.gke-prod] + "roles/resourcemanager.projectCreator" = [local.automation_sas_iam.gke-prod] + "roles/compute.xpnAdmin" = [local.automation_sas_iam.gke-prod] + } + tag_bindings = { + context = var.tags.values["${var.tags.names.environment}/production"] + } +} + +module "branch-gke-dev-sa" { + source = "../../../modules/iam-service-account" + count = var.fast_features.gke ? 1 : 0 + project_id = var.automation.project_id + name = "gke-dev-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam = { + "roles/iam.serviceAccountTokenCreator" = concat( + ( + local.groups.gcp-devops == null + ? [] + : ["group:${local.groups.gcp-devops}"] + ), + compact([ + try(module.branch-gke-dev-sa-cicd.0.iam_email, null) + ]) + ) + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-gke-prod-sa" { + source = "../../../modules/iam-service-account" + count = var.fast_features.gke ? 1 : 0 + project_id = var.automation.project_id + name = "gke-prod-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam = { + "roles/iam.serviceAccountTokenCreator" = concat( + ( + local.groups.gcp-devops == null + ? [] + : ["group:${local.groups.gcp-devops}"] + ), + compact([ + try(module.branch-gke-prod-sa-cicd.0.iam_email, null) + ]) + ) + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-gke-dev-gcs" { + source = "../../../modules/gcs" + count = var.fast_features.gke ? 1 : 0 + project_id = var.automation.project_id + name = "dev-resman-gke-0" + prefix = var.prefix + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.gke-dev] + } +} + +module "branch-gke-prod-gcs" { + source = "../../../modules/gcs" + count = var.fast_features.gke ? 1 : 0 + project_id = var.automation.project_id + name = "prod-resman-gke-0" + prefix = var.prefix + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.gke-prod] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/branch-networking.tf b/fast/stages-multitenant/1-resman-tenant/branch-networking.tf new file mode 100644 index 000000000..85490baf0 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/branch-networking.tf @@ -0,0 +1,107 @@ +/** + * 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. + */ + +# tfdoc:file:description Networking stage resources. + +module "branch-network-folder" { + source = "../../../modules/folder" + parent = module.root-folder.id + name = "Networking" + group_iam = local.groups.gcp-network-admins == null ? {} : { + (local.groups.gcp-network-admins) = [ + # add any needed roles for resources/services not managed via Terraform, + # or replace editor with ~viewer if no broad resource management needed + # e.g. + # "roles/compute.networkAdmin", + # "roles/dns.admin", + # "roles/compute.securityAdmin", + "roles/editor", + ] + } + iam = { + "roles/logging.admin" = [local.automation_sas_iam.networking] + "roles/owner" = [local.automation_sas_iam.networking] + "roles/resourcemanager.folderAdmin" = [local.automation_sas_iam.networking] + "roles/resourcemanager.projectCreator" = [local.automation_sas_iam.networking] + "roles/compute.xpnAdmin" = [local.automation_sas_iam.networking] + } + tag_bindings = { + context = var.tags.values["${var.tags.names.context}/networking"] + } +} + +module "branch-network-prod-folder" { + source = "../../../modules/folder" + parent = module.branch-network-folder.id + name = "Production" + iam = { + (local.custom_roles.service_project_network_admin) = concat( + local.branch_optional_sa_lists.dp-prod, + local.branch_optional_sa_lists.gke-prod, + local.branch_optional_sa_lists.pf-prod, + ) + } + tag_bindings = { + environment = var.tags.values["${var.tags.names.environment}/production"] + } +} + +module "branch-network-dev-folder" { + source = "../../../modules/folder" + parent = module.branch-network-folder.id + name = "Development" + iam = { + (local.custom_roles.service_project_network_admin) = concat( + local.branch_optional_sa_lists.dp-dev, + local.branch_optional_sa_lists.gke-dev, + local.branch_optional_sa_lists.pf-dev, + ) + } + tag_bindings = { + environment = var.tags.values["${var.tags.names.environment}/development"] + } +} + +# automation service account and bucket + +module "branch-network-sa" { + source = "../../../modules/iam-service-account" + project_id = var.automation.project_id + name = "networking-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.branch-network-sa-cicd.0.iam_email, null) + ]) + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-network-gcs" { + source = "../../../modules/gcs" + project_id = var.automation.project_id + name = "prod-resman-net-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.networking] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/branch-project-factory.tf b/fast/stages-multitenant/1-resman-tenant/branch-project-factory.tf new file mode 100644 index 000000000..2fa64bbc5 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/branch-project-factory.tf @@ -0,0 +1,79 @@ +/** + * 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. + */ + +# tfdoc:file:description Project factory stage resources. + +module "branch-pf-dev-sa" { + source = "../../../modules/iam-service-account" + count = var.fast_features.project_factory ? 1 : 0 + project_id = var.automation.project_id + name = "pf-dev-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.branch-pf-dev-sa-cicd.0.iam_email, null) + ]) + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-pf-prod-sa" { + source = "../../../modules/iam-service-account" + count = var.fast_features.project_factory ? 1 : 0 + project_id = var.automation.project_id + name = "pf-prod-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.branch-pf-prod-sa-cicd.0.iam_email, null) + ]) + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-pf-dev-gcs" { + source = "../../../modules/gcs" + count = var.fast_features.project_factory ? 1 : 0 + project_id = var.automation.project_id + name = "dev-resman-pf-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.pf-dev] + } +} + +module "branch-pf-prod-gcs" { + source = "../../../modules/gcs" + count = var.fast_features.project_factory ? 1 : 0 + project_id = var.automation.project_id + name = "prod-resman-pf-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.pf-prod] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/branch-sandbox.tf b/fast/stages-multitenant/1-resman-tenant/branch-sandbox.tf new file mode 100644 index 000000000..6f3d526c8 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/branch-sandbox.tf @@ -0,0 +1,51 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Sandbox stage resources. + +module "branch-sandbox-folder" { + source = "../../../modules/folder" + count = var.fast_features.sandbox ? 1 : 0 + parent = module.root-folder.id + name = "Sandbox" + iam = { + "roles/logging.admin" = [local.automation_sas_iam.sandbox] + "roles/owner" = [local.automation_sas_iam.sandbox] + "roles/resourcemanager.folderAdmin" = [local.automation_sas_iam.sandbox] + "roles/resourcemanager.projectCreator" = [local.automation_sas_iam.sandbox] + } + org_policies = { + "constraints/sql.restrictPublicIp" = { enforce = false } + "constraints/compute.vmExternalIpAccess" = { allow = { all = true } } + } + tag_bindings = { + context = var.tags.values["${var.tags.names.context}/sandbox"] + } +} + +module "branch-sandbox-gcs" { + source = "../../../modules/gcs" + count = var.fast_features.sandbox ? 1 : 0 + project_id = var.automation.project_id + name = "dev-resman-sbox-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.sandbox] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/branch-security.tf b/fast/stages-multitenant/1-resman-tenant/branch-security.tf new file mode 100644 index 000000000..d7253cce1 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/branch-security.tf @@ -0,0 +1,76 @@ +/** + * 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. + */ + +# tfdoc:file:description Security stage resources. + +module "branch-security-folder" { + source = "../../../modules/folder" + parent = module.root-folder.id + name = "Security" + group_iam = local.groups.gcp-security-admins == null ? {} : { + (local.groups.gcp-security-admins) = [ + # add any needed roles for resources/services not managed via Terraform, + # e.g. + # "roles/bigquery.admin", + # "roles/cloudasset.owner", + # "roles/cloudkms.admin", + # "roles/logging.admin", + # "roles/secretmanager.admin", + # "roles/storage.admin", + "roles/viewer" + ] + } + iam = { + "roles/logging.admin" = [local.automation_sas_iam.security] + "roles/owner" = [local.automation_sas_iam.security] + "roles/resourcemanager.folderAdmin" = [local.automation_sas_iam.security] + "roles/resourcemanager.projectCreator" = [local.automation_sas_iam.security] + } + tag_bindings = { + context = var.tags.values["${var.tags.names.context}/security"] + } +} + +# automation service account and bucket + +module "branch-security-sa" { + source = "../../../modules/iam-service-account" + project_id = var.automation.project_id + name = "security-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.branch-security-sa-cicd.0.iam_email, null) + ]) + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-security-gcs" { + source = "../../../modules/gcs" + project_id = var.automation.project_id + name = "prod-resman-sec-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [local.automation_sas_iam.security] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/branch-teams.tf b/fast/stages-multitenant/1-resman-tenant/branch-teams.tf new file mode 100644 index 000000000..57f221104 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/branch-teams.tf @@ -0,0 +1,163 @@ +/** + * 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. + */ + +# tfdoc:file:description Team stage resources. + +# TODO(ludo): add support for CI/CD + +############### top-level Teams branch and automation resources ############### + +module "branch-teams-folder" { + source = "../../../modules/folder" + count = var.fast_features.teams ? 1 : 0 + parent = module.root-folder.id + name = "Teams" + iam = { + "roles/logging.admin" = [local.automation_sas_iam.teams] + "roles/owner" = [local.automation_sas_iam.teams] + "roles/resourcemanager.folderAdmin" = [local.automation_sas_iam.teams] + "roles/resourcemanager.projectCreator" = [local.automation_sas_iam.teams] + "roles/compute.xpnAdmin" = [local.automation_sas_iam.teams] + } + tag_bindings = { + context = var.tags.values["${var.tags.names.context}/teams"] + } +} + +module "branch-teams-sa" { + source = "../../../modules/iam-service-account" + count = var.fast_features.teams ? 1 : 0 + project_id = var.automation.project_id + name = "teams-0" + prefix = var.prefix + service_account_create = var.test_skip_data_sources + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.admin"] + } +} + +module "branch-teams-gcs" { + source = "../../../modules/gcs" + count = var.fast_features.teams ? 1 : 0 + project_id = var.automation.project_id + name = "prod-resman-teams-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [module.branch-teams-sa.0.iam_email] + } +} + +################## per-team folders and automation resources ################## + +module "branch-teams-team-folder" { + source = "../../../modules/folder" + for_each = var.fast_features.teams ? coalesce(var.team_folders, {}) : {} + parent = module.branch-teams-folder.0.id + name = each.value.descriptive_name + iam = { + "roles/logging.admin" = [module.branch-teams-team-sa[each.key].iam_email] + "roles/owner" = [module.branch-teams-team-sa[each.key].iam_email] + "roles/resourcemanager.folderAdmin" = [module.branch-teams-team-sa[each.key].iam_email] + "roles/resourcemanager.projectCreator" = [module.branch-teams-team-sa[each.key].iam_email] + "roles/compute.xpnAdmin" = [module.branch-teams-team-sa[each.key].iam_email] + } + group_iam = each.value.group_iam == null ? {} : each.value.group_iam +} + +module "branch-teams-team-sa" { + source = "../../../modules/iam-service-account" + for_each = var.fast_features.teams ? coalesce(var.team_folders, {}) : {} + project_id = var.automation.project_id + name = "prod-teams-${each.key}-0" + display_name = "Terraform team ${each.key} service account." + prefix = var.prefix + iam = { + "roles/iam.serviceAccountTokenCreator" = ( + each.value.impersonation_groups == null + ? [] + : [for g in each.value.impersonation_groups : "group:${g}"] + ) + } +} + +module "branch-teams-team-gcs" { + source = "../../../modules/gcs" + for_each = var.fast_features.teams ? coalesce(var.team_folders, {}) : {} + project_id = var.automation.project_id + name = "prod-teams-${each.key}-0" + prefix = var.prefix + location = var.locations.gcs + storage_class = local.gcs_storage_class + versioning = true + iam = { + "roles/storage.objectAdmin" = [module.branch-teams-team-sa[each.key].iam_email] + } +} + +# per-team environment folders where project factory SAs can create projects + +module "branch-teams-team-dev-folder" { + source = "../../../modules/folder" + for_each = var.fast_features.teams ? coalesce(var.team_folders, {}) : {} + parent = module.branch-teams-team-folder[each.key].id + # naming: environment descriptive name + name = "Development" + # environment-wide human permissions on the whole teams environment + group_iam = {} + iam = { + (local.custom_roles.service_project_network_admin) = ( + local.branch_optional_sa_lists.pf-dev + ) + # remove owner here and at project level if SA does not manage project resources + "roles/owner" = local.branch_optional_sa_lists.pf-dev + "roles/logging.admin" = local.branch_optional_sa_lists.pf-dev + "roles/resourcemanager.folderAdmin" = local.branch_optional_sa_lists.pf-dev + "roles/resourcemanager.projectCreator" = local.branch_optional_sa_lists.pf-dev + } + tag_bindings = { + environment = try( + var.tags.values["${var.tags.names.environment}/development"], null + ) + } +} + +module "branch-teams-team-prod-folder" { + source = "../../../modules/folder" + for_each = var.fast_features.teams ? coalesce(var.team_folders, {}) : {} + parent = module.branch-teams-team-folder[each.key].id + # naming: environment descriptive name + name = "Production" + # environment-wide human permissions on the whole teams environment + group_iam = {} + iam = { + (local.custom_roles.service_project_network_admin) = ( + local.branch_optional_sa_lists.pf-prod + ) + # remove owner here and at project level if SA does not manage project resources + "roles/owner" = local.branch_optional_sa_lists.pf-prod + "roles/logging.admin" = local.branch_optional_sa_lists.pf-prod + "roles/resourcemanager.folderAdmin" = local.branch_optional_sa_lists.pf-prod + "roles/resourcemanager.projectCreator" = local.branch_optional_sa_lists.pf-prod + } + tag_bindings = { + environment = try( + var.tags.values["${var.tags.names.environment}/production"], null + ) + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/cicd-data-platform.tf b/fast/stages-multitenant/1-resman-tenant/cicd-data-platform.tf new file mode 100644 index 000000000..704f45d78 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/cicd-data-platform.tf @@ -0,0 +1,173 @@ +/** + * 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. + */ + +# tfdoc:file:description CI/CD resources for the data platform branch. + +# source repositories + +module "branch-dp-dev-cicd-repo" { + source = "../../../modules/source-repository" + for_each = ( + try(local.cicd_repositories.data_platform_dev.type, null) == "sourcerepo" + ? { 0 = local.cicd_repositories.data_platform_dev } + : {} + ) + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = local.branch_optional_sa_lists.dp-dev + "roles/source.reader" = compact([ + try(module.branch-dp-dev-sa-cicd.0.iam_email, "") + ]) + } + triggers = { + fast-03-dp-dev = { + filename = ".cloudbuild/workflow.yaml" + included_files = [ + "**/*json", "**/*tf", "**/*yaml", ".cloudbuild/workflow.yaml" + ] + service_account = module.branch-dp-dev-sa-cicd.0.id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } + depends_on = [module.branch-dp-dev-sa-cicd] +} + +module "branch-dp-prod-cicd-repo" { + source = "../../../modules/source-repository" + for_each = ( + try(local.cicd_repositories.data_platform_prod.type, null) == "sourcerepo" + ? { 0 = local.cicd_repositories.data_platform_prod } + : {} + ) + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = local.branch_optional_sa_lists.dp-prod + "roles/source.reader" = [module.branch-dp-prod-sa-cicd.0.iam_email] + } + triggers = { + fast-03-dp-prod = { + filename = ".cloudbuild/workflow.yaml" + included_files = [ + "**/*json", "**/*tf", "**/*yaml", ".cloudbuild/workflow.yaml" + ] + service_account = module.branch-dp-prod-sa-cicd.0.id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } + depends_on = [module.branch-dp-prod-sa-cicd] +} + +# SAs used by CI/CD workflows to impersonate automation SAs + +module "branch-dp-dev-sa-cicd" { + source = "../../../modules/iam-service-account" + for_each = ( + try(local.cicd_repositories.data_platform_dev.name, null) != null + ? { 0 = local.cicd_repositories.data_platform_dev } + : {} + ) + project_id = var.automation.project_id + name = "dev-resman-dp-1" + display_name = "Terraform CI/CD data platform development service account." + prefix = var.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam + } + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} + +module "branch-dp-prod-sa-cicd" { + source = "../../../modules/iam-service-account" + for_each = ( + try(local.cicd_repositories.data_platform_prod.name, null) != null + ? { 0 = local.cicd_repositories.data_platform_prod } + : {} + ) + project_id = var.automation.project_id + name = "prod-resman-dp-1" + display_name = "Terraform CI/CD data platform production service account." + prefix = var.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam + } + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/cicd-gke.tf b/fast/stages-multitenant/1-resman-tenant/cicd-gke.tf new file mode 100644 index 000000000..dfd035a51 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/cicd-gke.tf @@ -0,0 +1,175 @@ +/** + * 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. + */ + +# tfdoc:file:description CI/CD resources for the data platform branch. + +# source repositories + +module "branch-gke-dev-cicd-repo" { + source = "../../../modules/source-repository" + for_each = ( + try(local.cicd_repositories.gke_dev.type, null) == "sourcerepo" + ? { 0 = local.cicd_repositories.gke_dev } + : {} + ) + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = compact([ + try(module.branch-gke-dev-sa.0.iam_email, "") + ]) + "roles/source.reader" = compact([ + try(module.branch-gke-dev-sa-cicd.0.iam_email, "") + ]) + } + triggers = { + fast-03-gke-dev = { + filename = ".cloudbuild/workflow.yaml" + included_files = [ + "**/*json", "**/*tf", "**/*yaml", ".cloudbuild/workflow.yaml" + ] + service_account = module.branch-gke-dev-sa-cicd.0.id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } + depends_on = [module.branch-gke-dev-sa-cicd] +} + +module "branch-gke-prod-cicd-repo" { + source = "../../../modules/source-repository" + for_each = ( + try(local.cicd_repositories.gke_prod.type, null) == "sourcerepo" + ? { 0 = local.cicd_repositories.gke_prod } + : {} + ) + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = [module.branch-gke-prod-sa.0.iam_email] + "roles/source.reader" = [module.branch-gke-prod-sa-cicd.0.iam_email] + } + triggers = { + fast-03-gke-prod = { + filename = ".cloudbuild/workflow.yaml" + included_files = [ + "**/*json", "**/*tf", "**/*yaml", ".cloudbuild/workflow.yaml" + ] + service_account = module.branch-gke-prod-sa-cicd.0.id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } + depends_on = [module.branch-gke-prod-sa-cicd] +} + +# SAs used by CI/CD workflows to impersonate automation SAs + +module "branch-gke-dev-sa-cicd" { + source = "../../../modules/iam-service-account" + for_each = ( + try(local.cicd_repositories.gke_dev.name, null) != null + ? { 0 = local.cicd_repositories.gke_dev } + : {} + ) + project_id = var.automation.project_id + name = "dev-resman-gke-1" + display_name = "Terraform CI/CD GKE development service account." + prefix = var.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam + } + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} + +module "branch-gke-prod-sa-cicd" { + source = "../../../modules/iam-service-account" + for_each = ( + try(local.cicd_repositories.gke_prod.name, null) != null + ? { 0 = local.cicd_repositories.gke_prod } + : {} + ) + project_id = var.automation.project_id + name = "prod-resman-gke-1" + display_name = "Terraform CI/CD GKE production service account." + prefix = var.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam + } + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/cicd-networking.tf b/fast/stages-multitenant/1-resman-tenant/cicd-networking.tf new file mode 100644 index 000000000..dbaf587d6 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/cicd-networking.tf @@ -0,0 +1,94 @@ +/** + * 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. + */ + +# tfdoc:file:description CI/CD resources for the networking branch. + +# source repository + +module "branch-network-cicd-repo" { + source = "../../../modules/source-repository" + for_each = ( + try(local.cicd_repositories.networking.type, null) == "sourcerepo" + ? { 0 = local.cicd_repositories.networking } + : {} + ) + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = [module.branch-network-sa.iam_email] + "roles/source.reader" = [module.branch-network-sa-cicd.0.iam_email] + } + triggers = { + fast-02-networking = { + filename = ".cloudbuild/workflow.yaml" + included_files = ["**/*tf", ".cloudbuild/workflow.yaml"] + service_account = module.branch-network-sa-cicd.0.id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } + depends_on = [module.branch-network-sa-cicd] +} + +# SA used by CI/CD workflows to impersonate automation SAs + +module "branch-network-sa-cicd" { + source = "../../../modules/iam-service-account" + for_each = ( + try(local.cicd_repositories.networking.name, null) != null + ? { 0 = local.cicd_repositories.networking } + : {} + ) + project_id = var.automation.project_id + name = "prod-resman-net-1" + display_name = "Terraform CI/CD stage 2 networking service account." + prefix = var.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam + } + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/cicd-project-factory.tf b/fast/stages-multitenant/1-resman-tenant/cicd-project-factory.tf new file mode 100644 index 000000000..4c46d8585 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/cicd-project-factory.tf @@ -0,0 +1,191 @@ +/** + * 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. + */ + +# tfdoc:file:description CI/CD resources for the teams branch. + +# source repositories + +moved { + from = module.branch-teams-dev-pf-cicd-repo + to = module.branch-pf-dev-cicd-repo +} + +module "branch-pf-dev-cicd-repo" { + source = "../../../modules/source-repository" + for_each = ( + try(local.cicd_repositories.project_factory_dev.type, null) == "sourcerepo" + ? { 0 = local.cicd_repositories.project_factory_dev } + : {} + ) + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = local.branch_optional_sa_lists.pf-dev + "roles/source.reader" = [module.branch-pf-dev-sa-cicd.0.iam_email] + } + triggers = { + fast-03-pf-dev = { + filename = ".cloudbuild/workflow.yaml" + included_files = [ + "**/*json", "**/*tf", "**/*yaml", ".cloudbuild/workflow.yaml" + ] + service_account = module.branch-pf-dev-sa-cicd.0.id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } + depends_on = [module.branch-pf-dev-sa-cicd] +} + +moved { + from = module.branch-teams-prod-pf-cicd-repo + to = module.branch-pf-prod-cicd-repo +} + +module "branch-pf-prod-cicd-repo" { + source = "../../../modules/source-repository" + for_each = ( + try(local.cicd_repositories.project_factory_prod.type, null) == "sourcerepo" + ? { 0 = local.cicd_repositories.project_factory_prod } + : {} + ) + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = local.branch_optional_sa_lists.pf-prod + "roles/source.reader" = [module.branch-pf-prod-sa-cicd.0.iam_email] + } + triggers = { + fast-03-pf-prod = { + filename = ".cloudbuild/workflow.yaml" + included_files = [ + "**/*json", "**/*tf", "**/*yaml", ".cloudbuild/workflow.yaml" + ] + service_account = module.branch-pf-prod-sa-cicd.0.id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } + depends_on = [module.branch-pf-prod-sa-cicd] +} + +# SAs used by CI/CD workflows to impersonate automation SAs + +moved { + from = module.branch-teams-dev-pf-sa-cicd + to = module.branch-pf-dev-sa-cicd +} + +module "branch-pf-dev-sa-cicd" { + source = "../../../modules/iam-service-account" + for_each = ( + try(local.cicd_repositories.project_factory_dev.name, null) != null + ? { 0 = local.cicd_repositories.project_factory_dev } + : {} + ) + project_id = var.automation.project_id + name = "dev-pf-resman-pf-1" + display_name = "Terraform CI/CD project factory development service account." + prefix = var.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam + } + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} + +moved { + from = module.branch-teams-prod-pf-sa-cicd + to = module.branch-pf-prod-sa-cicd +} + +module "branch-pf-prod-sa-cicd" { + source = "../../../modules/iam-service-account" + for_each = ( + try(local.cicd_repositories.project_factory_prod.name, null) != null + ? { 0 = local.cicd_repositories.project_factory_prod } + : {} + ) + project_id = var.automation.project_id + name = "prod-pf-resman-pf-1" + display_name = "Terraform CI/CD project factory production service account." + prefix = var.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam + } + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + var.automation.federated_identity_pool, + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + var.automation.federated_identity_pool, + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/cicd-security.tf b/fast/stages-multitenant/1-resman-tenant/cicd-security.tf new file mode 100644 index 000000000..5cb1581cf --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/cicd-security.tf @@ -0,0 +1,94 @@ +/** + * 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. + */ + +# tfdoc:file:description CI/CD resources for the security branch. + +# source repository + +module "branch-security-cicd-repo" { + source = "../../../modules/source-repository" + for_each = ( + try(local.cicd_repositories.security.type, null) == "sourcerepo" + ? { 0 = local.cicd_repositories.security } + : {} + ) + project_id = var.automation.project_id + name = each.value.name + iam = { + "roles/source.admin" = [module.branch-security-sa.iam_email] + "roles/source.reader" = [module.branch-security-sa-cicd.0.iam_email] + } + triggers = { + fast-02-security = { + filename = ".cloudbuild/workflow.yaml" + included_files = ["**/*tf", ".cloudbuild/workflow.yaml"] + service_account = module.branch-security-sa-cicd.0.id + substitutions = {} + template = { + project_id = null + branch_name = each.value.branch + repo_name = each.value.name + tag_name = null + } + } + } + depends_on = [module.branch-security-sa-cicd] +} + +# SA used by CI/CD workflows to impersonate automation SAs + +module "branch-security-sa-cicd" { + source = "../../../modules/iam-service-account" + for_each = ( + try(local.cicd_repositories.security.name, null) != null + ? { 0 = local.cicd_repositories.security } + : {} + ) + project_id = var.automation.project_id + name = "prod-resman-sec-1" + display_name = "Terraform CI/CD stage 2 security service account." + prefix = var.prefix + iam = ( + each.value.type == "sourcerepo" + # used directly from the cloud build trigger for source repos + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam + } + # impersonated via workload identity federation for external repos + : { + "roles/iam.workloadIdentityUser" = [ + each.value.branch == null + ? format( + local.cicd_identity_providers[each.value.identity_provider].principalset_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name + ) + : format( + local.cicd_identity_providers[each.value.identity_provider].principal_tpl, + local.cicd_identity_pools[each.value.identity_provider], + each.value.name, + each.value.branch + ) + ] + } + ) + iam_project_roles = { + (var.automation.project_id) = ["roles/logging.logWriter"] + } + iam_storage_roles = { + (var.automation.outputs_bucket) = ["roles/storage.objectViewer"] + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/data/org-policies/compute.yaml b/fast/stages-multitenant/1-resman-tenant/data/org-policies/compute.yaml new file mode 100644 index 000000000..0d27ac426 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/data/org-policies/compute.yaml @@ -0,0 +1,73 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +compute.disableGuestAttributesAccess: + enforce: true + +compute.requireOsLogin: + enforce: true + +compute.restrictLoadBalancerCreationForTypes: + allow: + values: + - in:INTERNAL + +compute.skipDefaultNetworkCreation: + enforce: true + +compute.vmExternalIpAccess: + deny: + all: true + + +# compute.disableInternetNetworkEndpointGroup: +# enforce: true + +# compute.disableNestedVirtualization: +# enforce: true + +# compute.disableSerialPortAccess: +# enforce: true + +# compute.restrictCloudNATUsage: +# deny: +# all: true + +# compute.restrictDedicatedInterconnectUsage: +# deny: +# all: true + +# compute.restrictPartnerInterconnectUsage: +# deny: +# all: true + +# compute.restrictProtocolForwardingCreationForTypes: +# deny: +# all: true + +# compute.restrictSharedVpcHostProjects: +# deny: +# all: true + +# compute.restrictSharedVpcSubnetworks: +# deny: +# all: true + +# compute.restrictVpcPeering: +# deny: +# all: true + +# compute.restrictVpnPeerIPs: +# deny: +# all: true + +# compute.restrictXpnProjectLienRemoval: +# enforce: true + +# compute.setNewProjectDefaultToZonalDNSOnly: +# enforce: true + +# compute.vmCanIpForward: +# deny: +# all: true diff --git a/fast/stages-multitenant/1-resman-tenant/data/org-policies/iam.yaml b/fast/stages-multitenant/1-resman-tenant/data/org-policies/iam.yaml new file mode 100644 index 000000000..4d83f827f --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/data/org-policies/iam.yaml @@ -0,0 +1,12 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +iam.automaticIamGrantsForDefaultServiceAccounts: + enforce: true + +iam.disableServiceAccountKeyCreation: + enforce: true + +iam.disableServiceAccountKeyUpload: + enforce: true diff --git a/fast/stages-multitenant/1-resman-tenant/data/org-policies/serverless.yaml b/fast/stages-multitenant/1-resman-tenant/data/org-policies/serverless.yaml new file mode 100644 index 000000000..de62e6c70 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/data/org-policies/serverless.yaml @@ -0,0 +1,26 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +run.allowedIngress: + allow: + values: + - is:internal + +# run.allowedVPCEgress: +# allow: +# values: +# - is:private-ranges-only + +# cloudfunctions.allowedIngressSettings: +# allow: +# values: +# - is:ALLOW_INTERNAL_ONLY + +# cloudfunctions.allowedVpcConnectorEgressSettings: +# allow: +# values: +# - is:PRIVATE_RANGES_ONLY + +# cloudfunctions.requireVPCConnector: +# enforce: true diff --git a/fast/stages-multitenant/1-resman-tenant/data/org-policies/sql.yaml b/fast/stages-multitenant/1-resman-tenant/data/org-policies/sql.yaml new file mode 100644 index 000000000..88b84d9d5 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/data/org-policies/sql.yaml @@ -0,0 +1,9 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +sql.restrictAuthorizedNetworks: + enforce: true + +sql.restrictPublicIp: + enforce: true diff --git a/fast/stages-multitenant/1-resman-tenant/data/org-policies/storage.yaml b/fast/stages-multitenant/1-resman-tenant/data/org-policies/storage.yaml new file mode 100644 index 000000000..6c0a673f3 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/data/org-policies/storage.yaml @@ -0,0 +1,6 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +storage.uniformBucketLevelAccess: + enforce: true diff --git a/fast/stages/01-resman/diagram.png b/fast/stages-multitenant/1-resman-tenant/diagram.png similarity index 100% rename from fast/stages/01-resman/diagram.png rename to fast/stages-multitenant/1-resman-tenant/diagram.png diff --git a/fast/stages/01-resman/diagram.svg b/fast/stages-multitenant/1-resman-tenant/diagram.svg similarity index 100% rename from fast/stages/01-resman/diagram.svg rename to fast/stages-multitenant/1-resman-tenant/diagram.svg diff --git a/fast/stages-multitenant/1-resman-tenant/main.tf b/fast/stages-multitenant/1-resman-tenant/main.tf new file mode 100644 index 000000000..76c046396 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/main.tf @@ -0,0 +1,79 @@ +/** + * 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 { + automation_resman_sa_iam = [ + "serviceAccount:${var.automation.service_accounts.resman}" + ] + automation_sas_iam = { + for k, v in var.automation.service_accounts : + k => v == null ? null : "serviceAccount:${v}" + } + branch_optional_sa_lists = { + dp-dev = compact([local.automation_sas_iam.dp-dev]) + dp-prod = compact([local.automation_sas_iam.dp-prod]) + gke-dev = compact([local.automation_sas_iam.gke-dev]) + gke-prod = compact([local.automation_sas_iam.gke-prod]) + pf-dev = compact([local.automation_sas_iam.pf-dev]) + pf-prod = compact([local.automation_sas_iam.pf-prod]) + } + # derive identity pool names from identity providers for easy reference + cicd_identity_pools = { + for k, v in local.cicd_identity_providers : + k => split("/providers/", v.name)[0] + } + cicd_identity_providers = coalesce( + try(var.automation.federated_identity_providers, null), {} + ) + cicd_repositories = { + for k, v in coalesce(var.cicd_repositories, {}) : k => v + if( + v != null && + ( + try(v.type, null) == "sourcerepo" + || + contains( + keys(local.cicd_identity_providers), + coalesce(try(v.identity_provider, null), ":") + ) + ) && + fileexists("${path.module}/templates/workflow-${try(v.type, "")}.yaml") + ) + } + cicd_workflow_var_files = { + stage_2 = [ + "0-bootstrap-tenant.auto.tfvars.json", + ] + stage_3 = [ + "0-bootstrap-tenant.auto.tfvars.json", + "2-networking.auto.tfvars.json", + "2-security.auto.tfvars.json" + ] + } + custom_roles = coalesce(var.custom_roles, {}) + gcs_storage_class = ( + length(split("-", var.locations.gcs)) < 2 + ? "MULTI_REGIONAL" + : "REGIONAL" + ) + groups = { + for k, v in var.groups : + k => v == null ? null : "${v}@${var.organization.domain}" + } + groups_iam = { + for k, v in local.groups : k => v != null ? "group:${v}" : null + } +} diff --git a/fast/stages-multitenant/1-resman-tenant/outputs-files.tf b/fast/stages-multitenant/1-resman-tenant/outputs-files.tf new file mode 100644 index 000000000..29d5ed460 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/outputs-files.tf @@ -0,0 +1,46 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Output files persistence to local filesystem. + +locals { + outputs_root = join("/", [ + try(pathexpand(var.outputs_location), ""), + "tenants", + var.short_name + ]) +} + +resource "local_file" "providers" { + for_each = var.outputs_location == null ? {} : local.providers + file_permission = "0644" + filename = "${local.outputs_root}/providers/${each.key}-providers.tf" + content = try(each.value, null) +} + +resource "local_file" "tfvars" { + count = var.outputs_location == null ? 0 : 1 + file_permission = "0644" + filename = "${local.outputs_root}/tfvars/1-resman.auto.tfvars.json" + content = jsonencode(local.tfvars) +} + +resource "local_file" "workflows" { + for_each = var.outputs_location == null ? {} : local.cicd_workflows + file_permission = "0644" + filename = "${local.outputs_root}/workflows/${replace(each.key, "_", "-")}-workflow.yaml" + content = try(each.value, null) +} diff --git a/fast/stages-multitenant/1-resman-tenant/outputs-gcs.tf b/fast/stages-multitenant/1-resman-tenant/outputs-gcs.tf new file mode 100644 index 000000000..6b0fc89cb --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/outputs-gcs.tf @@ -0,0 +1,37 @@ +/** + * 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. + */ + +# tfdoc:file:description Output files persistence to automation GCS bucket. + +resource "google_storage_bucket_object" "providers" { + for_each = local.providers + bucket = var.automation.outputs_bucket + name = "providers/${each.key}-providers.tf" + content = each.value +} + +resource "google_storage_bucket_object" "tfvars" { + bucket = var.automation.outputs_bucket + name = "tfvars/1-resman.auto.tfvars.json" + content = jsonencode(local.tfvars) +} + +resource "google_storage_bucket_object" "workflows" { + for_each = local.cicd_workflows + bucket = var.automation.outputs_bucket + name = "workflows/${replace(each.key, "_", "-")}-workflow.yaml" + content = each.value +} diff --git a/fast/stages-multitenant/1-resman-tenant/outputs.tf b/fast/stages-multitenant/1-resman-tenant/outputs.tf new file mode 100644 index 000000000..592f995ea --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/outputs.tf @@ -0,0 +1,311 @@ +/** + * 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 { + _tpl_providers = "${path.module}/templates/providers.tf.tpl" + cicd_workflow_attrs = { + data_platform_dev = { + service_account = try(module.branch-dp-dev-sa-cicd.0.email, null) + tf_providers_file = "3-data-platform-dev-providers.tf" + tf_var_files = local.cicd_workflow_var_files.stage_3 + } + data_platform_prod = { + service_account = try(module.branch-dp-prod-sa-cicd.0.email, null) + tf_providers_file = "3-data-platform-prod-providers.tf" + tf_var_files = local.cicd_workflow_var_files.stage_3 + } + gke_dev = { + service_account = try(module.branch-gke-dev-sa-cicd.0.email, null) + tf_providers_file = "3-gke-dev-providers.tf" + tf_var_files = local.cicd_workflow_var_files.stage_3 + } + gke_prod = { + service_account = try(module.branch-gke-prod-sa-cicd.0.email, null) + tf_providers_file = "3-gke-prod-providers.tf" + tf_var_files = local.cicd_workflow_var_files.stage_3 + } + networking = { + service_account = try(module.branch-network-sa-cicd.0.email, null) + tf_providers_file = "2-networking-providers.tf" + tf_var_files = local.cicd_workflow_var_files.stage_2 + } + project_factory_dev = { + service_account = try(module.branch-pf-dev-sa-cicd.0.email, null) + tf_providers_file = "3-project-factory-dev-providers.tf" + tf_var_files = local.cicd_workflow_var_files.stage_3 + } + project_factory_prod = { + service_account = try(module.branch-pf-prod-sa-cicd.0.email, null) + tf_providers_file = "3-project-factory-prod-providers.tf" + tf_var_files = local.cicd_workflow_var_files.stage_3 + } + security = { + service_account = try(module.branch-security-sa-cicd.0.email, null) + tf_providers_file = "2-security-providers.tf" + tf_var_files = local.cicd_workflow_var_files.stage_2 + } + } + cicd_workflows = { + for k, v in local.cicd_repositories : k => templatefile( + "${path.module}/templates/workflow-${v.type}.yaml", + merge(local.cicd_workflow_attrs[k], { + identity_provider = try( + local.cicd_identity_providers[v.identity_provider].name, null + ) + outputs_bucket = var.automation.outputs_bucket + stage_name = k + }) + ) + } + folder_ids = merge( + { + data-platform-dev = try(module.branch-dp-dev-folder.0.id, null) + data-platform-prod = try(module.branch-dp-prod-folder.0.id, null) + gke-dev = try(module.branch-gke-dev-folder.0.id, null) + gke-prod = try(module.branch-gke-prod-folder.0.id, null) + networking = module.branch-network-folder.id + networking-dev = module.branch-network-dev-folder.id + networking-prod = module.branch-network-prod-folder.id + sandbox = try(module.branch-sandbox-folder.0.id, null) + security = module.branch-security-folder.id + teams = try(module.branch-teams-folder.0.id, null) + }, + { + for k, v in module.branch-teams-team-folder : + "team-${k}" => v.id + }, + { + for k, v in module.branch-teams-team-dev-folder : + "team-${k}-dev" => v.id + }, + { + for k, v in module.branch-teams-team-prod-folder : + "team-${k}-prod" => v.id + } + ) + providers = merge( + { + "2-networking" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-network-gcs.name + name = "networking" + sa = module.branch-network-sa.email + }) + "2-security" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-security-gcs.name + name = "security" + sa = module.branch-security-sa.email + }) + }, + !var.fast_features.data_platform ? {} : { + "3-data-platform-dev" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-dp-dev-gcs.0.name + name = "dp-dev" + sa = module.branch-dp-dev-sa.0.email + }) + "3-data-platform-prod" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-dp-prod-gcs.0.name + name = "dp-prod" + sa = module.branch-dp-prod-sa.0.email + }) + }, + !var.fast_features.gke ? {} : { + "3-gke-dev" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-gke-dev-gcs.0.name + name = "gke-dev" + sa = module.branch-gke-dev-sa.0.email + }) + "3-gke-prod" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-gke-prod-gcs.0.name + name = "gke-prod" + sa = module.branch-gke-prod-sa.0.email + }) + }, + !var.fast_features.project_factory ? {} : { + "3-project-factory-dev" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-pf-dev-gcs.0.name + name = "team-dev" + sa = var.automation.service_accounts.pf-dev + }) + "3-project-factory-prod" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-pf-prod-gcs.0.name + name = "team-prod" + sa = var.automation.service_accounts.pf-prod + }) + }, + !var.fast_features.sandbox ? {} : { + "9-sandbox" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-sandbox-gcs.0.name + name = "sandbox" + sa = var.automation.service_accounts.sandbox + }) + }, + !var.fast_features.teams ? {} : merge( + { + "3-teams" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-teams-gcs.0.name + name = "teams" + sa = module.branch-teams-sa.0.email + }) + }, + { + for k, v in module.branch-teams-team-sa : + "3-teams-${k}" => templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-teams-team-gcs[k].name + name = "teams" + sa = v.email + }) + } + ) + ) + tfvars = { + folder_ids = local.folder_ids + } +} + +output "cicd_repositories" { + description = "WIF configuration for CI/CD repositories." + value = { + for k, v in local.cicd_repositories : k => { + branch = v.branch + name = v.name + provider = try( + local.cicd_identity_providers[v.identity_provider].name, null + ) + service_account = local.cicd_workflow_attrs[k].service_account + } if v != null + } +} + +output "dataplatform" { + description = "Data for the Data Platform stage." + value = !var.fast_features.data_platform ? {} : { + dev = { + folder = module.branch-dp-dev-folder.0.id + gcs_bucket = module.branch-dp-dev-gcs.0.name + service_account = module.branch-dp-dev-sa.0.email + } + prod = { + folder = module.branch-dp-prod-folder.0.id + gcs_bucket = module.branch-dp-prod-gcs.0.name + service_account = module.branch-dp-prod-sa.0.email + } + } +} + +output "gke_multitenant" { + # tfdoc:output:consumers 03-gke-multitenant + description = "Data for the GKE multitenant stage." + value = ( + var.fast_features.gke + ? { + "dev" = { + folder = module.branch-gke-dev-folder.0.id + gcs_bucket = module.branch-gke-dev-gcs.0.name + service_account = module.branch-gke-dev-sa.0.email + } + "prod" = { + folder = module.branch-gke-prod-folder.0.id + gcs_bucket = module.branch-gke-prod-gcs.0.name + service_account = module.branch-gke-prod-sa.0.email + } + } + : {} + ) +} + +output "networking" { + description = "Data for the networking stage." + value = { + folder = module.branch-network-folder.id + gcs_bucket = module.branch-network-gcs.name + service_account = module.branch-network-sa.iam_email + } +} + +output "project_factories" { + description = "Data for the project factories stage." + value = !var.fast_features.project_factory ? {} : { + dev = { + bucket = module.branch-pf-dev-gcs.0.name + sa = var.automation.service_accounts.pf-dev + } + prod = { + bucket = module.branch-pf-prod-gcs.0.name + sa = var.automation.service_accounts.pf-prod + } + } +} + +# ready to use provider configurations for subsequent stages +output "providers" { + # tfdoc:output:consumers 02-networking 02-security 03-dataplatform xx-sandbox xx-teams + description = "Terraform provider files for this stage and dependent stages." + sensitive = true + value = local.providers +} + +output "sandbox" { + # tfdoc:output:consumers xx-sandbox + description = "Data for the sandbox stage." + value = ( + var.fast_features.sandbox + ? { + folder = module.branch-sandbox-folder.0.id + gcs_bucket = module.branch-sandbox-gcs.0.name + service_account = var.automation.service_accounts.sandbox + } + : null + ) +} + +output "security" { + # tfdoc:output:consumers 02-security + description = "Data for the networking stage." + value = { + folder = module.branch-security-folder.id + gcs_bucket = module.branch-security-gcs.name + service_account = module.branch-security-sa.iam_email + } +} + +output "teams" { + description = "Data for the teams stage." + value = { + for k, v in module.branch-teams-team-folder : k => { + folder = v.id + gcs_bucket = module.branch-teams-team-gcs[k].name + service_account = module.branch-teams-team-sa[k].email + } + } +} + +# ready to use variable values for subsequent stages +output "tfvars" { + description = "Terraform variable files for the following stages." + sensitive = true + value = local.tfvars +} diff --git a/fast/stages-multitenant/1-resman-tenant/root_node.tf b/fast/stages-multitenant/1-resman-tenant/root_node.tf new file mode 100644 index 000000000..5b83d2dd2 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/root_node.tf @@ -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. + */ + +# tfdoc:file:description Tenant root folder configuration. + +module "root-folder" { + source = "../../../modules/folder" + id = var.root_node + folder_create = var.test_skip_data_sources + # start test attributes + parent = ( + var.test_skip_data_sources ? "organizations/${var.organization.id}" : null + ) + name = var.test_skip_data_sources ? "Test" : null + # end test attributes + iam_additive = { + "roles/accesscontextmanager.policyAdmin" = [ + local.automation_sas_iam.security + ] + "roles/compute.orgFirewallPolicyAdmin" = [ + local.automation_sas_iam.networking + ] + "roles/compute.xpnAdmin" = [ + local.automation_sas_iam.networking + ] + } + org_policies_data_path = var.organization_policy_data_path +} diff --git a/fast/stages-multitenant/1-resman-tenant/templates/providers.tf.tpl b/fast/stages-multitenant/1-resman-tenant/templates/providers.tf.tpl new file mode 100644 index 000000000..993c78ca4 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/templates/providers.tf.tpl @@ -0,0 +1,33 @@ +/** + * 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. + */ + +terraform { + backend "gcs" { + bucket = "${bucket}" + impersonate_service_account = "${sa}" + %{~ if backend_extra != null ~} + ${indent(4, backend_extra)} + %{~ endif ~} + } +} +provider "google" { + impersonate_service_account = "${sa}" +} +provider "google-beta" { + impersonate_service_account = "${sa}" +} + +# end provider.tf for ${name} diff --git a/fast/stages-multitenant/1-resman-tenant/templates/workflow-github.yaml b/fast/stages-multitenant/1-resman-tenant/templates/workflow-github.yaml new file mode 100644 index 000000000..e96699092 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/templates/workflow-github.yaml @@ -0,0 +1,198 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "FAST ${stage_name} stage" + +on: + pull_request: + branches: + - main + types: + - closed + - opened + - synchronize + +env: + FAST_OUTPUTS_BUCKET: ${outputs_bucket} + FAST_SERVICE_ACCOUNT: ${service_account} + FAST_WIF_PROVIDER: ${identity_provider} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + TF_PROVIDERS_FILE: ${tf_providers_file} + TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + TF_VERSION: 1.3.2 + +jobs: + fast-pr: + permissions: + contents: read + id-token: write + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - id: checkout + name: Checkout repository + uses: actions/checkout@v3 + + # set up SSH key authentication to the modules repository + - id: ssh-config + name: Configure SSH authentication + run: | + ssh-agent -a "$SSH_AUTH_SOCK" > /dev/null + ssh-add - <<< "$${{ secrets.CICD_MODULES_KEY }}" + + # set up authentication via Workload identity Federation + - id: gcp-auth + name: Authenticate to Google Cloud + uses: google-github-actions/auth@v0 + with: + workload_identity_provider: $${{ env.FAST_WIF_PROVIDER }} + service_account: $${{ env.FAST_SERVICE_ACCOUNT }} + access_token_lifetime: 3600s + + - id: gcp-sdk + name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v0 + with: + install_components: alpha + + # copy provider and tfvars files + - id: tf-config + name: Copy Terraform output files + run: | + gcloud alpha storage cp -r \ + "gs://$${{env.FAST_OUTPUTS_BUCKET}}/providers/$${{env.TF_PROVIDERS_FILE}}" ./ + gcloud alpha storage cp -r \ + "gs://$${{env.FAST_OUTPUTS_BUCKET}}/tfvars" ./ + for f in $${{env.TF_VAR_FILES}}; do + ln -s "tfvars/$f" ./ + done + + - id: tf-setup + name: Set up Terraform + uses: hashicorp/setup-terraform@v2.0.3 + with: + terraform_version: $${{ env.TF_VERSION }} + + # run Terraform init/validate/plan + - id: tf-init + name: Terraform init + continue-on-error: true + run: | + terraform init -no-color + + - id: tf-validate + name: Terraform validate + continue-on-error: true + run: terraform validate -no-color + + - id: tf-plan + name: Terraform plan + continue-on-error: true + run: | + terraform plan -input=false -out ../plan.out -no-color + + - id: tf-apply + if: github.event.pull_request.merged == true && success() + name: Terraform apply + continue-on-error: true + run: | + terraform apply -input=false -auto-approve -no-color ../plan.out + + - id: pr-comment + name: Post comment to Pull Request + continue-on-error: true + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' + env: + PLAN: $${{ steps.tf-plan.outputs.stdout }}\n$${{ steps.tf-plan.outputs.stderr }} + with: + script: | + const output = `### Terraform Initialization \`$${{ steps.tf-init.outcome }}\` + + ### Terraform Validation \`$${{ steps.tf-validate.outcome }}\` + +
Validation Output + + \`\`\`\n + $${{ steps.tf-validate.outputs.stdout }} + \`\`\` + +
+ + ### Terraform Plan \`$${{ steps.tf-plan.outcome }}\` + +
Show Plan + + \`\`\`\n + $${process.env.PLAN.split('\n').filter(l => l.match(/^([A-Z\s].*|)$$/)).join('\n')} + \`\`\` + +
+ + ### Terraform Apply \`$${{ steps.tf-apply.outcome }}\` + + *Pusher: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`, Working Directory: \`$${{ env.tf_actions_working_dir }}\`, Workflow: \`$${{ github.workflow }}\`*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - id: pr-short-comment + name: Post comment to Pull Request + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' && steps.pr-comment.outcome != 'success' + with: + script: | + const output = `### Terraform Initialization \`$${{ steps.tf-init.outcome }}\` + + ### Terraform Validation \`$${{ steps.tf-validate.outcome }}\` + + ### Terraform Plan \`$${{ steps.tf-plan.outcome }}\` + + Plan output is in the action log. + + ### Terraform Apply \`$${{ steps.tf-apply.outcome }}\` + + *Pusher: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`, Working Directory: \`$${{ env.tf_actions_working_dir }}\`, Workflow: \`$${{ github.workflow }}\`*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - id: check-init + name: Check init failure + if: steps.tf-init.outcome != 'success' + run: exit 1 + + - id: check-validate + name: Check validate failure + if: steps.tf-validate.outcome != 'success' + run: exit 1 + + - id: check-plan + name: Check plan failure + if: steps.tf-plan.outcome != 'success' + run: exit 1 + + - id: check-apply + name: Check apply failure + if: github.event.pull_request.merged == true && steps.tf-apply.outcome != 'success' + run: exit 1 diff --git a/fast/stages-multitenant/1-resman-tenant/templates/workflow-gitlab.yaml b/fast/stages-multitenant/1-resman-tenant/templates/workflow-gitlab.yaml new file mode 100644 index 000000000..8981e70b3 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/templates/workflow-gitlab.yaml @@ -0,0 +1,120 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +default: + before_script: + - echo "$${CI_JOB_JWT_V2}" > token.txt + image: + name: hashicorp/terraform + entrypoint: + - "/usr/bin/env" + - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +variables: + GOOGLE_CREDENTIALS: cicd-sa-credentials.json + FAST_OUTPUTS_BUCKET: ${outputs_bucket} + FAST_SERVICE_ACCOUNT: ${service_account} + FAST_WIF_PROVIDER: ${identity_provider} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + TF_PROVIDERS_FILE: ${tf_providers_file} + TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + +stages: + - gcp-auth + - tf-files + - tf-plan + - tf-apply + +cache: + key: gcp-auth + paths: + - cicd-sa-credentials.json + - .tf-setup + +gcp-auth: + image: + name: google/cloud-sdk:slim + stage: gcp-auth + script: + - | + gcloud iam workload-identity-pools create-cred-config \ + $${FAST_WIF_PROVIDER} \ + --service-account=$${FAST_SERVICE_ACCOUNT} \ + --service-account-token-lifetime-seconds=3600 \ + --output-file=$${GOOGLE_CREDENTIALS} \ + --credential-source-file=token.txt +tf-files: + dependencies: + - gcp-auth + image: + name: google/cloud-sdk:slim + stage: tf-files + script: + # - gcloud components install -q alpha + - gcloud config set auth/credential_file_override $${GOOGLE_CREDENTIALS} + - mkdir -p .tf-setup + - | + gcloud alpha storage cp -r \ + "gs://$${FAST_OUTPUTS_BUCKET}/providers/$${TF_PROVIDERS_FILE}" .tf-setup/ + - | + gcloud alpha storage cp -r \ + "gs://$${FAST_OUTPUTS_BUCKET}/tfvars" .tf-setup/ + +tf-plan: + # uncomment the following lines and set the SSH key secret for private modules repo + # before_script: + # - | + # ssh-agent -a $SSH_AUTH_SOCK > /dev/null + # echo "$CICD_MODULES_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null + # mkdir -p ~/.ssh + # ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts + # ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts + stage: tf-plan + script: + - cp .tf-setup/$${TF_PROVIDERS_FILE} ./ + - | + for f in $${TF_VAR_FILES}; do + ln -s ".tf-setup/tfvars/$f" ./ + done + - terraform init + - terraform validate + - terraform plan + dependencies: + - tf-files + +tf-apply: + # uncomment the following lines and set the SSH key secret for private modules repo + # before_script: + # - | + # ssh-agent -a $SSH_AUTH_SOCK > /dev/null + # echo "$CICD_MODULES_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null + # mkdir -p ~/.ssh + # ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts + # ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts + stage: tf-apply + script: + - cp .tf-setup/$${TF_PROVIDERS_FILE} ./ + - | + for f in $${TF_VAR_FILES}; do + ln -s ".tf-setup/tfvars/$f" ./ + done + - terraform init + - terraform validate + - terraform apply -input=false -auto-approve + dependencies: + - tf-files + when: manual + only: + variables: + - $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH diff --git a/fast/stages-multitenant/1-resman-tenant/templates/workflow-sourcerepo.yaml b/fast/stages-multitenant/1-resman-tenant/templates/workflow-sourcerepo.yaml new file mode 100644 index 000000000..446c9c960 --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/templates/workflow-sourcerepo.yaml @@ -0,0 +1,98 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +steps: + - name: alpine:3 + id: tf-download + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + mkdir -p /builder/home/.local/bin + wget https://releases.hashicorp.com/terraform/$${_TF_VERSION}/terraform_$${_TF_VERSION}_linux_amd64.zip + unzip terraform_$${_TF_VERSION}_linux_amd64.zip -d /builder/home/.local/bin + rm terraform_$${_TF_VERSION}_linux_amd64.zip + chmod 755 /builder/home/.local/bin/terraform + - name: alpine:3 + id: tf-check-format + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform fmt -recursive -check /workspace/ + - name: gcr.io/google.com/cloudsdktool/cloud-sdk:alpine + id: tf-files + entrypoint: bash + args: + - -eEuo + - pipefail + - -c + - |- + /google-cloud-sdk/bin/gsutil cp \ + gs://$${_FAST_OUTPUTS_BUCKET}/providers/$${_TF_PROVIDERS_FILE} ./ + /google-cloud-sdk/bin/gsutil cp -r \ + gs://$${_FAST_OUTPUTS_BUCKET}/tfvars ./ + for f in $${_TF_VAR_FILES}; do + ln -s tfvars/$f ./ + done + - name: alpine:3 + id: tf-init + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform init -no-color + - name: alpine:3 + id: tf-check-validate + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform validate -no-color + - name: alpine:3 + id: tf-plan + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform plan -no-color -input=false -out plan.out + # store artifact and ask for approval here if needed + - name: alpine:3 + id: tf-apply + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform apply -no-color -input=false -auto-approve plan.out +options: + env: + - PATH=/usr/local/bin:/usr/bin:/bin:/builder/home/.local/bin + logging: CLOUD_LOGGING_ONLY +substitutions: + _FAST_OUTPUTS_BUCKET: ${outputs_bucket} + _TF_PROVIDERS_FILE: ${tf_providers_file} + _TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + _TF_VERSION: 1.3.2 diff --git a/fast/stages-multitenant/1-resman-tenant/variables.tf b/fast/stages-multitenant/1-resman-tenant/variables.tf new file mode 100644 index 000000000..0229dd78f --- /dev/null +++ b/fast/stages-multitenant/1-resman-tenant/variables.tf @@ -0,0 +1,281 @@ +/** + * 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. + */ + +# defaults for variables marked with global tfdoc annotations, can be set via +# the tfvars file generated in stage 00 and stored in its outputs + +variable "automation" { + # tfdoc:variable:source 0-bootstrap + description = "Automation resources created by the bootstrap stage." + type = object({ + outputs_bucket = string + project_id = string + project_number = string + federated_identity_pools = list(string) + federated_identity_providers = map(object({ + issuer = string + issuer_uri = string + name = string + principal_tpl = string + principalset_tpl = string + })) + service_accounts = object({ + networking = string + resman = string + security = string + dp-dev = optional(string) + dp-prod = optional(string) + gke-dev = optional(string) + gke-prod = optional(string) + pf-dev = optional(string) + pf-prod = optional(string) + sandbox = optional(string) + teams = optional(string) + }) + }) +} + +variable "billing_account" { + # tfdoc:variable:source 0-bootstrap + description = "Billing account id. If billing account is not part of the same org set `is_org_level` to false." + type = object({ + id = string + is_org_level = optional(bool, true) + }) + validation { + condition = var.billing_account.is_org_level != null + error_message = "Invalid `null` value for `billing_account.is_org_level`." + } +} + +variable "cicd_repositories" { + description = "CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed." + type = object({ + data_platform_dev = object({ + branch = string + identity_provider = string + name = string + type = string + }) + data_platform_prod = object({ + branch = string + identity_provider = string + name = string + type = string + }) + gke_dev = object({ + branch = string + identity_provider = string + name = string + type = string + }) + gke_prod = object({ + branch = string + identity_provider = string + name = string + type = string + }) + networking = object({ + branch = string + identity_provider = string + name = string + type = string + }) + project_factory_dev = object({ + branch = string + identity_provider = string + name = string + type = string + }) + project_factory_prod = object({ + branch = string + identity_provider = string + name = string + type = string + }) + security = object({ + branch = string + identity_provider = string + name = string + type = string + }) + }) + default = null + validation { + condition = alltrue([ + for k, v in coalesce(var.cicd_repositories, {}) : + v == null || try(v.name, null) != null + ]) + error_message = "Non-null repositories need a non-null name." + } + validation { + condition = alltrue([ + for k, v in coalesce(var.cicd_repositories, {}) : + v == null || ( + try(v.identity_provider, null) != null + || + try(v.type, null) == "sourcerepo" + ) + ]) + error_message = "Non-null repositories need a non-null provider unless type is 'sourcerepo'." + } + validation { + condition = alltrue([ + for k, v in coalesce(var.cicd_repositories, {}) : + v == null || ( + contains(["github", "gitlab", "sourcerepo"], coalesce(try(v.type, null), "null")) + ) + ]) + error_message = "Invalid repository type, supported types: 'github' 'gitlab' or 'sourcerepo'." + } +} + +variable "custom_roles" { + # tfdoc:variable:source 0-bootstrap + description = "Custom roles defined at the org level, in key => id format." + type = object({ + service_project_network_admin = string + }) + default = null +} + +variable "data_dir" { + description = "Relative path for the folder storing configuration data." + type = string + default = "data" +} + +variable "fast_features" { + # tfdoc:variable:source 0-0-bootstrap + description = "Selective control for top-level FAST features." + type = object({ + data_platform = optional(bool, false) + gke = optional(bool, false) + project_factory = optional(bool, false) + sandbox = optional(bool, false) + teams = optional(bool, false) + }) + default = {} + nullable = false +} + +variable "groups" { + # tfdoc:variable:source 0-bootstrap + # https://cloud.google.com/docs/enterprise/setup-checklist + description = "Group names to grant organization-level permissions." + type = object({ + gcp-devops = optional(string) + gcp-network-admins = optional(string) + gcp-security-admins = optional(string) + }) + default = {} + nullable = false +} + +variable "locations" { + # tfdoc:variable:source 0-bootstrap + description = "Optional locations for GCS, BigQuery, and logging buckets created here." + type = object({ + bq = string + gcs = string + logging = string + pubsub = list(string) + }) + default = { + bq = "EU" + gcs = "EU" + logging = "global" + pubsub = [] + } + nullable = false +} + +variable "organization" { + # tfdoc:variable:source 0-bootstrap + description = "Organization details." + type = object({ + domain = string + id = number + customer_id = string + }) +} + +variable "organization_policy_data_path" { + description = "Path for the data folder used by the organization policies factory." + type = string + default = null +} + +variable "outputs_location" { + description = "Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable." + type = string + default = null +} + +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." + } +} + +variable "root_node" { + description = "Root folder node for the tenant, in folders/nnnnnn format." + type = string +} + +variable "short_name" { + description = "Short name used to identify the tenant." + type = string +} + +variable "tags" { + description = "Resource management tags." + type = object({ + keys = object({ + context = string + environment = string + tenant = string + }) + names = object({ + context = string + environment = string + tenant = string + }) + values = map(string) + }) + nullable = false +} + +variable "team_folders" { + description = "Team folders to be created. Format is described in a code comment." + type = map(object({ + descriptive_name = string + group_iam = map(list(string)) + impersonation_groups = list(string) + })) + default = null +} + +variable "test_skip_data_sources" { + description = "Used when testing to bypass data sources." + type = bool + default = false +} diff --git a/fast/stages-multitenant/README.md b/fast/stages-multitenant/README.md new file mode 100644 index 000000000..27ce17dd6 --- /dev/null +++ b/fast/stages-multitenant/README.md @@ -0,0 +1,27 @@ +# FAST multitenant stages + +The stages in this folder set up separate resource hierarchies inside the same organization that are fully FAST-compliant, and allow each tenant to run and manage their own networking, security, or application-level stages. They are designed to be used where a high degree of autonomy is needed for each tenant, for example individual subsidiaries of a large corporation all sharing the same GCP organization. + +The multitenant stages have the following characteristics: + +- they support one tenant at a time, so one copy of both stages is needed for each tenant +- they have the organization-level bootstrap and resource management stages as prerequisite +- they are logically equivalent to the respective organization-level stages but behave slightly differently, as they actively minimize access and changes to organization or shared resources + +Once both tenant-level stages are run, a hierarchy and a set of resources is available for the new tenant, including a separate automation project, service accounts for subsequent stages, etc. + +The tenant-level stages require that organization-level stage 0 (bootstrap) and 1 (resource management) have been applied. Their position and role in the FAST stage flow is shown in the following diagram: + +

+ Stages diagram +

+ +## Tenant bootstrap (0) + +This stage creates the top-level root folder and tag for the tenant, and the tenant-level automation project and automation service accounts. It also sets up billing and organization-level roles for the tenant administrators group and the automation service accounts. As in the organizational-level stages, it can optionally set up CI/CD for itself and the tenant resource management stage. + +This stage is run with the organization-level resource management service account as it leverages its permissions, and is the bridge between the organization-level stages and the tenant stages which are effectively decoupled from the rest of the organization. + +## Tenant resource management (1) + +This stage populates the resource hierarchy rooted in the top-level tenant folder, assigns roles to the tenant automation service accounts, and optionally sets up CI/CD for the following stages. It is functionally equivalent to the organization-level resource management stage, but runs with a tenant-specific service account and has no control over resources outside of the tenant context. diff --git a/fast/stages-multitenant/diagram.png b/fast/stages-multitenant/diagram.png new file mode 100644 index 000000000..940fa3c36 Binary files /dev/null and b/fast/stages-multitenant/diagram.png differ diff --git a/fast/stages-multitenant/stages.png b/fast/stages-multitenant/stages.png new file mode 100644 index 000000000..dc2acc0a2 Binary files /dev/null and b/fast/stages-multitenant/stages.png differ diff --git a/fast/stages-multitenant/stages.svg b/fast/stages-multitenant/stages.svg new file mode 100644 index 000000000..157453f0d --- /dev/null +++ b/fast/stages-multitenant/stages.svg @@ -0,0 +1,1278 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fast/stages.png b/fast/stages.png index 238ad5a04..d6c3e386a 100644 Binary files a/fast/stages.png and b/fast/stages.png differ diff --git a/fast/stages.svg b/fast/stages.svg index f952c8844..b6855676f 100644 --- a/fast/stages.svg +++ b/fast/stages.svg @@ -1,1063 +1,929 @@ + inkscape:version="1.0.2 (e86c870879, 2021-01-15)"> + + + + image/svg+xml + + + + + id="defs273" /> + inkscape:window-maximized="1" + inkscape:current-layer="svg269" /> + id="g1d4f14c2bd7_0_215.0"> + id="path2" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fast/stages/00-bootstrap/IAM.md b/fast/stages/0-bootstrap/IAM.md similarity index 100% rename from fast/stages/00-bootstrap/IAM.md rename to fast/stages/0-bootstrap/IAM.md diff --git a/fast/stages/00-bootstrap/README.md b/fast/stages/0-bootstrap/README.md similarity index 78% rename from fast/stages/00-bootstrap/README.md rename to fast/stages/0-bootstrap/README.md index cf4bfd51d..e1bb2948a 100644 --- a/fast/stages/00-bootstrap/README.md +++ b/fast/stages/0-bootstrap/README.md @@ -14,11 +14,33 @@ Use the following diagram as a simple high level reference for the following sec Organization-level diagram

+## Table of contents + +- [Design overview and choices](#design-overview-and-choices) + - [User groups](#user-groups) + - [Organization-level IAM](#organization-level-iam) + - [Automation project and resources](#automation-project-and-resources) + - [Billing account](#billing-account) + - [Organization-level logging](#organization-level-logging) + - [Naming](#naming) + - [Workload Identity Federation and CI/CD](#workload-identity-federation-and-cicd) +- [How to run this stage](#how-to-run-this-stage) + - [Prerequisites](#prerequisites) + - [Output files and cross-stage variables](#output-files-and-cross-stage-variables) + - [Running the stage](#running-the-stage) +- [Customizations](#customizations) + - [Group names](#group-names) + - [IAM](#iam) + - [Log sinks and log destinations](#log-sinks-and-log-destinations) + - [Names and naming convention](#names-and-naming-convention) + - [Workload Identity Federation](#workload-identity-federation) + - [CI/CD repositories](#cicd-repositories) + ## Design overview and choices As mentioned above, this stage only does the bare minimum required to bootstrap automation, and ensure that base audit and billing exports are in place from the start to provide some measure of accountability, even before the security configurations are applied in a later stage. -It also sets up organization-level IAM bindings so the Organization Administrator role is only used here, trading off some design freedom for ease of auditing and troubleshooting, and reducing the risk of costly security mistakes down the line. The only exception to this rule is for the [Resource Management stage](../01-resman) service account, described below. +It also sets up organization-level IAM bindings so the Organization Administrator role is only used here, trading off some design freedom for ease of auditing and troubleshooting, and reducing the risk of costly security mistakes down the line. The only exception to this rule is for the [Resource Management stage](../1-resman) service account, described below. ### User groups @@ -28,7 +50,7 @@ We have standardized the initial set of groups on those outlined in the [GCP Ent ### Organization-level IAM -The service account used in the [Resource Management stage](../01-resman) needs to be able to grant specific permissions at the organizational level, to enable specific functionality for subsequent stages that deal with network or security resources, or billing-related activities. +The service account used in the [Resource Management stage](../1-resman) needs to be able to grant specific permissions at the organizational level, to enable specific functionality for subsequent stages that deal with network or security resources, or billing-related activities. In order to be able to assign those roles without having the full authority of the Organization Admin role, this stage defines a custom role that only allows setting IAM policies on the organization, and grants it via a [delegated role grant](https://cloud.google.com/iam/docs/setting-limits-on-granting-roles) that only allows it to be used to grant a limited subset of roles. @@ -54,6 +76,8 @@ For same-organization billing, we configure a custom organization role that can For details on configuring the different billing account modes, refer to the [How to run this stage](#how-to-run-this-stage) section below. +Because of limitations of API availability, manual steps have to be followed to enable billing export within billing project to BigQuery dataset `billing_export` which will be created as part of the bootstrap stage. The process to share billing data [is outlined here](https://cloud.google.com/billing/docs/how-to/export-data-bigquery-setup#enable-bq-export). + ### Organization-level logging We create organization-level log sinks early in the bootstrap process to ensure a proper audit trail is in place from the very beginning. By default, we provide log filters to capture [Cloud Audit Logs](https://cloud.google.com/logging/docs/audit) and [VPC Service Controls violations](https://cloud.google.com/vpc-service-controls/docs/troubleshooting#vpc-sc-errors) into a Bigquery dataset in the top-level audit project. @@ -78,7 +102,7 @@ The convention is used in its full form only for specific resources with globall The [Customizations](#names-and-naming-convention) section on names below explains how to configure tokens, or implement a different naming convention. -## Workload Identity Federation and CI/CD +### Workload Identity Federation and CI/CD This stage also implements initial support for two interrelated features @@ -87,7 +111,7 @@ This stage also implements initial support for two interrelated features Workload Identity Federation support allows configuring external providers independently from CI/CD, and offers predefined attributes for a few well known ones (more can be easily added by editing the `identity-providers.tf` file). Once providers have been configured their names are passed to the following stages via interface outputs, and can be leveraged to set up access or impersonation in IAM bindings. -CI/CD support is fully implemented for GitHub, Gitlab, and Cloud Source Repositories / Cloud Build. For GitHub, we also offer a [separate supporting setup](../../extras/00-cicd-github/) to quickly create / configure repositories. +CI/CD support is fully implemented for GitHub, Gitlab, and Cloud Source Repositories / Cloud Build. For GitHub, we also offer a [separate supporting setup](../../extras/0-cicd-github/) to quickly create / configure repositories. @@ -122,7 +146,7 @@ To quickly self-grant the above roles, run the following code snippet as the ini export FAST_BU=$(gcloud config list --format 'value(core.account)') # find and set your org id -gcloud organizations list --filter display_name:$partofyourdomain +gcloud organizations list export FAST_ORG_ID=123456 # set needed roles @@ -137,25 +161,6 @@ done Then make sure the same user is also part of the `gcp-organization-admins` group so that impersonating the automation service account later on will be possible. -#### Billing account in a different organization - -If you are using a billing account belonging to a different organization (e.g. in multiple organization setups), some initial configurations are needed to ensure the identities running this stage can assign billing-related roles. - -If the billing organization is managed by another version of this stage, we leverage the `organizationIamAdmin` role created there, to allow restricted granting of billing roles at the organization level. - -If that's not the case, an equivalent role needs to exist, or the predefined `resourcemanager.organizationAdmin` role can be used if not managed authoritatively. The role name then needs to be manually changed in the `billing.tf` file, in the `google_organization_iam_binding` resource. - -The identity applying this stage for the first time also needs two roles in billing organization, they can be removed after the first `apply` completes successfully: - -```bash -export FAST_BILLING_ORG_ID=789012 -export FAST_ROLES=(roles/billing.admin roles/resourcemanager.organizationAdmin) -for role in $FAST_ROLES; do - gcloud organizations add-iam-policy-binding $FAST_BILLING_ORG_ID \ - --member user:$FAST_BU --role $role -done -``` - #### Standalone billing account If you are using a standalone billing account, the identity applying this stage for the first time needs to be a billing account administrator: @@ -185,7 +190,7 @@ Please note that FAST also supports an additional group for users with permissio Then make sure you have configured the correct values for the following variables by providing a `terraform.tfvars` file: - `billing_account` - an object containing `id` as the id of your billing account, derived from the Cloud Console UI or by running `gcloud beta billing accounts list`, and `organization_id` as the id of the organization owning it, or `null` to use the billing account in isolation + an object containing `id` as the id of your billing account, derived from the Cloud Console UI or by running `gcloud beta billing accounts list`, and the `is_org_level` flag that controls whether organization or account-level bindings are used, and a billing export project and dataset are created - `groups` the name mappings for your groups, if you're following the default convention you can leave this to the provided default - `organization.id`, `organization.domain`, `organization.customer_id` @@ -200,7 +205,6 @@ You can also adapt the example that follows to your needs: # if you have too many accounts, check the Cloud Console :) billing_account = { id = "012345-67890A-BCDEF0" - organization_id = 1234567890 } # use `gcloud organizations list` @@ -224,7 +228,7 @@ Alongisde the GCS stored files, you can also configure a second copy to be saves This second set of files is disabled by default, you can enable it by setting the `outputs_location` variable to a valid path on a local filesystem, e.g. -```hcl +```tfvars outputs_location = "~/fast-config" ``` @@ -235,18 +239,18 @@ Below is the outline of the output files generated by all stages, which is ident ```bash [path specified in outputs_location] ├── providers -│   ├── 00-bootstrap-providers.tf -│   ├── 01-resman-providers.tf -│   ├── 02-networking-providers.tf -│   ├── 02-security-providers.tf -│   ├── 03-project-factory-dev-providers.tf -│   ├── 03-project-factory-prod-providers.tf -│   └── 99-sandbox-providers.tf +│   ├── 0-bootstrap-providers.tf +│   ├── 1-resman-providers.tf +│   ├── 2-networking-providers.tf +│   ├── 2-security-providers.tf +│   ├── 3-project-factory-dev-providers.tf +│   ├── 3-project-factory-prod-providers.tf +│   └── 9-sandbox-providers.tf └── tfvars -│ ├── 00-bootstrap.auto.tfvars.json -│ ├── 01-resman.auto.tfvars.json -│ ├── 02-networking.auto.tfvars.json -│ └── 02-security.auto.tfvars.json +│ ├── 0-bootstrap.auto.tfvars.json +│ ├── 1-resman.auto.tfvars.json +│ ├── 2-networking.auto.tfvars.json +│ └── 2-security.auto.tfvars.json └── workflows └── [optional depending on the configured CI/CD repositories] ``` @@ -262,19 +266,37 @@ terraform init terraform apply \ -var bootstrap_user=$(gcloud config list --format 'value(core.account)') ``` + > If you see an error related to project name already exists, please make sure the project name is unique or the project was not deleted recently -Once the initial `apply` completes successfully, configure a remote backend using the new GCS bucket, and impersonation on the automation service account for this stage. To do this you can use the generated `providers.tf` file if you have configured output files as described above, or extract its contents from Terraform's output, then migrate state with `terraform init`: +Once the initial `apply` completes successfully, configure a remote backend using the new GCS bucket, and impersonation on the automation service account for this stage. To do this you can use the generated `providers.tf` file from either + +- the local filesystem if you have configured output files as described above +- the GCS bucket where output files are always stored +- Terraform outputs (not recommended as it's more complex) + +The following two snippets show how to leverage the `stage-links.sh` script in the root FAST folder to fetch the commands required for output files linking or copying, using either the local output folder configured via Terraform variables, or the GCS bucket which can be derived from the `automation` output. + +```bash +../../stage-links.sh ~/fast-config + +# copy and paste the following commands for '0-bootstrap' + +ln -s ~/fast-config/providers/0-bootstrap-providers.tf ./ +``` + +```bash +../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '0-bootstrap' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/0-bootstrap-providers.tf ./ +``` + +Copy/paste the command returned by the script to link or copy the provider file, then migrate state with `terraform init` and run `terraform apply`: ```bash -# if using output files via the outputs_location and set to `~/fast-config` -ln -s ~/fast-config/providers/00-bootstrap* ./ -# or from outputs if not using output files -terraform output -json providers | jq -r '.["00-bootstrap"]' \ - > providers.tf -# migrate state to GCS bucket configured in providers file terraform init -migrate-state -# run terraform apply to remove the bootstrap_user iam binding terraform apply ``` @@ -295,10 +317,11 @@ variable "groups" { description = "Group names to grant organization-level permissions." type = map(string) default = { - gcp-network-admins = "net-rockstars" + gcp-network-admins = "net-rockstars" # [...] } } +# tftest skip ``` If your groups layout differs substantially from the checklist, define all relevant groups in the `groups` variable, then rearrange IAM roles in the code to match your setup. @@ -330,7 +353,7 @@ You can customize organization-level logs through the `log_sinks` variable in tw - creating additional log sinks to capture more logs - changing the destination of captured logs -By default, all logs are exported to Bigquery, but FAST can create sinks to Cloud Logging Buckets, GCS, or PubSub. +By default, all logs are exported to a log bucket, but FAST can create sinks to BigQuery, GCS, or PubSub. If you need to capture additional logs, please refer to GCP's documentation on [scenarios for exporting logging data](https://cloud.google.com/architecture/exporting-stackdriver-logging-for-security-and-access-analytics), where you can find ready-made filter expressions for different use cases. @@ -357,7 +380,7 @@ Provider key names are used by the `cicd_repositories` variable to configure aut This is a sample configuration of a GitHub and a Gitlab provider, `attribute_condition` attribute can use any of the mapped attribute for the provider (refer to the `identity-providers.tf` file for the full list) or set to `null` if needed: -```hcl +```tfvars federated_identity_providers = { github-sample = { attribute_condition = "attribute.repository_owner==\"my-github-org\"" @@ -372,9 +395,9 @@ federated_identity_providers = { gitlab-ce-sample = { attribute_condition = "attribute.namespace_path==\"my-gitlab-org\"" issuer = "gitlab" - custom_settings = { - issuer_uri = "https://gitlab.fast.example.com" - allowed_audiences = ["https://gitlab.fast.example.com"] + custom_settings = { + issuer_uri = "https://gitlab.fast.example.com" + allowed_audiences = ["https://gitlab.fast.example.com"] } } } @@ -388,7 +411,7 @@ The repository design we support is fairly simple, with a repository for modules This is an example of configuring the bootstrap and resource management repositories in this stage. CI/CD configuration is optional, so the entire variable or any of its attributes can be set to null if not needed. -```hcl +```tfvars cicd_repositories = { bootstrap = { branch = null @@ -396,12 +419,6 @@ cicd_repositories = { name = "my-gh-org/fast-bootstrap" type = "github" } - cicd = { - branch = null - identity_provider = "github-sample" - name = "my-gh-org/fast-cicd" - type = "github" - } resman = { branch = "main" identity_provider = "github-sample" @@ -415,7 +432,7 @@ The `type` attribute can be set to one of the supported repository types: `githu Once the stage is applied the generated output files will contain pre-configured workflow files for each repository, that will use Workload Identity Federation via a dedicated service account for each repository to impersonate the automation service account for the stage. -You can use Terraform to automate creation of the repositories using the extra stage defined in [fast/extras/00-cicd-github](../../extras/00-cicd-github/) (only for Github for now). +You can use Terraform to automate creation of the repositories using the extra stage defined in [fast/extras/0-cicd-github](../../extras/0-cicd-github/) (only for Github for now). The remaining configuration is manual, as it regards the repositories themselves: @@ -449,7 +466,7 @@ The remaining configuration is manual, as it regards the repositories themselves | name | description | modules | resources | |---|---|---|---| | [automation.tf](./automation.tf) | Automation project and resources. | gcs · iam-service-account · project | | -| [billing.tf](./billing.tf) | Billing export project and dataset. | bigquery-dataset · organization · project | google_billing_account_iam_member · google_organization_iam_binding | +| [billing.tf](./billing.tf) | Billing export project and dataset. | bigquery-dataset · project | google_billing_account_iam_member | | [cicd.tf](./cicd.tf) | Workload Identity Federation configurations for CI/CD. | iam-service-account · source-repository | | | [identity-providers.tf](./identity-providers.tf) | Workload Identity Federation provider definitions. | | google_iam_workload_identity_pool · google_iam_workload_identity_pool_provider | | [log-export.tf](./log-export.tf) | Audit log project and sink. | bigquery-dataset · gcs · logging-bucket · project · pubsub | | @@ -464,35 +481,35 @@ The remaining configuration is manual, as it regards the repositories themselves | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [billing_account](variables.tf#L17) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | | -| [organization](variables.tf#L202) | Organization details. | object({…}) | ✓ | | | -| [prefix](variables.tf#L217) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | | -| [bootstrap_user](variables.tf#L25) | Email of the nominal user running this stage for the first time. | string | | null | | -| [cicd_repositories](variables.tf#L31) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | -| [custom_role_names](variables.tf#L83) | Names of custom roles defined at the org level. | object({…}) | | {…} | | -| [fast_features](variables.tf#L95) | Selective control for top-level FAST features. | object({…}) | | {…} | | -| [federated_identity_providers](variables.tf#L114) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…})) | | {} | | -| [groups](variables.tf#L128) | Group names to grant organization-level permissions. | map(string) | | {…} | | -| [iam](variables.tf#L146) | Organization-level custom IAM settings in role => [principal] format. | map(list(string)) | | {} | | -| [iam_additive](variables.tf#L152) | Organization-level custom IAM settings in role => [principal] format for non-authoritative bindings. | map(list(string)) | | {} | | -| [locations](variables.tf#L158) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | | -| [log_sinks](variables.tf#L177) | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | | -| [outputs_location](variables.tf#L211) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | -| [project_parent_ids](variables.tf#L227) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | object({…}) | | {…} | | +| [billing_account](variables.tf#L17) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | | +| [organization](variables.tf#L196) | Organization details. | object({…}) | ✓ | | | +| [prefix](variables.tf#L211) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | | +| [bootstrap_user](variables.tf#L29) | Email of the nominal user running this stage for the first time. | string | | null | | +| [cicd_repositories](variables.tf#L35) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | +| [custom_role_names](variables.tf#L81) | Names of custom roles defined at the org level. | object({…}) | | {…} | | +| [fast_features](variables.tf#L95) | Selective control for top-level FAST features. | object({…}) | | {} | | +| [federated_identity_providers](variables.tf#L108) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…})) | | {} | | +| [groups](variables.tf#L122) | Group names to grant organization-level permissions. | map(string) | | {…} | | +| [iam](variables.tf#L140) | Organization-level custom IAM settings in role => [principal] format. | map(list(string)) | | {} | | +| [iam_additive](variables.tf#L146) | Organization-level custom IAM settings in role => [principal] format for non-authoritative bindings. | map(list(string)) | | {} | | +| [locations](variables.tf#L152) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | | +| [log_sinks](variables.tf#L171) | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | | +| [outputs_location](variables.tf#L205) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | +| [project_parent_ids](variables.tf#L221) | Optional parents for projects created here in folders/nnnnnnn format. Null values will use the organization as parent. | object({…}) | | {…} | | ## Outputs | name | description | sensitive | consumers | |---|---|:---:|---| -| [automation](outputs.tf#L89) | Automation resources. | | | -| [billing_dataset](outputs.tf#L94) | BigQuery dataset prepared for billing export. | | | -| [cicd_repositories](outputs.tf#L99) | CI/CD repository configurations. | | | -| [custom_roles](outputs.tf#L111) | Organization-level custom roles. | | | -| [federated_identity](outputs.tf#L116) | Workload Identity Federation pool and providers. | | | -| [outputs_bucket](outputs.tf#L126) | GCS bucket where generated output files are stored. | | | -| [project_ids](outputs.tf#L131) | Projects created by this stage. | | | -| [providers](outputs.tf#L141) | Terraform provider files for this stage and dependent stages. | ✓ | stage-01 | -| [service_accounts](outputs.tf#L148) | Automation service accounts created by this stage. | | | -| [tfvars](outputs.tf#L158) | Terraform variable files for the following stages. | ✓ | | +| [automation](outputs.tf#L86) | Automation resources. | | | +| [billing_dataset](outputs.tf#L91) | BigQuery dataset prepared for billing export. | | | +| [cicd_repositories](outputs.tf#L96) | CI/CD repository configurations. | | | +| [custom_roles](outputs.tf#L108) | Organization-level custom roles. | | | +| [federated_identity](outputs.tf#L113) | Workload Identity Federation pool and providers. | | | +| [outputs_bucket](outputs.tf#L123) | GCS bucket where generated output files are stored. | | | +| [project_ids](outputs.tf#L128) | Projects created by this stage. | | | +| [providers](outputs.tf#L138) | Terraform provider files for this stage and dependent stages. | ✓ | stage-01 | +| [service_accounts](outputs.tf#L145) | Automation service accounts created by this stage. | | | +| [tfvars](outputs.tf#L154) | Terraform variable files for the following stages. | ✓ | | diff --git a/fast/stages/00-bootstrap/automation.tf b/fast/stages/0-bootstrap/automation.tf similarity index 82% rename from fast/stages/00-bootstrap/automation.tf rename to fast/stages/0-bootstrap/automation.tf index 1475c811c..90c14a81d 100644 --- a/fast/stages/00-bootstrap/automation.tf +++ b/fast/stages/0-bootstrap/automation.tf @@ -127,39 +127,6 @@ module "automation-tf-bootstrap-sa" { } } -# cicd stage's bucket and service account - -module "automation-tf-cicd-gcs" { - source = "../../../modules/gcs" - project_id = module.automation-project.project_id - name = "iac-core-cicd-0" - prefix = local.prefix - location = var.locations.gcs - storage_class = local.gcs_storage_class - versioning = true - iam = { - "roles/storage.objectAdmin" = [module.automation-tf-cicd-provisioning-sa.iam_email] - } - depends_on = [module.organization] -} - -module "automation-tf-cicd-provisioning-sa" { - source = "../../../modules/iam-service-account" - project_id = module.automation-project.project_id - name = "cicd-0" - display_name = "Terraform stage 1 CICD service account." - prefix = local.prefix - # allow SA used by CI/CD workflow to impersonate this SA - iam = { - "roles/iam.serviceAccountTokenCreator" = compact([ - try(module.automation-tf-cicd-sa["cicd"].iam_email, null) - ]) - } - iam_storage_roles = { - (module.automation-tf-output-gcs.name) = ["roles/storage.admin"] - } -} - # resource hierarchy stage's bucket and service account module "automation-tf-resman-gcs" { @@ -183,7 +150,8 @@ module "automation-tf-resman-sa" { display_name = "Terraform stage 1 resman service account." prefix = local.prefix # allow SA used by CI/CD workflow to impersonate this SA - iam = { + # we use additive IAM to allow tenant CI/CD SAs to impersonate it + iam_additive = { "roles/iam.serviceAccountTokenCreator" = compact([ try(module.automation-tf-cicd-sa["resman"].iam_email, null) ]) diff --git a/fast/stages/00-bootstrap/billing.tf b/fast/stages/0-bootstrap/billing.tf similarity index 60% rename from fast/stages/00-bootstrap/billing.tf rename to fast/stages/0-bootstrap/billing.tf index df10e8f08..aee033bd8 100644 --- a/fast/stages/00-bootstrap/billing.tf +++ b/fast/stages/0-bootstrap/billing.tf @@ -30,7 +30,7 @@ locals { module "billing-export-project" { source = "../../../modules/project" - count = local.billing_org ? 1 : 0 + count = var.billing_account.is_org_level ? 1 : 0 billing_account = var.billing_account.id name = "billing-exp-0" parent = coalesce( @@ -52,56 +52,18 @@ module "billing-export-project" { module "billing-export-dataset" { source = "../../../modules/bigquery-dataset" - count = local.billing_org ? 1 : 0 + count = var.billing_account.is_org_level ? 1 : 0 project_id = module.billing-export-project.0.project_id id = "billing_export" friendly_name = "Billing export." location = var.locations.bq } -# billing account in a different org - -module "billing-organization-ext" { - source = "../../../modules/organization" - count = local.billing_org_ext ? 1 : 0 - organization_id = "organizations/${var.billing_account.organization_id}" - iam_additive = { - "roles/billing.admin" = local.billing_ext_admins - } -} - - -resource "google_organization_iam_binding" "billing_org_ext_admin_delegated" { - # refer to organization.tf for the explanation of how this binding works - count = local.billing_org_ext ? 1 : 0 - org_id = var.billing_account.organization_id - # if the billing org does not have our custom role, user the predefined one - # role = "roles/resourcemanager.organizationAdmin" - role = join("", [ - "organizations/${var.billing_account.organization_id}/", - "roles/${var.custom_role_names.organization_iam_admin}" - ]) - members = [module.automation-tf-resman-sa.iam_email] - condition { - title = "automation_sa_delegated_grants" - description = "Automation service account delegated grants." - expression = format( - "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])", - join(",", formatlist("'%s'", [ - "roles/billing.costsManager", - "roles/billing.user", - ] - )) - ) - } - depends_on = [module.billing-organization-ext] -} - # standalone billing account resource "google_billing_account_iam_member" "billing_ext_admin" { for_each = toset( - local.billing_ext ? local.billing_ext_admins : [] + !var.billing_account.is_org_level ? local.billing_ext_admins : [] ) billing_account_id = var.billing_account.id role = "roles/billing.admin" @@ -110,7 +72,7 @@ resource "google_billing_account_iam_member" "billing_ext_admin" { resource "google_billing_account_iam_member" "billing_ext_cost_manager" { for_each = toset( - local.billing_ext ? local.billing_ext_admins : [] + !var.billing_account.is_org_level ? local.billing_ext_admins : [] ) billing_account_id = var.billing_account.id role = "roles/billing.costsManager" diff --git a/fast/stages/00-bootstrap/cicd.tf b/fast/stages/0-bootstrap/cicd.tf similarity index 86% rename from fast/stages/00-bootstrap/cicd.tf rename to fast/stages/0-bootstrap/cicd.tf index 7cdae41c9..2b2a3df95 100644 --- a/fast/stages/00-bootstrap/cicd.tf +++ b/fast/stages/0-bootstrap/cicd.tf @@ -17,6 +17,16 @@ # tfdoc:file:description Workload Identity Federation configurations for CI/CD. locals { + cicd_providers = { + for k, v in google_iam_workload_identity_pool_provider.default : + k => { + issuer = local.identity_providers[k].issuer + issuer_uri = local.identity_providers[k].issuer_uri + name = v.name + principal_tpl = local.identity_providers[k].principal_tpl + principalset_tpl = local.identity_providers[k].principalset_tpl + } + } cicd_repositories = { for k, v in coalesce(var.cicd_repositories, {}) : k => v if( @@ -32,18 +42,13 @@ locals { ) } cicd_workflow_providers = { - bootstrap = "00-bootstrap-providers.tf" - cicd = "00-cicd-providers.tf" - resman = "01-resman-providers.tf" + bootstrap = "0-bootstrap-providers.tf" + resman = "1-resman-providers.tf" } cicd_workflow_var_files = { bootstrap = [] - cicd = [ - "00-bootstrap.auto.tfvars.json", - "globals.auto.tfvars.json" - ] resman = [ - "00-bootstrap.auto.tfvars.json", + "0-bootstrap.auto.tfvars.json", "globals.auto.tfvars.json" ] } @@ -69,7 +74,7 @@ module "automation-tf-cicd-repo" { ] } triggers = { - "fast-00-${each.key}" = { + "fast-0-${each.key}" = { filename = ".cloudbuild/workflow.yaml" included_files = ["**/*tf", ".cloudbuild/workflow.yaml"] service_account = module.automation-tf-cicd-sa[each.key].id diff --git a/fast/stages/00-bootstrap/diagram.png b/fast/stages/0-bootstrap/diagram.png similarity index 100% rename from fast/stages/00-bootstrap/diagram.png rename to fast/stages/0-bootstrap/diagram.png diff --git a/fast/stages/00-bootstrap/diagram.svg b/fast/stages/0-bootstrap/diagram.svg similarity index 100% rename from fast/stages/00-bootstrap/diagram.svg rename to fast/stages/0-bootstrap/diagram.svg diff --git a/fast/stages/00-bootstrap/groups.gif b/fast/stages/0-bootstrap/groups.gif similarity index 100% rename from fast/stages/00-bootstrap/groups.gif rename to fast/stages/0-bootstrap/groups.gif diff --git a/fast/stages/00-bootstrap/identity-providers.tf b/fast/stages/0-bootstrap/identity-providers.tf similarity index 100% rename from fast/stages/00-bootstrap/identity-providers.tf rename to fast/stages/0-bootstrap/identity-providers.tf diff --git a/fast/stages/00-bootstrap/log-export.tf b/fast/stages/0-bootstrap/log-export.tf similarity index 82% rename from fast/stages/00-bootstrap/log-export.tf rename to fast/stages/0-bootstrap/log-export.tf index 1c9f5a87a..b9b5da42f 100644 --- a/fast/stages/00-bootstrap/log-export.tf +++ b/fast/stages/0-bootstrap/log-export.tf @@ -17,6 +17,16 @@ # tfdoc:file:description Audit log project and sink. locals { + log_sink_destinations = merge( + # use the same dataset for all sinks with `bigquery` as destination + { for k, v in var.log_sinks : k => module.log-export-dataset.0 if v.type == "bigquery" }, + # use the same gcs bucket for all sinks with `storage` as destination + { for k, v in var.log_sinks : k => module.log-export-gcs.0 if v.type == "storage" }, + # use separate pubsub topics and logging buckets for sinks with + # destination `pubsub` and `logging` + module.log-export-pubsub, + module.log-export-logbucket + ) log_types = toset([for k, v in var.log_sinks : v.type]) } diff --git a/fast/stages/00-bootstrap/main.tf b/fast/stages/0-bootstrap/main.tf similarity index 78% rename from fast/stages/00-bootstrap/main.tf rename to fast/stages/0-bootstrap/main.tf index 839019f3c..dba2ed089 100644 --- a/fast/stages/00-bootstrap/main.tf +++ b/fast/stages/0-bootstrap/main.tf @@ -28,10 +28,6 @@ locals { for k, v in local.groups : k => "group:${v}" } - # convenience flags that express where billing account resides - billing_ext = var.billing_account.organization_id == null - billing_org = var.billing_account.organization_id == var.organization.id - billing_org_ext = !local.billing_ext && !local.billing_org # naming: environment used in most resource names prefix = join("-", compact([var.prefix, "prod"])) } diff --git a/fast/stages/00-bootstrap/organization.tf b/fast/stages/0-bootstrap/organization.tf similarity index 89% rename from fast/stages/00-bootstrap/organization.tf rename to fast/stages/0-bootstrap/organization.tf index 0700d564e..154b37fe5 100644 --- a/fast/stages/00-bootstrap/organization.tf +++ b/fast/stages/0-bootstrap/organization.tf @@ -23,9 +23,13 @@ locals { "roles/browser" = [ "domain:${var.organization.domain}" ] - "roles/logging.admin" = [ - module.automation-tf-bootstrap-sa.iam_email - ] + "roles/logging.admin" = concat( + [ + module.automation-tf-bootstrap-sa.iam_email, + module.automation-tf-resman-sa.iam_email + ], + local._iam_bootstrap_user + ) "roles/owner" = local._iam_bootstrap_user "roles/resourcemanager.folderAdmin" = [ module.automation-tf-resman-sa.iam_email @@ -34,12 +38,11 @@ locals { [module.automation-tf-bootstrap-sa.iam_email], local._iam_bootstrap_user ) - # the following is useful if roles/browser is not desirable - # "roles/resourcemanager.organizationViewer" = [ - # "domain:${var.organization.domain}" - # ] "roles/resourcemanager.projectCreator" = concat( - [module.automation-tf-bootstrap-sa.iam_email], + [ + module.automation-tf-bootstrap-sa.iam_email, + module.automation-tf-resman-sa.iam_email + ], local._iam_bootstrap_user ) "roles/resourcemanager.projectMover" = [ @@ -77,8 +80,12 @@ locals { local.groups_iam.gcp-security-admins, module.automation-tf-resman-sa.iam_email ] + # the following is useful if roles/browser is not desirable + # "roles/resourcemanager.organizationViewer" = [ + # "domain:${var.organization.domain}" + # ] }, - local.billing_org ? { + var.billing_account.is_org_level ? { "roles/billing.admin" = [ local.groups_iam.gcp-billing-admins, local.groups_iam.gcp-organization-admins, @@ -114,16 +121,6 @@ locals { iam_roles_additive = distinct(concat( keys(local._iam_additive), keys(var.iam_additive) )) - log_sink_destinations = merge( - # use the same dataset for all sinks with `bigquery` as destination - { for k, v in var.log_sinks : k => module.log-export-dataset.0 if v.type == "bigquery" }, - # use the same gcs bucket for all sinks with `storage` as destination - { for k, v in var.log_sinks : k => module.log-export-gcs.0 if v.type == "storage" }, - # use separate pubsub topics and logging buckets for sinks with - # destination `pubsub` and `logging` - module.log-export-pubsub, - module.log-export-logbucket - ) } module "organization" { @@ -189,6 +186,9 @@ module "organization" { "dns.networks.bindPrivateDNSZone", "resourcemanager.projects.get", ] + (var.custom_role_names.tenant_network_admin) = [ + "compute.globalOperations.get", + ] } logging_sinks = { for name, attrs in var.log_sinks : name => { @@ -219,8 +219,10 @@ resource "google_organization_iam_binding" "org_admin_delegated" { "roles/compute.orgFirewallPolicyAdmin", "roles/compute.xpnAdmin", "roles/orgpolicy.policyAdmin", + "roles/resourcemanager.organizationViewer", + module.organization.custom_role_id[var.custom_role_names.tenant_network_admin] ], - local.billing_org ? [ + var.billing_account.is_org_level ? [ "roles/billing.admin", "roles/billing.costsManager", "roles/billing.user", diff --git a/fast/stages/00-bootstrap/outputs-files.tf b/fast/stages/0-bootstrap/outputs-files.tf similarity index 97% rename from fast/stages/00-bootstrap/outputs-files.tf rename to fast/stages/0-bootstrap/outputs-files.tf index ded88cd56..90e195b53 100644 --- a/fast/stages/00-bootstrap/outputs-files.tf +++ b/fast/stages/0-bootstrap/outputs-files.tf @@ -26,7 +26,7 @@ resource "local_file" "providers" { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/00-bootstrap.auto.tfvars.json" + filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/0-bootstrap.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/00-bootstrap/outputs-gcs.tf b/fast/stages/0-bootstrap/outputs-gcs.tf similarity index 96% rename from fast/stages/00-bootstrap/outputs-gcs.tf rename to fast/stages/0-bootstrap/outputs-gcs.tf index 2c281d4cc..0aded986a 100644 --- a/fast/stages/00-bootstrap/outputs-gcs.tf +++ b/fast/stages/0-bootstrap/outputs-gcs.tf @@ -26,7 +26,7 @@ resource "google_storage_bucket_object" "providers" { resource "google_storage_bucket_object" "tfvars" { bucket = module.automation-tf-output-gcs.name - name = "tfvars/00-bootstrap.auto.tfvars.json" + name = "tfvars/0-bootstrap.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/00-bootstrap/outputs.tf b/fast/stages/0-bootstrap/outputs.tf similarity index 77% rename from fast/stages/00-bootstrap/outputs.tf rename to fast/stages/0-bootstrap/outputs.tf index 73dd64f4e..364abd684 100644 --- a/fast/stages/00-bootstrap/outputs.tf +++ b/fast/stages/0-bootstrap/outputs.tf @@ -21,7 +21,7 @@ locals { for k, v in local.cicd_repositories : k => templatefile( "${path.module}/templates/workflow-${v.type}.yaml", { identity_provider = try( - local.wif_providers[v["identity_provider"]].name, "" + local.cicd_providers[v["identity_provider"]].name, "" ) outputs_bucket = module.automation-tf-output-gcs.name service_account = try( @@ -38,19 +38,26 @@ locals { k => try(module.organization.custom_role_id[v], null) } providers = { - "00-bootstrap" = templatefile(local._tpl_providers, { - bucket = module.automation-tf-bootstrap-gcs.name - name = "bootstrap" - sa = module.automation-tf-bootstrap-sa.email + "0-bootstrap" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.automation-tf-bootstrap-gcs.name + name = "bootstrap" + sa = module.automation-tf-bootstrap-sa.email }) - "00-cicd" = templatefile(local._tpl_providers, { - bucket = module.automation-tf-cicd-gcs.name - name = "cicd" - sa = module.automation-tf-cicd-provisioning-sa.email + "1-resman" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.automation-tf-resman-gcs.name + name = "resman" + sa = module.automation-tf-resman-sa.email }) - "01-resman" = templatefile(local._tpl_providers, { + "0-bootstrap-tenant" = templatefile(local._tpl_providers, { + backend_extra = join("\n", [ + "# remove the newline between quotes and set the tenant name as prefix", + "prefix = \"", + "\"" + ]) bucket = module.automation-tf-resman-gcs.name - name = "resman" + name = "bootstrap-tenant" sa = module.automation-tf-resman-sa.email }) } @@ -59,7 +66,7 @@ locals { federated_identity_pool = try( google_iam_workload_identity_pool.default.0.name, null ) - federated_identity_providers = local.wif_providers + federated_identity_providers = local.cicd_providers outputs_bucket = module.automation-tf-output-gcs.name project_id = module.automation-project.project_id project_number = module.automation-project.number @@ -74,16 +81,6 @@ locals { organization = var.organization prefix = var.prefix } - wif_providers = { - for k, v in google_iam_workload_identity_pool_provider.default : - k => { - issuer = local.identity_providers[k].issuer - issuer_uri = local.identity_providers[k].issuer_uri - name = v.name - principal_tpl = local.identity_providers[k].principal_tpl - principalset_tpl = local.identity_providers[k].principalset_tpl - } - } } output "automation" { @@ -102,7 +99,7 @@ output "cicd_repositories" { for k, v in local.cicd_repositories : k => { branch = v.branch name = v.name - provider = try(local.wif_providers[v.identity_provider].name, null) + provider = try(local.cicd_providers[v.identity_provider].name, null) service_account = try(module.automation-tf-cicd-sa[k].email, null) } } @@ -119,7 +116,7 @@ output "federated_identity" { pool = try( google_iam_workload_identity_pool.default.0.name, null ) - providers = local.wif_providers + providers = local.cicd_providers } } @@ -149,7 +146,6 @@ output "service_accounts" { description = "Automation service accounts created by this stage." value = { bootstrap = module.automation-tf-bootstrap-sa.email - cicd = module.automation-tf-cicd-provisioning-sa.email resman = module.automation-tf-resman-sa.email } } diff --git a/tests/modules/gke_cluster/fixture/variables.tf b/fast/stages/0-bootstrap/templates/providers.tf.tpl similarity index 62% rename from tests/modules/gke_cluster/fixture/variables.tf rename to fast/stages/0-bootstrap/templates/providers.tf.tpl index 97fc6a635..d1c224c5c 100644 --- a/tests/modules/gke_cluster/fixture/variables.tf +++ b/fast/stages/0-bootstrap/templates/providers.tf.tpl @@ -14,24 +14,20 @@ * limitations under the License. */ -variable "enable_addons" { - type = any - default = { - horizontal_pod_autoscaling = true - http_load_balancing = true +terraform { + backend "gcs" { + bucket = "${bucket}" + impersonate_service_account = "${sa}" + %{~ if backend_extra != null ~} + ${indent(4, backend_extra)} + %{~ endif ~} } } +provider "google" { + impersonate_service_account = "${sa}" +} +provider "google-beta" { + impersonate_service_account = "${sa}" +} -variable "enable_features" { - type = any - default = { - workload_identity = true - } -} - -variable "monitoring_config" { - type = any - default = { - managed_prometheus = true - } -} +# end provider.tf for ${name} diff --git a/fast/stages/0-bootstrap/templates/workflow-github.yaml b/fast/stages/0-bootstrap/templates/workflow-github.yaml new file mode 100644 index 000000000..87b5ae1a0 --- /dev/null +++ b/fast/stages/0-bootstrap/templates/workflow-github.yaml @@ -0,0 +1,202 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "FAST ${stage_name} stage" + +on: + pull_request: + branches: + - main + types: + - closed + - opened + - synchronize + +env: + FAST_OUTPUTS_BUCKET: ${outputs_bucket} + FAST_SERVICE_ACCOUNT: ${service_account} + FAST_WIF_PROVIDER: ${identity_provider} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + TF_PROVIDERS_FILE: ${tf_providers_file} + %{~ if tf_var_files != [] ~} + TF_VAR_FILES: ${join("\n ", tf_var_files)} + %{~ endif ~} + TF_VERSION: 1.3.2 + +jobs: + fast-pr: + permissions: + contents: read + id-token: write + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - id: checkout + name: Checkout repository + uses: actions/checkout@v3 + + # set up SSH key authentication to the modules repository + - id: ssh-config + name: Configure SSH authentication + run: | + ssh-agent -a "$SSH_AUTH_SOCK" > /dev/null + ssh-add - <<< "$${{ secrets.CICD_MODULES_KEY }}" + + # set up authentication via Workload identity Federation + - id: gcp-auth + name: Authenticate to Google Cloud + uses: google-github-actions/auth@v0 + with: + workload_identity_provider: $${{ env.FAST_WIF_PROVIDER }} + service_account: $${{ env.FAST_SERVICE_ACCOUNT }} + access_token_lifetime: 3600s + + - id: gcp-sdk + name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v0 + with: + install_components: alpha + + # copy provider and tfvars files + - id: tf-config + name: Copy Terraform output files + run: | + gcloud alpha storage cp -r \ + "gs://$${{env.FAST_OUTPUTS_BUCKET}}/providers/$${{env.TF_PROVIDERS_FILE}}" ./ + %{~ if tf_var_files != [] ~} + gcloud alpha storage cp -r \ + "gs://$${{env.FAST_OUTPUTS_BUCKET}}/tfvars" ./ + for f in $${{env.TF_VAR_FILES}}; do + ln -s "tfvars/$f" ./ + done + %{~ endif ~} + + - id: tf-setup + name: Set up Terraform + uses: hashicorp/setup-terraform@v2.0.3 + with: + terraform_version: $${{ env.TF_VERSION }} + + # run Terraform init/validate/plan + - id: tf-init + name: Terraform init + continue-on-error: true + run: | + terraform init -no-color + + - id: tf-validate + continue-on-error: true + name: Terraform validate + run: terraform validate -no-color + + - id: tf-plan + name: Terraform plan + continue-on-error: true + run: | + terraform plan -input=false -out ../plan.out -no-color + + - id: tf-apply + if: github.event.pull_request.merged == true && success() + name: Terraform apply + continue-on-error: true + run: | + terraform apply -input=false -auto-approve -no-color ../plan.out + + - id: pr-comment + name: Post comment to Pull Request + continue-on-error: true + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' + env: + PLAN: $${{ steps.tf-plan.outputs.stdout }}\n$${{ steps.tf-plan.outputs.stderr }} + with: + script: | + const output = `### Terraform Initialization \`$${{ steps.tf-init.outcome }}\` + + ### Terraform Validation \`$${{ steps.tf-validate.outcome }}\` + +
Validation Output + + \`\`\`\n + $${{ steps.tf-validate.outputs.stdout }} + \`\`\` + +
+ + ### Terraform Plan \`$${{ steps.tf-plan.outcome }}\` + +
Show Plan + + \`\`\`\n + $${process.env.PLAN.split('\n').filter(l => l.match(/^([A-Z\s].*|)$$/)).join('\n')} + \`\`\` + +
+ + ### Terraform Apply \`$${{ steps.tf-apply.outcome }}\` + + *Pusher: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`, Working Directory: \`$${{ env.tf_actions_working_dir }}\`, Workflow: \`$${{ github.workflow }}\`*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - id: pr-short-comment + name: Post comment to Pull Request + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' && steps.pr-comment.outcome != 'success' + with: + script: | + const output = `### Terraform Initialization \`$${{ steps.tf-init.outcome }}\` + + ### Terraform Validation \`$${{ steps.tf-validate.outcome }}\` + + ### Terraform Plan \`$${{ steps.tf-plan.outcome }}\` + + Plan output is in the action log. + + ### Terraform Apply \`$${{ steps.tf-apply.outcome }}\` + + *Pusher: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`, Working Directory: \`$${{ env.tf_actions_working_dir }}\`, Workflow: \`$${{ github.workflow }}\`*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - id: check-init + name: Check init failure + if: steps.tf-init.outcome != 'success' + run: exit 1 + + - id: check-validate + name: Check validate failure + if: steps.tf-validate.outcome != 'success' + run: exit 1 + + - id: check-plan + name: Check plan failure + if: steps.tf-plan.outcome != 'success' + run: exit 1 + + - id: check-apply + name: Check apply failure + if: github.event.pull_request.merged == true && steps.tf-apply.outcome != 'success' + run: exit 1 diff --git a/fast/stages/0-bootstrap/templates/workflow-gitlab.yaml b/fast/stages/0-bootstrap/templates/workflow-gitlab.yaml new file mode 100644 index 000000000..8981e70b3 --- /dev/null +++ b/fast/stages/0-bootstrap/templates/workflow-gitlab.yaml @@ -0,0 +1,120 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +default: + before_script: + - echo "$${CI_JOB_JWT_V2}" > token.txt + image: + name: hashicorp/terraform + entrypoint: + - "/usr/bin/env" + - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +variables: + GOOGLE_CREDENTIALS: cicd-sa-credentials.json + FAST_OUTPUTS_BUCKET: ${outputs_bucket} + FAST_SERVICE_ACCOUNT: ${service_account} + FAST_WIF_PROVIDER: ${identity_provider} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + TF_PROVIDERS_FILE: ${tf_providers_file} + TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + +stages: + - gcp-auth + - tf-files + - tf-plan + - tf-apply + +cache: + key: gcp-auth + paths: + - cicd-sa-credentials.json + - .tf-setup + +gcp-auth: + image: + name: google/cloud-sdk:slim + stage: gcp-auth + script: + - | + gcloud iam workload-identity-pools create-cred-config \ + $${FAST_WIF_PROVIDER} \ + --service-account=$${FAST_SERVICE_ACCOUNT} \ + --service-account-token-lifetime-seconds=3600 \ + --output-file=$${GOOGLE_CREDENTIALS} \ + --credential-source-file=token.txt +tf-files: + dependencies: + - gcp-auth + image: + name: google/cloud-sdk:slim + stage: tf-files + script: + # - gcloud components install -q alpha + - gcloud config set auth/credential_file_override $${GOOGLE_CREDENTIALS} + - mkdir -p .tf-setup + - | + gcloud alpha storage cp -r \ + "gs://$${FAST_OUTPUTS_BUCKET}/providers/$${TF_PROVIDERS_FILE}" .tf-setup/ + - | + gcloud alpha storage cp -r \ + "gs://$${FAST_OUTPUTS_BUCKET}/tfvars" .tf-setup/ + +tf-plan: + # uncomment the following lines and set the SSH key secret for private modules repo + # before_script: + # - | + # ssh-agent -a $SSH_AUTH_SOCK > /dev/null + # echo "$CICD_MODULES_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null + # mkdir -p ~/.ssh + # ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts + # ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts + stage: tf-plan + script: + - cp .tf-setup/$${TF_PROVIDERS_FILE} ./ + - | + for f in $${TF_VAR_FILES}; do + ln -s ".tf-setup/tfvars/$f" ./ + done + - terraform init + - terraform validate + - terraform plan + dependencies: + - tf-files + +tf-apply: + # uncomment the following lines and set the SSH key secret for private modules repo + # before_script: + # - | + # ssh-agent -a $SSH_AUTH_SOCK > /dev/null + # echo "$CICD_MODULES_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null + # mkdir -p ~/.ssh + # ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts + # ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts + stage: tf-apply + script: + - cp .tf-setup/$${TF_PROVIDERS_FILE} ./ + - | + for f in $${TF_VAR_FILES}; do + ln -s ".tf-setup/tfvars/$f" ./ + done + - terraform init + - terraform validate + - terraform apply -input=false -auto-approve + dependencies: + - tf-files + when: manual + only: + variables: + - $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH diff --git a/fast/stages/0-bootstrap/templates/workflow-sourcerepo.yaml b/fast/stages/0-bootstrap/templates/workflow-sourcerepo.yaml new file mode 100644 index 000000000..446c9c960 --- /dev/null +++ b/fast/stages/0-bootstrap/templates/workflow-sourcerepo.yaml @@ -0,0 +1,98 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +steps: + - name: alpine:3 + id: tf-download + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + mkdir -p /builder/home/.local/bin + wget https://releases.hashicorp.com/terraform/$${_TF_VERSION}/terraform_$${_TF_VERSION}_linux_amd64.zip + unzip terraform_$${_TF_VERSION}_linux_amd64.zip -d /builder/home/.local/bin + rm terraform_$${_TF_VERSION}_linux_amd64.zip + chmod 755 /builder/home/.local/bin/terraform + - name: alpine:3 + id: tf-check-format + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform fmt -recursive -check /workspace/ + - name: gcr.io/google.com/cloudsdktool/cloud-sdk:alpine + id: tf-files + entrypoint: bash + args: + - -eEuo + - pipefail + - -c + - |- + /google-cloud-sdk/bin/gsutil cp \ + gs://$${_FAST_OUTPUTS_BUCKET}/providers/$${_TF_PROVIDERS_FILE} ./ + /google-cloud-sdk/bin/gsutil cp -r \ + gs://$${_FAST_OUTPUTS_BUCKET}/tfvars ./ + for f in $${_TF_VAR_FILES}; do + ln -s tfvars/$f ./ + done + - name: alpine:3 + id: tf-init + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform init -no-color + - name: alpine:3 + id: tf-check-validate + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform validate -no-color + - name: alpine:3 + id: tf-plan + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform plan -no-color -input=false -out plan.out + # store artifact and ask for approval here if needed + - name: alpine:3 + id: tf-apply + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform apply -no-color -input=false -auto-approve plan.out +options: + env: + - PATH=/usr/local/bin:/usr/bin:/bin:/builder/home/.local/bin + logging: CLOUD_LOGGING_ONLY +substitutions: + _FAST_OUTPUTS_BUCKET: ${outputs_bucket} + _TF_PROVIDERS_FILE: ${tf_providers_file} + _TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + _TF_VERSION: 1.3.2 diff --git a/fast/stages/00-bootstrap/terraform.tfvars.sample b/fast/stages/0-bootstrap/terraform.tfvars.sample similarity index 65% rename from fast/stages/00-bootstrap/terraform.tfvars.sample rename to fast/stages/0-bootstrap/terraform.tfvars.sample index 66710ba4a..a134f8dc5 100644 --- a/fast/stages/00-bootstrap/terraform.tfvars.sample +++ b/fast/stages/0-bootstrap/terraform.tfvars.sample @@ -1,15 +1,14 @@ # use `gcloud beta billing accounts list` # if you have too many accounts, check the Cloud Console :) billing_account = { - id = "012345-67890A-BCDEF0" - organization_id = 1234567890 + id = "012345-67890A-BCDEF0" } # use `gcloud organizations list` organization = { - domain = "example.org" - id = 1234567890 - customer_id = "C000001" + domain = "example.org" + id = 1234567890 + customer_id = "C000001" } outputs_location = "~/fast-config" diff --git a/fast/stages/00-bootstrap/variables.tf b/fast/stages/0-bootstrap/variables.tf similarity index 88% rename from fast/stages/00-bootstrap/variables.tf rename to fast/stages/0-bootstrap/variables.tf index 0b9f37c20..4ab90deba 100644 --- a/fast/stages/00-bootstrap/variables.tf +++ b/fast/stages/0-bootstrap/variables.tf @@ -15,11 +15,15 @@ */ variable "billing_account" { - description = "Billing account id and organization id ('nnnnnnnn' or null)." + description = "Billing account id. If billing account is not part of the same org set `is_org_level` to false." type = object({ - id = string - organization_id = number + id = string + is_org_level = optional(bool, true) }) + validation { + condition = var.billing_account.is_org_level != null + error_message = "Invalid `null` value for `billing_account.is_org_level`." + } } variable "bootstrap_user" { @@ -31,24 +35,18 @@ variable "bootstrap_user" { variable "cicd_repositories" { description = "CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed." type = object({ - bootstrap = object({ + bootstrap = optional(object({ branch = string identity_provider = string name = string type = string - }) - cicd = object({ + })) + resman = optional(object({ branch = string identity_provider = string name = string type = string - }) - resman = object({ - branch = string - identity_provider = string - name = string - type = string - }) + })) }) default = null validation { @@ -85,29 +83,25 @@ variable "custom_role_names" { type = object({ organization_iam_admin = string service_project_network_admin = string + tenant_network_admin = string }) default = { organization_iam_admin = "organizationIamAdmin" service_project_network_admin = "serviceProjectNetworkAdmin" + tenant_network_admin = "tenantNetworkAdmin" } } variable "fast_features" { description = "Selective control for top-level FAST features." type = object({ - data_platform = bool - gke = bool - project_factory = bool - sandbox = bool - teams = bool + data_platform = optional(bool, false) + gke = optional(bool, false) + project_factory = optional(bool, false) + sandbox = optional(bool, false) + teams = optional(bool, false) }) - default = { - data_platform = true - gke = true - project_factory = true - sandbox = true - teams = true - } + default = {} nullable = false } @@ -183,11 +177,11 @@ variable "log_sinks" { default = { audit-logs = { filter = "logName:\"/logs/cloudaudit.googleapis.com%2Factivity\" OR logName:\"/logs/cloudaudit.googleapis.com%2Fsystem_event\"" - type = "bigquery" + type = "logging" } vpc-sc = { filter = "protoPayload.metadata.@type=\"type.googleapis.com/google.cloud.audit.VpcServiceControlAuditMetadata\"" - type = "bigquery" + type = "logging" } } validation { diff --git a/fast/stages/00-bootstrap/templates b/fast/stages/00-bootstrap/templates deleted file mode 120000 index bcb6967be..000000000 --- a/fast/stages/00-bootstrap/templates +++ /dev/null @@ -1 +0,0 @@ -../../assets/templates \ No newline at end of file diff --git a/fast/stages/01-resman/templates b/fast/stages/01-resman/templates deleted file mode 120000 index bcb6967be..000000000 --- a/fast/stages/01-resman/templates +++ /dev/null @@ -1 +0,0 @@ -../../assets/templates \ No newline at end of file diff --git a/fast/stages/03-data-platform/dev/README.md b/fast/stages/03-data-platform/dev/README.md deleted file mode 100644 index 12db8d292..000000000 --- a/fast/stages/03-data-platform/dev/README.md +++ /dev/null @@ -1,200 +0,0 @@ -# Data Platform - -The Data Platform builds on top of your foundations to create and set up projects (and related resources) to be used for your data platform. - -

- Data Platform diagram -

- -## Design overview and choices - -> A more comprehensive description of the Data Platform architecture and approach can be found in the [Data Platform module README](../../../../blueprints/data-solutions/data-platform-foundations/). The module is wrapped and configured here to leverage the FAST flow. - -The Data Platform creates projects in a well-defined context, usually an ad-hoc folder managed by the resource management setup. Resources are organized by environment within this folder. - -Across different data layers environment-specific projects are created to separate resources and IAM roles. - -The Data Platform manages: - -- project creation -- API/Services enablement -- service accounts creation -- IAM role assignment for groups and service accounts -- KMS keys roles assignment -- Shared VPC attachment and subnet IAM binding -- project-level organization policy definitions -- billing setup (billing account attachment and budget configuration) -- data-related resources in the managed projects - -### User groups - -As per our GCP best practices the Data Platform relies on user groups to assign roles to human identities. These are the specific groups used by the Data Platform and their access patterns, from the [module documentation](../../../../blueprints/data-solutions/data-platform-foundations/#groups): - -- *Data Engineers* They handle and run the Data Hub, with read access to all resources in order to troubleshoot possible issues with pipelines. This team can also impersonate any service account. -- *Data Analysts*. They perform analysis on datasets, with read access to the data warehouse Curated or Confidential projects depending on their privileges, and BigQuery READ/WRITE access to the playground project. -- *Data Security*:. They handle security configurations related to the Data Hub. This team has admin access to the common project to configure Cloud DLP templates or Data Catalog policy tags. - -|Group|Landing|Load|Transformation|Data Warehouse Landing|Data Warehouse Curated|Data Warehouse Confidential|Data Warehouse Playground|Orchestration|Common| -|-|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:| -|Data Engineers|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`| -|Data Analysts|-|-|-|-|-|`READ`|`READ`/`WRITE`|-|-| -|Data Security|-|-|-|-|-|-|-|-|`ADMIN`| - -### Network - -A Shared VPC is used here, either from one of the FAST networking stages (e.g. [hub and spoke via VPN](../../02-networking-vpn)) or from an external source. - -### Encryption - -Cloud KMS crypto keys can be configured wither from the [FAST security stage](../../02-security) or from an external source. This step is optional and depends on customer policies and security best practices. - -To configure the use of Cloud KMS on resources, you have to specify the key id on the `service_encryption_keys` variable. Key locations should match resource locations. - -## Data Catalog - -[Data Catalog](https://cloud.google.com/data-catalog) helps you to document your data entry at scale. Data Catalog relies on [tags](https://cloud.google.com/data-catalog/docs/tags-and-tag-templates#tags) and [tag template](https://cloud.google.com/data-catalog/docs/tags-and-tag-templates#tag-templates) to manage metadata for all data entries in a unified and centralized service. To implement [column-level security](https://cloud.google.com/bigquery/docs/column-level-security-intro) on BigQuery, we suggest to use `Tags` and `Tag templates`. - -The default configuration will implement 3 tags: - - `3_Confidential`: policy tag for columns that include very sensitive information, such as credit card numbers. - - `2_Private`: policy tag for columns that include sensitive personal identifiable information (PII) information, such as a person's first name. - - `1_Sensitive`: policy tag for columns that include data that cannot be made public, such as the credit limit. - -Anything that is not tagged is available to all users who have access to the data warehouse. - -You can configure your tags and roles associated by configuring the `data_catalog_tags` variable. We suggest useing the "[Best practices for using policy tags in BigQuery](https://cloud.google.com/bigquery/docs/best-practices-policy-tags)" article as a guide to designing your tags structure and access pattern. By default, no groups has access to tagged data. - -### VPC-SC - -As is often the case in real-world configurations, [VPC-SC](https://cloud.google.com/vpc-service-controls) is needed to mitigate data exfiltration. VPC-SC can be configured from the [FAST security stage](../../02-security). This step is optional, but highly recomended, and depends on customer policies and security best practices. - -To configure the use of VPC-SC on the data platform, you have to specify the data platform project numbers on the `vpc_sc_perimeter_projects.dev` variable on [FAST security stage](../../02-security#perimeter-resources). - -In the case your Data Warehouse need to handle confidential data and you have the requirement to separate them deeply from other data and IAM is not enough, the suggested configuration is to keep the confidential project in a separate VPC-SC perimeter with the adequate ingress/egress rules needed for the load and tranformation service account. Below you can find an high level diagram describing the configuration. - -

- Data Platform VPC-SC diagram -

- -## How to run this stage - -This stage can be run in isolation by prviding the necessary variables, but it's really meant to be used as part of the FAST flow after the "foundational stages" ([`00-bootstrap`](../../00-bootstrap), [`01-resman`](../../01-resman), [`02-networking`](../../02-networking-vpn) and [`02-security`](../../02-security)). - -When running in isolation, the following roles are needed on the principal used to apply Terraform: - -- on the organization or network folder level - - `roles/xpnAdmin` or a custom role which includes the following permissions - - `"compute.organizations.enableXpnResource"`, - - `"compute.organizations.disableXpnResource"`, - - `"compute.subnetworks.setIamPolicy"`, -- on each folder where projects are created - - `"roles/logging.admin"` - - `"roles/owner"` - - `"roles/resourcemanager.folderAdmin"` - - `"roles/resourcemanager.projectCreator"` -- on the host project for the Shared VPC - - `"roles/browser"` - - `"roles/compute.viewer"` -- on the organization or billing account - - `roles/billing.admin` - -The VPC host project, VPC and subnets should already exist. - -### Providers configuration - -If you're running this on top of Fast, you should run the following commands to create the providers file, and populate the required variables from the previous stage. - -```bash -# Variable `outputs_location` is set to `~/fast-config` in stage 01-resman -ln -s ~/fast-config/providers/03-data-platform-dev-providers.tf . -``` - -If you have not configured `outputs_location` in bootstrap, you can derive the providers file from that stage's outputs: - -```bash -cd ../../01-resman -terraform output -json providers | jq -r '.["03-data-platform-dev"]' \ - > ../03-data-platform/dev/providers.tf -``` - -### Variable configuration - -There are two broad sets of variables that can be configured: - -- variables shared by other stages (organization id, billing account id, etc.) or derived from a resource managed by a different stage (folder id, automation project id, etc.) -- variables specific to resources managed by this stage - -To avoid the tedious job of filling in the first group of variables with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. - -If you configured a valid path for `outputs_location` in the bootstrap security and networking stages, simply link the relevant `terraform-*.auto.tfvars.json` files from this stage's outputs folder under the path you specified. This will also link the providers configuration file: - -```bash -# Variable `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . -ln -s ~/fast-config/tfvars/02-networking.auto.tfvars.json . -# also copy the tfvars file used for the bootstrap stage -cp ../../00-bootstrap/terraform.tfvars . -``` - -If you're not using FAST or its output files, 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. - -Once the configuration is complete you can apply this stage: - -```bash -terraform init -terraform apply -``` - -## Demo pipeline - -The application layer is out of scope of this script. As a demo purpuse only, several Cloud Composer DAGs are provided. Demos will import data from the `landing` area to the `DataWarehouse Confidential` dataset suing different features. - -You can find examples in the `[demo](../../../../blueprints/data-solutions/data-platform-foundations/demo)` folder. - - - - -## Files - -| name | description | modules | resources | -|---|---|---|---| -| [main.tf](./main.tf) | Data Platform. | data-platform-foundations | | -| [outputs.tf](./outputs.tf) | Output variables. | | google_storage_bucket_object · local_file | -| [variables.tf](./variables.tf) | Terraform Variables. | | | - -## Variables - -| name | description | type | required | default | producer | -|---|---|:---:|:---:|:---:|:---:| -| [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | -| [billing_account](variables.tf#L25) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-globals | -| [folder_ids](variables.tf#L65) | Folder to be used for the networking resources in folders/nnnn format. | object({…}) | ✓ | | 01-resman | -| [host_project_ids](variables.tf#L83) | Shared VPC project ids. | object({…}) | ✓ | | 02-networking | -| [organization](variables.tf#L115) | Organization details. | object({…}) | ✓ | | 00-globals | -| [prefix](variables.tf#L131) | Unique prefix used for resource names. Not used for projects if 'project_create' is null. | string | ✓ | | 00-globals | -| [composer_config](variables.tf#L34) | Cloud Composer configuration options. | object({…}) | | {…} | | -| [data_catalog_tags](variables.tf#L48) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(map(list(string))) | | {…} | | -| [data_force_destroy](variables.tf#L59) | Flag to set 'force_destroy' on data services like BigQery or Cloud Storage. | bool | | false | | -| [groups](variables.tf#L73) | Groups. | map(string) | | {…} | | -| [location](variables.tf#L91) | Location used for multi-regional resources. | string | | "eu" | | -| [network_config_composer](variables.tf#L97) | Network configurations to use for Composer. | object({…}) | | {…} | | -| [outputs_location](variables.tf#L125) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | -| [project_services](variables.tf#L137) | List of core services enabled on all projects. | list(string) | | […] | | -| [region](variables.tf#L148) | Region used for regional resources. | string | | "europe-west1" | | -| [service_encryption_keys](variables.tf#L154) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | null | | -| [subnet_self_links](variables.tf#L166) | Shared VPC subnet self links. | object({…}) | | null | 02-networking | -| [vpc_self_links](variables.tf#L175) | Shared VPC self links. | object({…}) | | null | 02-networking | - -## Outputs - -| name | description | sensitive | consumers | -|---|---|:---:|---| -| [bigquery_datasets](outputs.tf#L42) | BigQuery datasets. | | | -| [demo_commands](outputs.tf#L47) | Demo commands. | | | -| [gcs_buckets](outputs.tf#L52) | GCS buckets. | | | -| [kms_keys](outputs.tf#L57) | Cloud MKS keys. | | | -| [projects](outputs.tf#L62) | GCP Projects informations. | | | -| [vpc_network](outputs.tf#L67) | VPC network. | | | -| [vpc_subnet](outputs.tf#L72) | VPC subnetworks. | | | - - diff --git a/fast/stages/01-resman/IAM.md b/fast/stages/1-resman/IAM.md similarity index 100% rename from fast/stages/01-resman/IAM.md rename to fast/stages/1-resman/IAM.md diff --git a/fast/stages/01-resman/README.md b/fast/stages/1-resman/README.md similarity index 53% rename from fast/stages/01-resman/README.md rename to fast/stages/1-resman/README.md index 56772816a..971c69633 100644 --- a/fast/stages/01-resman/README.md +++ b/fast/stages/1-resman/README.md @@ -13,6 +13,22 @@ The following diagram is a high level reference of the resources created and man Resource-management diagram

+## Table of contents + +- [Design overview and choices](#design-overview-and-choices) + - [Multitenancy](#multitenancy) + - [Workload Identity Federation and CI/CD](#workload-identity-federation-and-cicd) +- [How to run this stage](#how-to-run-this-stage) + - [Provider and Terraform variables](#provider-and-terraform-variables) + - [Impersonating the automation service account](#impersonating-the-automation-service-account) + - [Variable configuration](#variable-configuration) + - [Running the stage](#running-the-stage) +- [Customizations](#customizations) + - [Team folders](#team-folders) + - [Organization Policies](#organization-policies) + - [IAM](#iam) + - [Additional folders](#additional-folders) + ## Design overview and choices Despite its simplicity, this stage implements the basics of a design that we've seen working well for a variety of customers, where the hierarchy is laid out following two conceptually different approaches: @@ -34,67 +50,69 @@ Additionally, a few critical benefits are directly provided by this design: - grouping application resources and services using teams or business logic is a flexible approach, which maps well to typical operational or budget requirements - automation stages (e.g. Networking) can be segregated in a simple and effective way, by creating the required service accounts and buckets for each stage here, and applying a handful of IAM roles to the relevant folder -For a discussion on naming, please refer to the [Bootstrap stage documentation](../00-bootstrap/README.md#naming), as the same approach is shared by all stages. +For a discussion on naming, please refer to the [Bootstrap stage documentation](../0-bootstrap/README.md#naming), as the same approach is shared by all stages. + +### Multitenancy + +Fully multitenant hierarchies inside the same organization are implemented via [separate additional stages](../../stages-multitenant/) that need to be run once for each tenant, and require this stage as a prerequisite. ### Workload Identity Federation and CI/CD This stage also implements optional support for CI/CD, much in the same way as the bootstrap stage. The only difference is on Workload Identity Federation, which is only configured in bootstrap and made available here via stage interface variables (the automatically generated `.tfvars` files). -For details on how to configure CI/CD please refer to the [relevant section in the bootstrap stage documentation](../00-bootstrap/README.md#cicd-repositories). +For details on how to configure CI/CD please refer to the [relevant section in the bootstrap stage documentation](../0-bootstrap/README.md#cicd-repositories). ## How to run this stage -This stage is meant to be executed after the [bootstrap](../00-bootstrap) stage has run, as it leverages the automation service account and bucket created there. The relevant user groups must also exist, but that's one of the requirements for the previous stage too, so if you ran that successfully, you're good to go. +This stage is meant to be executed after the [bootstrap](../0-bootstrap) stage has run, as it leverages the automation service account and bucket created there. The relevant user groups must also exist, but that's one of the requirements for the previous stage too, so if you ran that successfully, you're good to go. It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the bootstrap stage for the actual roles needed. Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. -### Providers configuration +### Provider and Terraform variables -The default way of making sure you have the right permissions, is to use the identity of the service account pre-created for this stage during bootstrap, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). +As all other FAST stages, the [mechanism used to pass variable values and pre-built provider files from one stage to the next](../0-bootstrap/README.md#output-files-and-cross-stage-variables) is also leveraged here. -To simplify setup, the previous stage pre-configures a valid providers file in its output, and optionally writes it to a local file if the `outputs_location` variable is set to a valid path. - -If you have set a valid value for `outputs_location` in the bootstrap stage (see the [bootstrap stage README](../00-bootstrap/#output-files-and-cross-stage-variables) for more details), simply link the relevant `providers.tf` file from this stage's folder in the path you specified: +The commands to link or copy the provider and terraform variable files can be easily derived from the `stage-links.sh` script in the FAST root folder, passing it a single argument with the local output files folder (if configured) or the GCS output bucket in the automation project (derived from stage 0 outputs). The following examples demonstrate both cases, and the resulting commands that then need to be copy/pasted and run. ```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/providers/01-resman-providers.tf . -``` +../../stage-links.sh ~/fast-config -If you have not configured `outputs_location` in bootstrap, you can derive the providers file from that stage's outputs: +# copy and paste the following commands for '1-resman' + +ln -s ~/fast-config/providers/1-resman-providers.tf ./ +ln -s ~/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +``` ```bash -cd ../00-bootstrap -terraform output -json providers | jq -r '.["01-resman"]' \ - > ../01-resman/providers.tf +../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '1-resman' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/1-resman-providers.tf ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/globals.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./ ``` -If you want to continue to rely on `outputs_location` logic, create a `terraform.tfvars` file and configure it as described [here](../00-bootstrap/#output-files-and-cross-stage-variables). +### Impersonating the automation service account + +The preconfigured provider file uses impersonation to run with this stage's automation service account's credentials. The `gcp-devops` and `organization-admins` groups have the necessary IAM bindings in place to do that, so make sure the current user is a member of one of those groups. ### Variable configuration -There are two broad sets of variables you will need to fill in: +Variables in this stage -- like most other FAST stages -- are broadly divided into three separate sets: -- variables shared by other stages (org id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) -- variables specific to resources managed by this stage +- variables which refer to global values for the whole organization (org id, billing account id, prefix, etc.), which are pre-populated via the `globals.auto.tfvars.json` file linked or copied above +- variables which refer to resources managed by previous stage, which are prepopulated here via the `0-bootstrap.auto.tfvars.json` file linked or copied above +- and finally variables that optionally control this stage's behaviour and customizations, and can to be set in a custom `terraform.tfvars` file -To avoid the tedious job of filling in the first group of variable with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. +The latter set is explained in the [Customization](#customizations) sections below, and the full list can be found in the [Variables](#variables) table at the bottom of this document. -If you configured a valid path for `outputs_location` in the bootstrap stage, simply link the relevant `*.auto.tfvars.json` files from the outputs folder. For this stage, you need the `globals.auto.tfvars.json` file containing global values compiled manually for the bootstrap stage, and `00-bootstrap.auto.tfvars.json` containing values derived from resources managed by the bootstrap stage: +### Running the stage -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/tfvars/globals.auto.tfvars.json . -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -``` - -A second set of variables is specific to this stage, they are all optional so if you need to customize them, create an extra `terraform.tfvars` file. - -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. The sections below also describe some of the possible customizations. For billing configurations, refer to the [Bootstrap documentation on billing](../00-bootstrap/README.md#billing-account) as the `billing_account` variable is identical across all stages. - -Once done, you can run this stage: +Once provider and variable values are in place and the correct user is configured, the stage can be run: ```bash terraform init @@ -109,7 +127,7 @@ This stage provides a single built-in customization that offers a minimal (but u Consider the following example in a `tfvars` file: -```hcl +```tfvars team_folders = { team-a = { descriptive_name = "Team A" @@ -135,13 +153,13 @@ This allows to centralize the minimum set of resources to delegate control of ea ### Organization policies -Organization policies are laid out in an explicit manner in the `organization.tf` file, so it's fairly easy to add or remove specific policies. +Organization policies leverage -- with one exception -- the built-in factory implemented in the organization module, and configured via the yaml files in the `data` folder. To edit organization policies, check and edit the files there. -For policies where additional data is needed, a root-level `organization_policy_configs` variable allows passing in specific data. Its built-in use to add additional organizations to the [Domain Restricted Sharing](https://cloud.google.com/resource-manager/docs/organization-policy/restricting-domains) policy, can be taken as an example on how to leverage it for additional customizations. +The one exception is [Domain Restricted Sharing](https://cloud.google.com/resource-manager/docs/organization-policy/restricting-domains), which is made dynamic and implemented in code so as to auto-add the current organization's customer id. The `organization_policy_configs` variable allow to easily add ids from third party organizations if needed. ### IAM -IAM roles can be easily edited in the relevant `branch-xxx.tf` file, following the best practice outlined in the [bootstrap stage](../00-bootstrap#customizations) documentation of separating user-level and service-account level IAM policies in modules' `iam_groups`, `iam`, and `iam_additive` variables. +IAM roles can be easily edited in the relevant `branch-xxx.tf` file, following the best practice outlined in the [bootstrap stage](../0-bootstrap#customizations) documentation of separating user-level and service-account level IAM policies in modules' `iam_groups`, `iam`, and `iam_additive` variables. A full reference of IAM roles managed by this stage [is available here](./IAM.md). @@ -156,11 +174,11 @@ Due to its simplicity, this stage lends itself easily to customizations: adding | name | description | modules | resources | |---|---|---|---| -| [billing.tf](./billing.tf) | Billing resources for external billing use cases. | organization | google_billing_account_iam_member | -| [branch-data-platform.tf](./branch-data-platform.tf) | Data Platform stages resources. | folder · gcs · iam-service-account | | +| [billing.tf](./billing.tf) | Billing resources for external billing use cases. | | google_billing_account_iam_member | +| [branch-data-platform.tf](./branch-data-platform.tf) | Data Platform stages resources. | folder · gcs · iam-service-account | google_organization_iam_member | | [branch-gke.tf](./branch-gke.tf) | GKE multitenant stage resources. | folder · gcs · iam-service-account | | | [branch-networking.tf](./branch-networking.tf) | Networking stage resources. | folder · gcs · iam-service-account | | -| [branch-project-factory.tf](./branch-project-factory.tf) | Project factory stage resources. | gcs · iam-service-account | | +| [branch-project-factory.tf](./branch-project-factory.tf) | Project factory stage resources. | gcs · iam-service-account | google_organization_iam_member | | [branch-sandbox.tf](./branch-sandbox.tf) | Sandbox stage resources. | folder · gcs · iam-service-account | | | [branch-security.tf](./branch-security.tf) | Security stage resources. | folder · gcs · iam-service-account | | | [branch-teams.tf](./branch-teams.tf) | Team stage resources. | folder · gcs · iam-service-account | | @@ -170,7 +188,7 @@ Due to its simplicity, this stage lends itself easily to customizations: adding | [cicd-project-factory.tf](./cicd-project-factory.tf) | CI/CD resources for the teams branch. | iam-service-account · source-repository | | | [cicd-security.tf](./cicd-security.tf) | CI/CD resources for the security branch. | iam-service-account · source-repository | | | [main.tf](./main.tf) | Module-level locals and resources. | | | -| [organization.tf](./organization.tf) | Organization policies. | organization | google_organization_iam_member | +| [organization.tf](./organization.tf) | Organization policies. | organization | | | [outputs-files.tf](./outputs-files.tf) | Output files persistence to local filesystem. | | local_file | | [outputs-gcs.tf](./outputs-gcs.tf) | Output files persistence to automation GCS bucket. | | google_storage_bucket_object | | [outputs.tf](./outputs.tf) | Module outputs. | | | @@ -180,34 +198,34 @@ Due to its simplicity, this stage lends itself easily to customizations: adding | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [automation](variables.tf#L20) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | -| [billing_account](variables.tf#L38) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [organization](variables.tf#L197) | Organization details. | object({…}) | ✓ | | 00-bootstrap | -| [prefix](variables.tf#L221) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | -| [cicd_repositories](variables.tf#L47) | CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | -| [custom_roles](variables.tf#L129) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 00-bootstrap | -| [data_dir](variables.tf#L138) | Relative path for the folder storing configuration data. | string | | "data" | | -| [fast_features](variables.tf#L144) | Selective control for top-level FAST features. | object({…}) | | {…} | 00-bootstrap | -| [groups](variables.tf#L164) | Group names to grant organization-level permissions. | map(string) | | {…} | 00-bootstrap | -| [locations](variables.tf#L179) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | 00-bootstrap | -| [organization_policy_configs](variables.tf#L207) | Organization policies customization. | object({…}) | | null | | -| [outputs_location](variables.tf#L215) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | -| [tag_names](variables.tf#L232) | Customized names for resource management tags. | object({…}) | | {…} | | -| [team_folders](variables.tf#L249) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | | +| [automation](variables.tf#L20) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | +| [billing_account](variables.tf#L38) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap | +| [organization](variables.tf#L193) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L217) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [cicd_repositories](variables.tf#L51) | CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | +| [custom_roles](variables.tf#L133) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | +| [data_dir](variables.tf#L142) | Relative path for the folder storing configuration data. | string | | "data" | | +| [fast_features](variables.tf#L148) | Selective control for top-level FAST features. | object({…}) | | {} | 0-0-bootstrap | +| [groups](variables.tf#L162) | Group names to grant organization-level permissions. | object({…}) | | {} | 0-bootstrap | +| [locations](variables.tf#L175) | Optional locations for GCS, BigQuery, and logging buckets created here. | object({…}) | | {…} | 0-bootstrap | +| [organization_policy_configs](variables.tf#L203) | Organization policies customization. | object({…}) | | null | | +| [outputs_location](variables.tf#L211) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable. | string | | null | | +| [tag_names](variables.tf#L228) | Customized names for resource management tags. | object({…}) | | {…} | | +| [team_folders](variables.tf#L247) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | | ## Outputs | name | description | sensitive | consumers | |---|---|:---:|---| -| [cicd_repositories](outputs.tf#L197) | WIF configuration for CI/CD repositories. | | | -| [dataplatform](outputs.tf#L211) | Data for the Data Platform stage. | | | -| [gke_multitenant](outputs.tf#L227) | Data for the GKE multitenant stage. | | 03-gke-multitenant | -| [networking](outputs.tf#L248) | Data for the networking stage. | | | -| [project_factories](outputs.tf#L257) | Data for the project factories stage. | | | -| [providers](outputs.tf#L272) | Terraform provider files for this stage and dependent stages. | ✓ | 02-networking · 02-security · 03-dataplatform · xx-sandbox · xx-teams | -| [sandbox](outputs.tf#L279) | Data for the sandbox stage. | | xx-sandbox | -| [security](outputs.tf#L293) | Data for the networking stage. | | 02-security | -| [teams](outputs.tf#L303) | Data for the teams stage. | | | -| [tfvars](outputs.tf#L315) | Terraform variable files for the following stages. | ✓ | | +| [cicd_repositories](outputs.tf#L210) | WIF configuration for CI/CD repositories. | | | +| [dataplatform](outputs.tf#L224) | Data for the Data Platform stage. | | | +| [gke_multitenant](outputs.tf#L240) | Data for the GKE multitenant stage. | | 03-gke-multitenant | +| [networking](outputs.tf#L261) | Data for the networking stage. | | | +| [project_factories](outputs.tf#L270) | Data for the project factories stage. | | | +| [providers](outputs.tf#L285) | Terraform provider files for this stage and dependent stages. | ✓ | 02-networking · 02-security · 03-dataplatform · xx-sandbox · xx-teams | +| [sandbox](outputs.tf#L292) | Data for the sandbox stage. | | xx-sandbox | +| [security](outputs.tf#L306) | Data for the networking stage. | | 02-security | +| [teams](outputs.tf#L316) | Data for the teams stage. | | | +| [tfvars](outputs.tf#L328) | Terraform variable files for the following stages. | ✓ | | diff --git a/fast/stages/01-resman/billing.tf b/fast/stages/1-resman/billing.tf similarity index 77% rename from fast/stages/01-resman/billing.tf rename to fast/stages/1-resman/billing.tf index fe497c7c3..ba20ab053 100644 --- a/fast/stages/01-resman/billing.tf +++ b/fast/stages/1-resman/billing.tf @@ -34,23 +34,11 @@ locals { # billing account in same org (resources is in the organization.tf file) -# billing account in a different org - -module "billing-organization-ext" { - source = "../../../modules/organization" - count = local.billing_org_ext ? 1 : 0 - organization_id = "organizations/${var.billing_account.organization_id}" - iam_additive = { - "roles/billing.user" = local.billing_ext_users - "roles/billing.costsManager" = local.billing_ext_users - } -} - # standalone billing account resource "google_billing_account_iam_member" "billing_ext_admin" { for_each = toset( - local.billing_ext ? local.billing_ext_users : [] + !var.billing_account.is_org_level ? local.billing_ext_users : [] ) billing_account_id = var.billing_account.id role = "roles/billing.user" @@ -59,7 +47,7 @@ resource "google_billing_account_iam_member" "billing_ext_admin" { resource "google_billing_account_iam_member" "billing_ext_costsmanager" { for_each = toset( - local.billing_ext ? local.billing_ext_users : [] + !var.billing_account.is_org_level ? local.billing_ext_users : [] ) billing_account_id = var.billing_account.id role = "roles/billing.costsManager" diff --git a/fast/stages/01-resman/branch-data-platform.tf b/fast/stages/1-resman/branch-data-platform.tf similarity index 86% rename from fast/stages/01-resman/branch-data-platform.tf rename to fast/stages/1-resman/branch-data-platform.tf index 66cc9fbb0..7a93e7a54 100644 --- a/fast/stages/01-resman/branch-data-platform.tf +++ b/fast/stages/1-resman/branch-data-platform.tf @@ -137,3 +137,22 @@ module "branch-dp-prod-gcs" { "roles/storage.objectAdmin" = [module.branch-dp-prod-sa.0.iam_email] } } + +resource "google_organization_iam_member" "org_policy_admin_dp" { + for_each = !var.fast_features.data_platform ? {} : { + data-dev = ["data", "development", module.branch-dp-dev-sa.0.iam_email] + data-prod = ["data", "production", module.branch-dp-prod-sa.0.iam_email] + } + org_id = var.organization.id + role = "roles/orgpolicy.policyAdmin" + member = each.value.2 + condition { + title = "org_policy_tag_dp_scoped" + description = "Org policy tag scoped grant for ${each.value.0}/${each.value.1}." + expression = <<-END + resource.matchTag('${var.organization.id}/${var.tag_names.context}', '${each.value.0}') + && + resource.matchTag('${var.organization.id}/${var.tag_names.environment}', '${each.value.1}') + END + } +} diff --git a/fast/stages/01-resman/branch-gke.tf b/fast/stages/1-resman/branch-gke.tf similarity index 94% rename from fast/stages/01-resman/branch-gke.tf rename to fast/stages/1-resman/branch-gke.tf index 84ca41ed5..76777d8fc 100644 --- a/fast/stages/01-resman/branch-gke.tf +++ b/fast/stages/1-resman/branch-gke.tf @@ -77,7 +77,11 @@ module "branch-gke-dev-sa" { prefix = var.prefix iam = { "roles/iam.serviceAccountTokenCreator" = concat( - ["group:${local.groups.gcp-devops}"], + ( + local.groups.gcp-devops == null + ? [] + : ["group:${local.groups.gcp-devops}"] + ), compact([ try(module.branch-gke-dev-sa-cicd.0.iam_email, null) ]) @@ -97,7 +101,11 @@ module "branch-gke-prod-sa" { prefix = var.prefix iam = { "roles/iam.serviceAccountTokenCreator" = concat( - ["group:${local.groups.gcp-devops}"], + ( + local.groups.gcp-devops == null + ? [] + : ["group:${local.groups.gcp-devops}"] + ), compact([ try(module.branch-gke-prod-sa-cicd.0.iam_email, null) ]) diff --git a/fast/stages/01-resman/branch-networking.tf b/fast/stages/1-resman/branch-networking.tf similarity index 98% rename from fast/stages/01-resman/branch-networking.tf rename to fast/stages/1-resman/branch-networking.tf index 530cf6b09..1fd7a6b3d 100644 --- a/fast/stages/01-resman/branch-networking.tf +++ b/fast/stages/1-resman/branch-networking.tf @@ -20,7 +20,7 @@ module "branch-network-folder" { source = "../../../modules/folder" parent = "organizations/${var.organization.id}" name = "Networking" - group_iam = { + group_iam = local.groups.gcp-network-admins == null ? {} : { (local.groups.gcp-network-admins) = [ # add any needed roles for resources/services not managed via Terraform, # or replace editor with ~viewer if no broad resource management needed diff --git a/fast/stages/01-resman/branch-project-factory.tf b/fast/stages/1-resman/branch-project-factory.tf similarity index 78% rename from fast/stages/01-resman/branch-project-factory.tf rename to fast/stages/1-resman/branch-project-factory.tf index 41651a28c..d74a8acb1 100644 --- a/fast/stages/01-resman/branch-project-factory.tf +++ b/fast/stages/1-resman/branch-project-factory.tf @@ -79,3 +79,22 @@ module "branch-pf-prod-gcs" { "roles/storage.objectAdmin" = [module.branch-pf-prod-sa.0.iam_email] } } + +resource "google_organization_iam_member" "org_policy_admin_pf" { + for_each = !var.fast_features.project_factory ? {} : { + pf-dev = ["teams", "development", module.branch-pf-dev-sa.0.iam_email] + pf-prod = ["teams", "production", module.branch-pf-prod-sa.0.iam_email] + } + org_id = var.organization.id + role = "roles/orgpolicy.policyAdmin" + member = each.value.2 + condition { + title = "org_policy_tag_pf_scoped" + description = "Org policy tag scoped grant for ${each.value.0}/${each.value.1}." + expression = <<-END + resource.matchTag('${var.organization.id}/${var.tag_names.context}', '${each.value.0}') + && + resource.matchTag('${var.organization.id}/${var.tag_names.environment}', '${each.value.1}') + END + } +} diff --git a/fast/stages/01-resman/branch-sandbox.tf b/fast/stages/1-resman/branch-sandbox.tf similarity index 100% rename from fast/stages/01-resman/branch-sandbox.tf rename to fast/stages/1-resman/branch-sandbox.tf diff --git a/fast/stages/01-resman/branch-security.tf b/fast/stages/1-resman/branch-security.tf similarity index 96% rename from fast/stages/01-resman/branch-security.tf rename to fast/stages/1-resman/branch-security.tf index c7b4fc970..4b0c0fb13 100644 --- a/fast/stages/01-resman/branch-security.tf +++ b/fast/stages/1-resman/branch-security.tf @@ -20,7 +20,7 @@ module "branch-security-folder" { source = "../../../modules/folder" parent = "organizations/${var.organization.id}" name = "Security" - group_iam = { + group_iam = local.groups.gcp-security-admins == null ? {} : { (local.groups.gcp-security-admins) = [ # add any needed roles for resources/services not managed via Terraform, # e.g. @@ -51,7 +51,7 @@ module "branch-security-folder" { module "branch-security-sa" { source = "../../../modules/iam-service-account" project_id = var.automation.project_id - name = "prod-resman-sec-0" + name = "security-0" display_name = "Terraform resman security service account." prefix = var.prefix iam = { diff --git a/fast/stages/01-resman/branch-teams.tf b/fast/stages/1-resman/branch-teams.tf similarity index 100% rename from fast/stages/01-resman/branch-teams.tf rename to fast/stages/1-resman/branch-teams.tf diff --git a/fast/stages/01-resman/cicd-data-platform.tf b/fast/stages/1-resman/cicd-data-platform.tf similarity index 99% rename from fast/stages/01-resman/cicd-data-platform.tf rename to fast/stages/1-resman/cicd-data-platform.tf index 5b07883c4..e69fd5bc1 100644 --- a/fast/stages/01-resman/cicd-data-platform.tf +++ b/fast/stages/1-resman/cicd-data-platform.tf @@ -103,7 +103,7 @@ module "branch-dp-dev-sa-cicd" { each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos ? { - "roles/iam.serviceAccountUser" = local.automation_resman_sa + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam } # impersonated via workload identity federation for external repos : { @@ -146,7 +146,7 @@ module "branch-dp-prod-sa-cicd" { each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos ? { - "roles/iam.serviceAccountUser" = local.automation_resman_sa + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam } # impersonated via workload identity federation for external repos : { diff --git a/fast/stages/01-resman/cicd-gke.tf b/fast/stages/1-resman/cicd-gke.tf similarity index 99% rename from fast/stages/01-resman/cicd-gke.tf rename to fast/stages/1-resman/cicd-gke.tf index fa4f8767c..4388a3ac5 100644 --- a/fast/stages/01-resman/cicd-gke.tf +++ b/fast/stages/1-resman/cicd-gke.tf @@ -103,7 +103,7 @@ module "branch-gke-dev-sa-cicd" { each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos ? { - "roles/iam.serviceAccountUser" = local.automation_resman_sa + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam } # impersonated via workload identity federation for external repos : { @@ -146,7 +146,7 @@ module "branch-gke-prod-sa-cicd" { each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos ? { - "roles/iam.serviceAccountUser" = local.automation_resman_sa + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam } # impersonated via workload identity federation for external repos : { diff --git a/fast/stages/01-resman/cicd-networking.tf b/fast/stages/1-resman/cicd-networking.tf similarity index 99% rename from fast/stages/01-resman/cicd-networking.tf rename to fast/stages/1-resman/cicd-networking.tf index 894348ff3..245d5ed02 100644 --- a/fast/stages/01-resman/cicd-networking.tf +++ b/fast/stages/1-resman/cicd-networking.tf @@ -65,7 +65,7 @@ module "branch-network-sa-cicd" { each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos ? { - "roles/iam.serviceAccountUser" = local.automation_resman_sa + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam } # impersonated via workload identity federation for external repos : { diff --git a/fast/stages/01-resman/cicd-project-factory.tf b/fast/stages/1-resman/cicd-project-factory.tf similarity index 99% rename from fast/stages/01-resman/cicd-project-factory.tf rename to fast/stages/1-resman/cicd-project-factory.tf index 8f357ce6c..1e2b45653 100644 --- a/fast/stages/01-resman/cicd-project-factory.tf +++ b/fast/stages/1-resman/cicd-project-factory.tf @@ -114,7 +114,7 @@ module "branch-pf-dev-sa-cicd" { each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos ? { - "roles/iam.serviceAccountUser" = local.automation_resman_sa + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam } # impersonated via workload identity federation for external repos : { @@ -162,7 +162,7 @@ module "branch-pf-prod-sa-cicd" { each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos ? { - "roles/iam.serviceAccountUser" = local.automation_resman_sa + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam } # impersonated via workload identity federation for external repos : { diff --git a/fast/stages/01-resman/cicd-security.tf b/fast/stages/1-resman/cicd-security.tf similarity index 99% rename from fast/stages/01-resman/cicd-security.tf rename to fast/stages/1-resman/cicd-security.tf index dd27a4733..c35bfbfbb 100644 --- a/fast/stages/01-resman/cicd-security.tf +++ b/fast/stages/1-resman/cicd-security.tf @@ -65,7 +65,7 @@ module "branch-security-sa-cicd" { each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos ? { - "roles/iam.serviceAccountUser" = local.automation_resman_sa + "roles/iam.serviceAccountUser" = local.automation_resman_sa_iam } # impersonated via workload identity federation for external repos : { diff --git a/fast/stages/1-resman/data/org-policies/compute.yaml b/fast/stages/1-resman/data/org-policies/compute.yaml new file mode 100644 index 000000000..0d27ac426 --- /dev/null +++ b/fast/stages/1-resman/data/org-policies/compute.yaml @@ -0,0 +1,73 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +compute.disableGuestAttributesAccess: + enforce: true + +compute.requireOsLogin: + enforce: true + +compute.restrictLoadBalancerCreationForTypes: + allow: + values: + - in:INTERNAL + +compute.skipDefaultNetworkCreation: + enforce: true + +compute.vmExternalIpAccess: + deny: + all: true + + +# compute.disableInternetNetworkEndpointGroup: +# enforce: true + +# compute.disableNestedVirtualization: +# enforce: true + +# compute.disableSerialPortAccess: +# enforce: true + +# compute.restrictCloudNATUsage: +# deny: +# all: true + +# compute.restrictDedicatedInterconnectUsage: +# deny: +# all: true + +# compute.restrictPartnerInterconnectUsage: +# deny: +# all: true + +# compute.restrictProtocolForwardingCreationForTypes: +# deny: +# all: true + +# compute.restrictSharedVpcHostProjects: +# deny: +# all: true + +# compute.restrictSharedVpcSubnetworks: +# deny: +# all: true + +# compute.restrictVpcPeering: +# deny: +# all: true + +# compute.restrictVpnPeerIPs: +# deny: +# all: true + +# compute.restrictXpnProjectLienRemoval: +# enforce: true + +# compute.setNewProjectDefaultToZonalDNSOnly: +# enforce: true + +# compute.vmCanIpForward: +# deny: +# all: true diff --git a/fast/stages/1-resman/data/org-policies/iam.yaml b/fast/stages/1-resman/data/org-policies/iam.yaml new file mode 100644 index 000000000..4d83f827f --- /dev/null +++ b/fast/stages/1-resman/data/org-policies/iam.yaml @@ -0,0 +1,12 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +iam.automaticIamGrantsForDefaultServiceAccounts: + enforce: true + +iam.disableServiceAccountKeyCreation: + enforce: true + +iam.disableServiceAccountKeyUpload: + enforce: true diff --git a/fast/stages/1-resman/data/org-policies/serverless.yaml b/fast/stages/1-resman/data/org-policies/serverless.yaml new file mode 100644 index 000000000..de62e6c70 --- /dev/null +++ b/fast/stages/1-resman/data/org-policies/serverless.yaml @@ -0,0 +1,26 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +run.allowedIngress: + allow: + values: + - is:internal + +# run.allowedVPCEgress: +# allow: +# values: +# - is:private-ranges-only + +# cloudfunctions.allowedIngressSettings: +# allow: +# values: +# - is:ALLOW_INTERNAL_ONLY + +# cloudfunctions.allowedVpcConnectorEgressSettings: +# allow: +# values: +# - is:PRIVATE_RANGES_ONLY + +# cloudfunctions.requireVPCConnector: +# enforce: true diff --git a/fast/stages/1-resman/data/org-policies/sql.yaml b/fast/stages/1-resman/data/org-policies/sql.yaml new file mode 100644 index 000000000..88b84d9d5 --- /dev/null +++ b/fast/stages/1-resman/data/org-policies/sql.yaml @@ -0,0 +1,9 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +sql.restrictAuthorizedNetworks: + enforce: true + +sql.restrictPublicIp: + enforce: true diff --git a/fast/stages/1-resman/data/org-policies/storage.yaml b/fast/stages/1-resman/data/org-policies/storage.yaml new file mode 100644 index 000000000..6c0a673f3 --- /dev/null +++ b/fast/stages/1-resman/data/org-policies/storage.yaml @@ -0,0 +1,6 @@ +# skip boilerplate check +# +# sample subset of useful organization policies, edit to suit requirements + +storage.uniformBucketLevelAccess: + enforce: true diff --git a/fast/stages/1-resman/diagram.png b/fast/stages/1-resman/diagram.png new file mode 100644 index 000000000..d1026318b Binary files /dev/null and b/fast/stages/1-resman/diagram.png differ diff --git a/fast/stages/1-resman/diagram.svg b/fast/stages/1-resman/diagram.svg new file mode 100644 index 000000000..541db3f4b --- /dev/null +++ b/fast/stages/1-resman/diagram.svg @@ -0,0 +1,1340 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fast/stages/01-resman/main.tf b/fast/stages/1-resman/main.tf similarity index 78% rename from fast/stages/01-resman/main.tf rename to fast/stages/1-resman/main.tf index 0651ee3fa..ff08b8c58 100644 --- a/fast/stages/01-resman/main.tf +++ b/fast/stages/1-resman/main.tf @@ -17,15 +17,13 @@ locals { # convenience flags that express where billing account resides automation_resman_sa = try( - [format( - "serviceAccount:%s", - data.google_client_openid_userinfo.provider_identity.0.email - )], - [] + data.google_client_openid_userinfo.provider_identity.0.email, null + ) + automation_resman_sa_iam = ( + local.automation_resman_sa == null + ? [] + : ["serviceAccount:${local.automation_resman_sa}"] ) - billing_ext = var.billing_account.organization_id == null - billing_org = var.billing_account.organization_id == var.organization.id - billing_org_ext = !local.billing_ext && !local.billing_org branch_optional_sa_lists = { dp-dev = compact([try(module.branch-dp-dev-sa.0.iam_email, "")]) dp-prod = compact([try(module.branch-dp-prod-sa.0.iam_email, "")]) @@ -51,16 +49,16 @@ locals { } cicd_workflow_var_files = { stage_2 = [ - "00-bootstrap.auto.tfvars.json", - "01-resman.auto.tfvars.json", + "0-bootstrap.auto.tfvars.json", + "1-resman.auto.tfvars.json", "globals.auto.tfvars.json" ] stage_3 = [ - "00-bootstrap.auto.tfvars.json", - "01-resman.auto.tfvars.json", + "0-bootstrap.auto.tfvars.json", + "1-resman.auto.tfvars.json", "globals.auto.tfvars.json", - "02-networking.auto.tfvars.json", - "02-security.auto.tfvars.json" + "2-networking.auto.tfvars.json", + "2-security.auto.tfvars.json" ] } custom_roles = coalesce(var.custom_roles, {}) @@ -74,8 +72,7 @@ locals { k => "${v}@${var.organization.domain}" } groups_iam = { - for k, v in local.groups : - k => "group:${v}" + for k, v in local.groups : k => v != null ? "group:${v}" : null } identity_providers = coalesce( try(var.automation.federated_identity_providers, null), {} diff --git a/fast/stages/01-resman/organization.tf b/fast/stages/1-resman/organization.tf similarity index 66% rename from fast/stages/01-resman/organization.tf rename to fast/stages/1-resman/organization.tf index 7ecf79523..3d7db46db 100644 --- a/fast/stages/01-resman/organization.tf +++ b/fast/stages/1-resman/organization.tf @@ -16,7 +16,6 @@ # tfdoc:file:description Organization policies. - locals { all_drs_domains = concat( [var.organization.customer_id], @@ -47,7 +46,7 @@ module "organization" { module.branch-network-sa.iam_email ] }, - local.billing_org ? { + var.billing_account.is_org_level ? { "roles/billing.costsManager" = concat( local.branch_optional_sa_lists.pf-dev, local.branch_optional_sa_lists.pf-prod @@ -85,6 +84,9 @@ module "organization" { } org_policies_data_path = "${var.data_dir}/org-policies" + # do not assign tagViewer or tagUser roles here on tag keys and values as + # they are managed authoritatively and will break multitenant stages + tags = { (var.tag_names.context) = { description = "Resource management context." @@ -106,45 +108,10 @@ module "organization" { production = null } } + (var.tag_names.tenant) = { + description = "Organization tenant." + } } } -# organization policy admin role assigned with a condition on tags - -resource "google_organization_iam_member" "org_policy_admin_dp" { - for_each = !var.fast_features.data_platform ? {} : { - data-dev = ["data", "development", module.branch-dp-dev-sa.0.iam_email] - data-prod = ["data", "production", module.branch-dp-prod-sa.0.iam_email] - } - org_id = var.organization.id - role = "roles/orgpolicy.policyAdmin" - member = each.value.2 - condition { - title = "org_policy_tag_dp_scoped" - description = "Org policy tag scoped grant for ${each.value.0}/${each.value.1}." - expression = <<-END - resource.matchTag('${var.organization.id}/${var.tag_names.context}', '${each.value.0}') - && - resource.matchTag('${var.organization.id}/${var.tag_names.environment}', '${each.value.1}') - END - } -} - -resource "google_organization_iam_member" "org_policy_admin_pf" { - for_each = !var.fast_features.project_factory ? {} : { - pf-dev = ["teams", "development", module.branch-pf-dev-sa.0.iam_email] - pf-prod = ["teams", "production", module.branch-pf-prod-sa.0.iam_email] - } - org_id = var.organization.id - role = "roles/orgpolicy.policyAdmin" - member = each.value.2 - condition { - title = "org_policy_tag_pf_scoped" - description = "Org policy tag scoped grant for ${each.value.0}/${each.value.1}." - expression = <<-END - resource.matchTag('${var.organization.id}/${var.tag_names.context}', '${each.value.0}') - && - resource.matchTag('${var.organization.id}/${var.tag_names.environment}', '${each.value.1}') - END - } -} +# organization policy conditional roles are in the relevant branch files diff --git a/fast/stages/01-resman/outputs-files.tf b/fast/stages/1-resman/outputs-files.tf similarity index 94% rename from fast/stages/01-resman/outputs-files.tf rename to fast/stages/1-resman/outputs-files.tf index bd281d451..f7f080dd9 100644 --- a/fast/stages/01-resman/outputs-files.tf +++ b/fast/stages/1-resman/outputs-files.tf @@ -30,7 +30,7 @@ resource "local_file" "providers" { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${local.outputs_location}/tfvars/01-resman.auto.tfvars.json" + filename = "${local.outputs_location}/tfvars/1-resman.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/01-resman/outputs-gcs.tf b/fast/stages/1-resman/outputs-gcs.tf similarity index 96% rename from fast/stages/01-resman/outputs-gcs.tf rename to fast/stages/1-resman/outputs-gcs.tf index f1db11ef5..5b9f5d851 100644 --- a/fast/stages/01-resman/outputs-gcs.tf +++ b/fast/stages/1-resman/outputs-gcs.tf @@ -25,7 +25,7 @@ resource "google_storage_bucket_object" "providers" { resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket - name = "tfvars/01-resman.auto.tfvars.json" + name = "tfvars/1-resman.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/01-resman/outputs.tf b/fast/stages/1-resman/outputs.tf similarity index 70% rename from fast/stages/01-resman/outputs.tf rename to fast/stages/1-resman/outputs.tf index 9b1a67605..80ec2b9df 100644 --- a/fast/stages/01-resman/outputs.tf +++ b/fast/stages/1-resman/outputs.tf @@ -19,42 +19,42 @@ locals { cicd_workflow_attrs = { data_platform_dev = { service_account = try(module.branch-dp-dev-sa-cicd.0.email, null) - tf_providers_file = "03-data-platform-dev-providers.tf" + tf_providers_file = "3-data-platform-dev-providers.tf" tf_var_files = local.cicd_workflow_var_files.stage_3 } data_platform_prod = { service_account = try(module.branch-dp-prod-sa-cicd.0.email, null) - tf_providers_file = "03-data-platform-prod-providers.tf" + tf_providers_file = "3-data-platform-prod-providers.tf" tf_var_files = local.cicd_workflow_var_files.stage_3 } gke_dev = { service_account = try(module.branch-gke-dev-sa-cicd.0.email, null) - tf_providers_file = "03-gke-dev-providers.tf" + tf_providers_file = "3-gke-dev-providers.tf" tf_var_files = local.cicd_workflow_var_files.stage_3 } gke_prod = { service_account = try(module.branch-gke-prod-sa-cicd.0.email, null) - tf_providers_file = "03-gke-prod-providers.tf" + tf_providers_file = "3-gke-prod-providers.tf" tf_var_files = local.cicd_workflow_var_files.stage_3 } networking = { service_account = try(module.branch-network-sa-cicd.0.email, null) - tf_providers_file = "02-networking-providers.tf" + tf_providers_file = "2-networking-providers.tf" tf_var_files = local.cicd_workflow_var_files.stage_2 } project_factory_dev = { service_account = try(module.branch-pf-dev-sa-cicd.0.email, null) - tf_providers_file = "03-project-factory-dev-providers.tf" + tf_providers_file = "3-project-factory-dev-providers.tf" tf_var_files = local.cicd_workflow_var_files.stage_3 } project_factory_prod = { service_account = try(module.branch-pf-prod-sa-cicd.0.email, null) - tf_providers_file = "03-project-factory-prod-providers.tf" + tf_providers_file = "3-project-factory-prod-providers.tf" tf_var_files = local.cicd_workflow_var_files.stage_3 } security = { service_account = try(module.branch-security-sa-cicd.0.email, null) - tf_providers_file = "02-security-providers.tf" + tf_providers_file = "2-security-providers.tf" tf_var_files = local.cicd_workflow_var_files.stage_2 } } @@ -76,11 +76,11 @@ locals { data-platform-prod = try(module.branch-dp-prod-folder.0.id, null) gke-dev = try(module.branch-gke-dev-folder.0.id, null) gke-prod = try(module.branch-gke-prod-folder.0.id, null) - networking = module.branch-network-folder.id - networking-dev = module.branch-network-dev-folder.id - networking-prod = module.branch-network-prod-folder.id + networking = try(module.branch-network-folder.id, null) + networking-dev = try(module.branch-network-dev-folder.id, null) + networking-prod = try(module.branch-network-prod-folder.id, null) sandbox = try(module.branch-sandbox-folder.0.id, null) - security = module.branch-security-folder.id + security = try(module.branch-security-folder.id, null) teams = try(module.branch-teams-folder.0.id, null) }, { @@ -98,74 +98,85 @@ locals { ) providers = merge( { - "02-networking" = templatefile(local._tpl_providers, { - bucket = module.branch-network-gcs.name - name = "networking" - sa = module.branch-network-sa.email + "2-networking" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-network-gcs.name + name = "networking" + sa = module.branch-network-sa.email }) - "02-security" = templatefile(local._tpl_providers, { - bucket = module.branch-security-gcs.name - name = "security" - sa = module.branch-security-sa.email + "2-security" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-security-gcs.name + name = "security" + sa = module.branch-security-sa.email }) }, !var.fast_features.data_platform ? {} : { - "03-data-platform-dev" = templatefile(local._tpl_providers, { - bucket = module.branch-dp-dev-gcs.0.name - name = "dp-dev" - sa = module.branch-dp-dev-sa.0.email + "3-data-platform-dev" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-dp-dev-gcs.0.name + name = "dp-dev" + sa = module.branch-dp-dev-sa.0.email }) - "03-data-platform-prod" = templatefile(local._tpl_providers, { - bucket = module.branch-dp-prod-gcs.0.name - name = "dp-prod" - sa = module.branch-dp-prod-sa.0.email + "3-data-platform-prod" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-dp-prod-gcs.0.name + name = "dp-prod" + sa = module.branch-dp-prod-sa.0.email }) }, !var.fast_features.gke ? {} : { - "03-gke-dev" = templatefile(local._tpl_providers, { - bucket = module.branch-gke-dev-gcs.0.name - name = "gke-dev" - sa = module.branch-gke-dev-sa.0.email + "3-gke-dev" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-gke-dev-gcs.0.name + name = "gke-dev" + sa = module.branch-gke-dev-sa.0.email }) - "03-gke-prod" = templatefile(local._tpl_providers, { - bucket = module.branch-gke-prod-gcs.0.name - name = "gke-prod" - sa = module.branch-gke-prod-sa.0.email + "3-gke-prod" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-gke-prod-gcs.0.name + name = "gke-prod" + sa = module.branch-gke-prod-sa.0.email }) }, !var.fast_features.project_factory ? {} : { - "03-project-factory-dev" = templatefile(local._tpl_providers, { - bucket = module.branch-pf-dev-gcs.0.name - name = "team-dev" - sa = module.branch-pf-dev-sa.0.email + "3-project-factory-dev" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-pf-dev-gcs.0.name + name = "team-dev" + sa = module.branch-pf-dev-sa.0.email }) - "03-project-factory-prod" = templatefile(local._tpl_providers, { - bucket = module.branch-pf-prod-gcs.0.name - name = "team-prod" - sa = module.branch-pf-prod-sa.0.email + "3-project-factory-prod" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-pf-prod-gcs.0.name + name = "team-prod" + sa = module.branch-pf-prod-sa.0.email }) }, !var.fast_features.sandbox ? {} : { - "99-sandbox" = templatefile(local._tpl_providers, { - bucket = module.branch-sandbox-gcs.0.name - name = "sandbox" - sa = module.branch-sandbox-sa.0.email + "9-sandbox" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-sandbox-gcs.0.name + name = "sandbox" + sa = module.branch-sandbox-sa.0.email }) }, !var.fast_features.teams ? {} : merge( { - "03-teams" = templatefile(local._tpl_providers, { - bucket = module.branch-teams-gcs.0.name - name = "teams" - sa = module.branch-teams-sa.0.email + "3-teams" = templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-teams-gcs.0.name + name = "teams" + sa = module.branch-teams-sa.0.email }) }, { for k, v in module.branch-teams-team-sa : - "03-teams-${k}" => templatefile(local._tpl_providers, { - bucket = module.branch-teams-team-gcs[k].name - name = "teams" - sa = v.email + "3-teams-${k}" => templatefile(local._tpl_providers, { + backend_extra = null + bucket = module.branch-teams-team-gcs[k].name + name = "teams" + sa = v.email }) } ) @@ -190,7 +201,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_names = var.tag_names + tag_values = { for k, v in module.organization.tag_values : k => v.id } } } diff --git a/fast/stages/1-resman/templates/providers.tf.tpl b/fast/stages/1-resman/templates/providers.tf.tpl new file mode 100644 index 000000000..d1c224c5c --- /dev/null +++ b/fast/stages/1-resman/templates/providers.tf.tpl @@ -0,0 +1,33 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +terraform { + backend "gcs" { + bucket = "${bucket}" + impersonate_service_account = "${sa}" + %{~ if backend_extra != null ~} + ${indent(4, backend_extra)} + %{~ endif ~} + } +} +provider "google" { + impersonate_service_account = "${sa}" +} +provider "google-beta" { + impersonate_service_account = "${sa}" +} + +# end provider.tf for ${name} diff --git a/fast/stages/1-resman/templates/workflow-github.yaml b/fast/stages/1-resman/templates/workflow-github.yaml new file mode 100644 index 000000000..41c51411d --- /dev/null +++ b/fast/stages/1-resman/templates/workflow-github.yaml @@ -0,0 +1,198 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "FAST ${stage_name} stage" + +on: + pull_request: + branches: + - main + types: + - closed + - opened + - synchronize + +env: + FAST_OUTPUTS_BUCKET: ${outputs_bucket} + FAST_SERVICE_ACCOUNT: ${service_account} + FAST_WIF_PROVIDER: ${identity_provider} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + TF_PROVIDERS_FILE: ${tf_providers_file} + TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + TF_VERSION: 1.3.2 + +jobs: + fast-pr: + permissions: + contents: read + id-token: write + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - id: checkout + name: Checkout repository + uses: actions/checkout@v3 + + # set up SSH key authentication to the modules repository + - id: ssh-config + name: Configure SSH authentication + run: | + ssh-agent -a "$SSH_AUTH_SOCK" > /dev/null + ssh-add - <<< "$${{ secrets.CICD_MODULES_KEY }}" + + # set up authentication via Workload identity Federation + - id: gcp-auth + name: Authenticate to Google Cloud + uses: google-github-actions/auth@v0 + with: + workload_identity_provider: $${{ env.FAST_WIF_PROVIDER }} + service_account: $${{ env.FAST_SERVICE_ACCOUNT }} + access_token_lifetime: 3600s + + - id: gcp-sdk + name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v0 + with: + install_components: alpha + + # copy provider and tfvars files + - id: tf-config + name: Copy Terraform output files + run: | + gcloud alpha storage cp -r \ + "gs://$${{env.FAST_OUTPUTS_BUCKET}}/providers/$${{env.TF_PROVIDERS_FILE}}" ./ + gcloud alpha storage cp -r \ + "gs://$${{env.FAST_OUTPUTS_BUCKET}}/tfvars" ./ + for f in $${{env.TF_VAR_FILES}}; do + ln -s "tfvars/$f" ./ + done + + - id: tf-setup + name: Set up Terraform + uses: hashicorp/setup-terraform@v2.0.3 + with: + terraform_version: $${{ env.TF_VERSION }} + + # run Terraform init/validate/plan + - id: tf-init + name: Terraform init + continue-on-error: true + run: | + terraform init -no-color + + - id: tf-validate + continue-on-error: true + name: Terraform validate + run: terraform validate -no-color + + - id: tf-plan + name: Terraform plan + continue-on-error: true + run: | + terraform plan -input=false -out ../plan.out -no-color + + - id: tf-apply + if: github.event.pull_request.merged == true && success() + name: Terraform apply + continue-on-error: true + run: | + terraform apply -input=false -auto-approve -no-color ../plan.out + + - id: pr-comment + name: Post comment to Pull Request + continue-on-error: true + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' + env: + PLAN: $${{ steps.tf-plan.outputs.stdout }}\n$${{ steps.tf-plan.outputs.stderr }} + with: + script: | + const output = `### Terraform Initialization \`$${{ steps.tf-init.outcome }}\` + + ### Terraform Validation \`$${{ steps.tf-validate.outcome }}\` + +
Validation Output + + \`\`\`\n + $${{ steps.tf-validate.outputs.stdout }} + \`\`\` + +
+ + ### Terraform Plan \`$${{ steps.tf-plan.outcome }}\` + +
Show Plan + + \`\`\`\n + $${process.env.PLAN.split('\n').filter(l => l.match(/^([A-Z\s].*|)$$/)).join('\n')} + \`\`\` + +
+ + ### Terraform Apply \`$${{ steps.tf-apply.outcome }}\` + + *Pusher: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`, Working Directory: \`$${{ env.tf_actions_working_dir }}\`, Workflow: \`$${{ github.workflow }}\`*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - id: pr-short-comment + name: Post comment to Pull Request + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' && steps.pr-comment.outcome != 'success' + with: + script: | + const output = `### Terraform Initialization \`$${{ steps.tf-init.outcome }}\` + + ### Terraform Validation \`$${{ steps.tf-validate.outcome }}\` + + ### Terraform Plan \`$${{ steps.tf-plan.outcome }}\` + + Plan output is in the action log. + + ### Terraform Apply \`$${{ steps.tf-apply.outcome }}\` + + *Pusher: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`, Working Directory: \`$${{ env.tf_actions_working_dir }}\`, Workflow: \`$${{ github.workflow }}\`*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - id: check-init + name: Check init failure + if: steps.tf-init.outcome != 'success' + run: exit 1 + + - id: check-validate + name: Check validate failure + if: steps.tf-validate.outcome != 'success' + run: exit 1 + + - id: check-plan + name: Check plan failure + if: steps.tf-plan.outcome != 'success' + run: exit 1 + + - id: check-apply + name: Check apply failure + if: github.event.pull_request.merged == true && steps.tf-apply.outcome != 'success' + run: exit 1 diff --git a/fast/stages/1-resman/templates/workflow-gitlab.yaml b/fast/stages/1-resman/templates/workflow-gitlab.yaml new file mode 100644 index 000000000..8981e70b3 --- /dev/null +++ b/fast/stages/1-resman/templates/workflow-gitlab.yaml @@ -0,0 +1,120 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +default: + before_script: + - echo "$${CI_JOB_JWT_V2}" > token.txt + image: + name: hashicorp/terraform + entrypoint: + - "/usr/bin/env" + - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +variables: + GOOGLE_CREDENTIALS: cicd-sa-credentials.json + FAST_OUTPUTS_BUCKET: ${outputs_bucket} + FAST_SERVICE_ACCOUNT: ${service_account} + FAST_WIF_PROVIDER: ${identity_provider} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + TF_PROVIDERS_FILE: ${tf_providers_file} + TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + +stages: + - gcp-auth + - tf-files + - tf-plan + - tf-apply + +cache: + key: gcp-auth + paths: + - cicd-sa-credentials.json + - .tf-setup + +gcp-auth: + image: + name: google/cloud-sdk:slim + stage: gcp-auth + script: + - | + gcloud iam workload-identity-pools create-cred-config \ + $${FAST_WIF_PROVIDER} \ + --service-account=$${FAST_SERVICE_ACCOUNT} \ + --service-account-token-lifetime-seconds=3600 \ + --output-file=$${GOOGLE_CREDENTIALS} \ + --credential-source-file=token.txt +tf-files: + dependencies: + - gcp-auth + image: + name: google/cloud-sdk:slim + stage: tf-files + script: + # - gcloud components install -q alpha + - gcloud config set auth/credential_file_override $${GOOGLE_CREDENTIALS} + - mkdir -p .tf-setup + - | + gcloud alpha storage cp -r \ + "gs://$${FAST_OUTPUTS_BUCKET}/providers/$${TF_PROVIDERS_FILE}" .tf-setup/ + - | + gcloud alpha storage cp -r \ + "gs://$${FAST_OUTPUTS_BUCKET}/tfvars" .tf-setup/ + +tf-plan: + # uncomment the following lines and set the SSH key secret for private modules repo + # before_script: + # - | + # ssh-agent -a $SSH_AUTH_SOCK > /dev/null + # echo "$CICD_MODULES_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null + # mkdir -p ~/.ssh + # ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts + # ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts + stage: tf-plan + script: + - cp .tf-setup/$${TF_PROVIDERS_FILE} ./ + - | + for f in $${TF_VAR_FILES}; do + ln -s ".tf-setup/tfvars/$f" ./ + done + - terraform init + - terraform validate + - terraform plan + dependencies: + - tf-files + +tf-apply: + # uncomment the following lines and set the SSH key secret for private modules repo + # before_script: + # - | + # ssh-agent -a $SSH_AUTH_SOCK > /dev/null + # echo "$CICD_MODULES_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null + # mkdir -p ~/.ssh + # ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts + # ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts + stage: tf-apply + script: + - cp .tf-setup/$${TF_PROVIDERS_FILE} ./ + - | + for f in $${TF_VAR_FILES}; do + ln -s ".tf-setup/tfvars/$f" ./ + done + - terraform init + - terraform validate + - terraform apply -input=false -auto-approve + dependencies: + - tf-files + when: manual + only: + variables: + - $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH diff --git a/fast/stages/1-resman/templates/workflow-sourcerepo.yaml b/fast/stages/1-resman/templates/workflow-sourcerepo.yaml new file mode 100644 index 000000000..446c9c960 --- /dev/null +++ b/fast/stages/1-resman/templates/workflow-sourcerepo.yaml @@ -0,0 +1,98 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +steps: + - name: alpine:3 + id: tf-download + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + mkdir -p /builder/home/.local/bin + wget https://releases.hashicorp.com/terraform/$${_TF_VERSION}/terraform_$${_TF_VERSION}_linux_amd64.zip + unzip terraform_$${_TF_VERSION}_linux_amd64.zip -d /builder/home/.local/bin + rm terraform_$${_TF_VERSION}_linux_amd64.zip + chmod 755 /builder/home/.local/bin/terraform + - name: alpine:3 + id: tf-check-format + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform fmt -recursive -check /workspace/ + - name: gcr.io/google.com/cloudsdktool/cloud-sdk:alpine + id: tf-files + entrypoint: bash + args: + - -eEuo + - pipefail + - -c + - |- + /google-cloud-sdk/bin/gsutil cp \ + gs://$${_FAST_OUTPUTS_BUCKET}/providers/$${_TF_PROVIDERS_FILE} ./ + /google-cloud-sdk/bin/gsutil cp -r \ + gs://$${_FAST_OUTPUTS_BUCKET}/tfvars ./ + for f in $${_TF_VAR_FILES}; do + ln -s tfvars/$f ./ + done + - name: alpine:3 + id: tf-init + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform init -no-color + - name: alpine:3 + id: tf-check-validate + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform validate -no-color + - name: alpine:3 + id: tf-plan + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform plan -no-color -input=false -out plan.out + # store artifact and ask for approval here if needed + - name: alpine:3 + id: tf-apply + entrypoint: sh + args: + - -eEuo + - pipefail + - -c + - |- + terraform apply -no-color -input=false -auto-approve plan.out +options: + env: + - PATH=/usr/local/bin:/usr/bin:/bin:/builder/home/.local/bin + logging: CLOUD_LOGGING_ONLY +substitutions: + _FAST_OUTPUTS_BUCKET: ${outputs_bucket} + _TF_PROVIDERS_FILE: ${tf_providers_file} + _TF_VAR_FILES: ${tf_var_files == [] ? "''" : join("\n ", tf_var_files)} + _TF_VERSION: 1.3.2 diff --git a/fast/stages/01-resman/variables.tf b/fast/stages/1-resman/variables.tf similarity index 79% rename from fast/stages/01-resman/variables.tf rename to fast/stages/1-resman/variables.tf index 8b6f866bd..16c65ee83 100644 --- a/fast/stages/01-resman/variables.tf +++ b/fast/stages/1-resman/variables.tf @@ -18,7 +18,7 @@ # the tfvars file generated in stage 00 and stored in its outputs variable "automation" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Automation resources created by the bootstrap stage." type = object({ outputs_bucket = string @@ -36,65 +36,69 @@ variable "automation" { } variable "billing_account" { - # tfdoc:variable:source 00-bootstrap - description = "Billing account id and organization id ('nnnnnnnn' or null)." + # tfdoc:variable:source 0-bootstrap + description = "Billing account id. If billing account is not part of the same org set `is_org_level` to false." type = object({ - id = string - organization_id = number + id = string + is_org_level = optional(bool, true) }) + validation { + condition = var.billing_account.is_org_level != null + error_message = "Invalid `null` value for `billing_account.is_org_level`." + } } variable "cicd_repositories" { description = "CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed." type = object({ - data_platform_dev = object({ + data_platform_dev = optional(object({ branch = string identity_provider = string name = string type = string - }) - data_platform_prod = object({ + })) + data_platform_prod = optional(object({ branch = string identity_provider = string name = string type = string - }) - gke_dev = object({ + })) + gke_dev = optional(object({ branch = string identity_provider = string name = string type = string - }) - gke_prod = object({ + })) + gke_prod = optional(object({ branch = string identity_provider = string name = string type = string - }) - networking = object({ + })) + networking = optional(object({ branch = string identity_provider = string name = string type = string - }) - project_factory_dev = object({ + })) + project_factory_dev = optional(object({ branch = string identity_provider = string name = string type = string - }) - project_factory_prod = object({ + })) + project_factory_prod = optional(object({ branch = string identity_provider = string name = string type = string - }) - security = object({ + })) + security = optional(object({ branch = string identity_provider = string name = string type = string - }) + })) }) default = null validation { @@ -127,7 +131,7 @@ variable "cicd_repositories" { } variable "custom_roles" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Custom roles defined at the org level, in key => id format." type = object({ service_project_network_admin = string @@ -142,42 +146,34 @@ variable "data_dir" { } variable "fast_features" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-0-bootstrap description = "Selective control for top-level FAST features." type = object({ - data_platform = bool - gke = bool - project_factory = bool - sandbox = bool - teams = bool + data_platform = optional(bool, false) + gke = optional(bool, false) + project_factory = optional(bool, false) + sandbox = optional(bool, false) + teams = optional(bool, false) }) - default = { - data_platform = true - gke = true - project_factory = true - sandbox = true - teams = true - } - # nullable = false + default = {} + nullable = false } variable "groups" { - # tfdoc:variable:source 00-bootstrap - description = "Group names to grant organization-level permissions." - type = map(string) + # tfdoc:variable:source 0-bootstrap # https://cloud.google.com/docs/enterprise/setup-checklist - default = { - gcp-billing-admins = "gcp-billing-admins", - gcp-devops = "gcp-devops", - gcp-network-admins = "gcp-network-admins" - gcp-organization-admins = "gcp-organization-admins" - gcp-security-admins = "gcp-security-admins" - gcp-support = "gcp-support" - } + description = "Group names to grant organization-level permissions." + type = object({ + gcp-devops = optional(string) + gcp-network-admins = optional(string) + gcp-security-admins = optional(string) + }) + default = {} + nullable = false } variable "locations" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Optional locations for GCS, BigQuery, and logging buckets created here." type = object({ bq = string @@ -195,7 +191,7 @@ variable "locations" { } variable "organization" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Organization details." type = object({ domain = string @@ -219,7 +215,7 @@ variable "outputs_location" { } variable "prefix" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Prefix used for resources that need unique names. Use 9 characters or less." type = string @@ -234,10 +230,12 @@ variable "tag_names" { type = object({ context = string environment = string + tenant = string }) default = { context = "context" environment = "environment" + tenant = "tenant" } nullable = false validation { diff --git a/fast/stages/02-networking-peering/.gitignore b/fast/stages/2-networking-a-peering/.gitignore similarity index 100% rename from fast/stages/02-networking-peering/.gitignore rename to fast/stages/2-networking-a-peering/.gitignore diff --git a/fast/stages/02-networking-peering/IAM.md b/fast/stages/2-networking-a-peering/IAM.md similarity index 100% rename from fast/stages/02-networking-peering/IAM.md rename to fast/stages/2-networking-a-peering/IAM.md diff --git a/fast/stages/02-networking-peering/README.md b/fast/stages/2-networking-a-peering/README.md similarity index 71% rename from fast/stages/02-networking-peering/README.md rename to fast/stages/2-networking-a-peering/README.md index c7829f0fb..c066423cd 100644 --- a/fast/stages/02-networking-peering/README.md +++ b/fast/stages/2-networking-a-peering/README.md @@ -15,6 +15,33 @@ The following diagram illustrates the high-level design, and should be used as a Networking diagram

+## Table of contents + +- [Design overview and choices](#design-overview-and-choices) + - [VPC design](#vpc-design) + - [External connectivity](#external-connectivity) + - [Internal connectivity](#internal-connectivity) + - [IP ranges, subnetting, routing](#ip-ranges-subnetting-routing) + - [Internet egress](#internet-egress) + - [VPC and Hierarchical Firewall](#vpc-and-hierarchical-firewall) + - [DNS](#dns) +- [Stage structure and files layout](#stage-structure-and-files-layout) + - [VPCs](#vpcs) + - [VPNs](#vpns) + - [Routing and BGP](#routing-and-bgp) + - [Firewall](#firewall) + - [DNS architecture](#dns-architecture) + - [Private Google Access](#private-google-access) +- [How to run this stage](#how-to-run-this-stage) + - [Provider and Terraform variables](#provider-and-terraform-variables) + - [Impersonating the automation service account](#impersonating-the-automation-service-account) + - [Variable configuration](#variable-configuration) + - [Running the stage](#running-the-stage) + - [Post-deployment activities](#post-deployment-activities) +- [Customizations](#customizations) + - [Changing default regions](#changing-default-regions) + - [Adding an environment](#adding-an-environment) + ## Design overview and choices ### VPC design @@ -44,13 +71,13 @@ As mentioned initially, there are of course other ways to implement internal con This is a summary of the main options: -- [HA VPN](https://cloud.google.com/network-connectivity/docs/vpn/concepts/topologies) (implemented by [02-networking-vpn](../02-networking-vpn/)) +- [HA VPN](https://cloud.google.com/network-connectivity/docs/vpn/concepts/topologies) (implemented by [2-networking-vpn](../2-networking-b-vpn/)) - Pros: simple compatibility with GCP services that leverage peering internally, better control on routes, avoids peering groups shared quotas and limits - Cons: additional cost, marginal increase in latency, requires multiple tunnels for full bandwidth - [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering) (implemented here) - Pros: no additional costs, full bandwidth with no configurations, no extra latency, total environment isolation - Cons: no transitivity (e.g. to GKE masters, Cloud SQL, etc.), no selective exchange of routes, several quotas and limits shared between VPCs in a peering group -- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (implemented by [02-networking-nva](../02-networking-nva/)) +- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (implemented by [2-networking-nva](../2-networking-c-nva/)) - Pros: additional security features (e.g. IPS), potentially better integration with on-prem systems by using the same vendor - Cons: complex HA/failover setup, limited by VM bandwidth and scale, additional costs for VMs and licenses, out of band management of a critical cloud component @@ -120,58 +147,7 @@ From cloud, the `example.com` domain (used as a placeholder) is forwarded to on- This configuration is battle-tested, and flexible enough to lend itself to simple modifications without subverting its design, for example by forwarding and peering root zones to bypass Cloud DNS external resolution. -## How to run this stage - -This stage is meant to be executed after the [resman](../01-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../00-bootstrap) stage. - -It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. - -Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. - -### Providers configuration - -The default way of making sure you have the right permissions, is to use the identity of the service account pre-created for this stage during the [resource management](../01-resman) stage, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). - -To simplify setup, the previous stage pre-configures a valid providers file in its output, and optionally writes it to a local file if the `outputs_location` variable is set to a valid path. - -If you have set a valid value for `outputs_location` in the bootstrap stage, simply link the relevant `providers.tf` file from this stage's folder in the path you specified: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/providers/02-networking-providers.tf . -``` - -If you have not configured `outputs_location` in bootstrap, you can derive the providers file from that stage's outputs: - -```bash -cd ../01-resman -terraform output -json providers | jq -r '.["02-networking"]' \ - > ../02-networking/providers.tf -``` - -### Variable configuration - -There are two broad sets of variables you will need to fill in: - -- variables shared by other stages (org id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) -- variables specific to resources managed by this stage - -To avoid the tedious job of filling in the first group of variables with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. - -If you have set a valid value for `outputs_location` in the bootstrap and in the resman stage, simply link the relevant `*.auto.tfvars.json` files from this stage's folder in the path you specified. -The `*` above is set to the name of the stage that produced it, except for `globals.auto.tfvars.json` which is also generated by the bootstrap stage, containing global values compiled manually for the bootstrap stage. -For this stage, link the following files: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/tfvars/globals.auto.tfvars.json . -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . -``` - -A second set of variables is specific to this stage, they are all optional so if you need to customize them, create an extra `terraform.tfvars` file. - -Please refer to the [Variables](#variables) table below for a map of the variable origins, and to the sections below on how to adapt this stage to your networking configuration. +## Stage structure and files layout ### VPCs @@ -224,7 +200,72 @@ DNS queries sent to the on-premises infrastructure come from the `35.199.192.0/1 The [Inbound DNS Policy](https://cloud.google.com/dns/docs/server-policies-overview#dns-server-policy-in) defined in module `landing-vpc` ([`landing.tf`](./landing.tf)) automatically reserves the first available IP address on each created subnet (typically the third one in a CIDR) to expose the Cloud DNS service so that it can be consumed from outside of GCP. -### Private Google Access +## How to run this stage + +This stage is meant to be executed after the [resource management](../1-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../0-bootstrap) stage. + +It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. + +Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. + +### Provider and Terraform variables + +As all other FAST stages, the [mechanism used to pass variable values and pre-built provider files from one stage to the next](../0-bootstrap/README.md#output-files-and-cross-stage-variables) is also leveraged here. + +The commands to link or copy the provider and terraform variable files can be easily derived from the `stage-links.sh` script in the FAST root folder, passing it a single argument with the local output files folder (if configured) or the GCS output bucket in the automation project (derived from stage 0 outputs). The following examples demonstrate both cases, and the resulting commands that then need to be copy/pasted and run. + +```bash +../../stage-links.sh ~/fast-config + +# copy and paste the following commands for '2-networking-a-peering' + +ln -s ~/fast-config/providers/2-networking-providers.tf ./ +ln -s ~/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json ./ +``` + +```bash +../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '2-networking-a-peering' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/2-networking-providers.tf ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/globals.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/1-resman.auto.tfvars.json ./ +``` + +### Impersonating the automation service account + +The preconfigured provider file uses impersonation to run with this stage's automation service account's credentials. The `gcp-devops` and `organization-admins` groups have the necessary IAM bindings in place to do that, so make sure the current user is a member of one of those groups. + +### Variable configuration + +Variables in this stage -- like most other FAST stages -- are broadly divided into three separate sets: + +- variables which refer to global values for the whole organization (org id, billing account id, prefix, etc.), which are pre-populated via the `globals.auto.tfvars.json` file linked or copied above +- variables which refer to resources managed by previous stage, which are prepopulated here via the `0-bootstrap.auto.tfvars.json` and `1-resman.auto.tfvars.json` files linked or copied above +- and finally variables that optionally control this stage's behaviour and customizations, and can to be set in a custom `terraform.tfvars` file + +The latter set is explained in the [Customization](#customizations) sections below, and the full list can be found in the [Variables](#variables) table at the bottom of this document. + +### Running the stage + +Once provider and variable values are in place and the correct user is configured, the stage can be run: + +```bash +terraform init +terraform apply +``` + +### Post-deployment activities + +- On-prem routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recomment aggregating routes as much as possible. +- On-prem routers should accept BGP sessions from their cloud peers. +- On-prem DNS servers should have forward zones for GCP-managed ones. + +#### Private Google Access [Private Google Access](https://cloud.google.com/vpc/docs/private-google-access) (or PGA) enables VMs and on-prem systems to consume Google APIs from within the Google network, and is already fully configured on this environment. @@ -238,22 +279,15 @@ Per variable `vpn_onprem_configs` such ranges are advertised to onprem - further - A private DNS zone for `googleapis.com` should be created and configured per [this article](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid#config-domain), as implemented in module `googleapis-private-zone` in [`dns-landing.tf`](./dns-landing.tf) -### Preliminar activities - -Before running `terraform apply` on this stage, make sure to adapt all of `variables.tf` to your needs, to update all reference to regions (e.g. `europe-west1` or `ew1`) in the whole directory to match your preferences. - -If you're not using FAST, you'll also need to create a `providers.tf` file to configure the GCS backend and the service account to use to run the deployment. - -You're now ready to run `terraform init` and `apply`. - -### Post-deployment activities - -- On-prem routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recomment aggregating routes as much as possible. -- On-prem routers should accept BGP sessions from their cloud peers. -- On-prem DNS servers should have forward zones for GCP-managed ones. - ## Customizations +### Changing default regions + +Regions are defined via the `regions` variable which sets up a mapping between the `regions.primary` and `regions.secondary` logical names and actual GCP region names. If you need to change regions from the defaults: + +- change the values of the mappings in the `regions` variable to the regions you are going to use +- change the regions in the factory subnet files in the `data` folder + ### Adding an environment To create a new environment (e.g. `staging`), a few changes are required. @@ -262,10 +296,10 @@ Create a `spoke-staging.tf` file by copying `spoke-prod.tf` file, and adapt the new file by replacing the value "prod" with the value "staging". Running `diff spoke-dev.tf spoke-prod.tf` can help to see how environment files differ. -The new VPC requires a set of dedicated CIDRs, one per region, added to variable `custom_adv` (for example as `spoke_staging_ew1` and `spoke_staging_ew4`). +The new VPC requires a set of dedicated CIDRs, one per region, added to variable `custom_adv` (for example as `spoke_staging_primary` and `spoke_staging_secondary`). >`custom_adv` is a map that "resolves" CIDR names to actual addresses, and will be used later to configure routing. > -Variables managing L7 Interal Load Balancers (`l7ilb_subnets`) and Private Service Access (`psa_ranges`) should also be adapted, and subnets and firewall rules for the new spoke should be added as described above. +Variables managing L7 Internal Load Balancers (`l7ilb_subnets`) and Private Service Access (`psa_ranges`) should also be adapted, and subnets and firewall rules for the new spoke should be added as described above. DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS resolution to Landing through DNS peering, and optionally define a private zone (e.g. `dev.gcp.example.com`) which the landing peers to. To configure DNS for a new environment, copy one of the other environments DNS files [e.g. (dns-dev.tf](dns-dev.tf)) into a new `dns-*.tf` file suffixed with the environment name (e.g. `dns-staging.tf`), and update its content accordingly. Don't forget to add a peering zone from the landing to the newly created environment private zone. @@ -284,6 +318,7 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS | [monitoring.tf](./monitoring.tf) | Network monitoring dashboards. | | google_monitoring_dashboard | | [outputs.tf](./outputs.tf) | Module outputs. | | google_storage_bucket_object · local_file | | [peerings.tf](./peerings.tf) | None | net-vpc-peering | | +| [regions.tf](./regions.tf) | Compute short names for regions. | | | | [spoke-dev.tf](./spoke-dev.tf) | Dev spoke VPC and related resources. | net-cloudnat · net-vpc · net-vpc-firewall · project | google_project_iam_binding | | [spoke-prod.tf](./spoke-prod.tf) | Production spoke VPC and related resources. | net-cloudnat · net-vpc · net-vpc-firewall · project | google_project_iam_binding | | [test-resources.tf](./test-resources.tf) | temporary instances for testing | compute-vm | | @@ -295,23 +330,23 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | -| [billing_account](variables.tf#L25) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [folder_ids](variables.tf#L74) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 01-resman | -| [organization](variables.tf#L102) | Organization details. | object({…}) | ✓ | | 00-bootstrap | -| [prefix](variables.tf#L118) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | -| [custom_adv](variables.tf#L34) | Custom advertisement definitions in name => range format. | map(string) | | {…} | | -| [custom_roles](variables.tf#L51) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 00-bootstrap | -| [data_dir](variables.tf#L60) | Relative path for the folder storing configuration data for network resources. | string | | "data" | | -| [dns](variables.tf#L66) | Onprem DNS resolvers. | map(list(string)) | | {…} | | -| [l7ilb_subnets](variables.tf#L84) | Subnets used for L7 ILBs. | map(list(object({…}))) | | {…} | | -| [outputs_location](variables.tf#L112) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | +| [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#L92) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman | +| [organization](variables.tf#L126) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L142) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [custom_adv](variables.tf#L38) | Custom advertisement definitions in name => range format. | map(string) | | {…} | | +| [custom_roles](variables.tf#L55) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | +| [dns](variables.tf#L64) | Onprem DNS resolvers. | map(list(string)) | | {…} | | +| [factories_config](variables.tf#L72) | Configuration for network resource factories. | object({…}) | | {…} | | +| [l7ilb_subnets](variables.tf#L102) | Subnets used for L7 ILBs. | object({…}) | | {…} | | +| [outputs_location](variables.tf#L136) | 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. | map(object({…})) | | {…} | | -| [psa_ranges](variables.tf#L129) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | -| [region_trigram](variables.tf#L166) | Short names for GCP regions. | map(string) | | {…} | | -| [router_onprem_configs](variables.tf#L175) | Configurations for routers used for onprem connectivity. | map(object({…})) | | {…} | | -| [service_accounts](variables.tf#L193) | Automation service accounts in name => email format. | object({…}) | | null | 01-resman | -| [vpn_onprem_configs](variables.tf#L207) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | +| [psa_ranges](variables.tf#L153) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | +| [regions](variables.tf#L190) | Region definitions. | object({…}) | | {…} | | +| [router_onprem_configs](variables.tf#L202) | Configurations for routers used for onprem connectivity. | map(object({…})) | | {…} | | +| [service_accounts](variables.tf#L220) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | +| [vpn_onprem_configs](variables.tf#L234) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | ## Outputs diff --git a/fast/stages/02-networking-nva/data/cidrs.yaml b/fast/stages/2-networking-a-peering/data/cidrs.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/cidrs.yaml rename to fast/stages/2-networking-a-peering/data/cidrs.yaml diff --git a/fast/stages/02-networking-peering/data/dashboards/firewall_insights.json b/fast/stages/2-networking-a-peering/data/dashboards/firewall_insights.json similarity index 100% rename from fast/stages/02-networking-peering/data/dashboards/firewall_insights.json rename to fast/stages/2-networking-a-peering/data/dashboards/firewall_insights.json diff --git a/fast/stages/02-networking-peering/data/dashboards/vpn.json b/fast/stages/2-networking-a-peering/data/dashboards/vpn.json similarity index 100% rename from fast/stages/02-networking-peering/data/dashboards/vpn.json rename to fast/stages/2-networking-a-peering/data/dashboards/vpn.json diff --git a/fast/stages/02-networking-nva/data/firewall-rules/dev/rules.yaml b/fast/stages/2-networking-a-peering/data/firewall-rules/dev/rules.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/firewall-rules/dev/rules.yaml rename to fast/stages/2-networking-a-peering/data/firewall-rules/dev/rules.yaml diff --git a/fast/stages/02-networking-peering/data/firewall-rules/landing/rules.yaml b/fast/stages/2-networking-a-peering/data/firewall-rules/landing/rules.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/firewall-rules/landing/rules.yaml rename to fast/stages/2-networking-a-peering/data/firewall-rules/landing/rules.yaml diff --git a/fast/stages/02-networking-nva/data/hierarchical-policy-rules.yaml b/fast/stages/2-networking-a-peering/data/hierarchical-policy-rules.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/hierarchical-policy-rules.yaml rename to fast/stages/2-networking-a-peering/data/hierarchical-policy-rules.yaml diff --git a/fast/stages/02-networking-peering/data/subnets/dev/dev-dataplatform-ew1.yaml b/fast/stages/2-networking-a-peering/data/subnets/dev/dev-dataplatform-ew1.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/subnets/dev/dev-dataplatform-ew1.yaml rename to fast/stages/2-networking-a-peering/data/subnets/dev/dev-dataplatform-ew1.yaml diff --git a/fast/stages/02-networking-peering/data/subnets/dev/dev-default-ew1.yaml b/fast/stages/2-networking-a-peering/data/subnets/dev/dev-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/subnets/dev/dev-default-ew1.yaml rename to fast/stages/2-networking-a-peering/data/subnets/dev/dev-default-ew1.yaml diff --git a/fast/stages/02-networking-peering/data/subnets/dev/dev-gke-nodes-ew1.yaml b/fast/stages/2-networking-a-peering/data/subnets/dev/dev-gke-nodes-ew1.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/subnets/dev/dev-gke-nodes-ew1.yaml rename to fast/stages/2-networking-a-peering/data/subnets/dev/dev-gke-nodes-ew1.yaml diff --git a/fast/stages/02-networking-peering/data/subnets/landing/landing-default-ew1.yaml b/fast/stages/2-networking-a-peering/data/subnets/landing/landing-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/subnets/landing/landing-default-ew1.yaml rename to fast/stages/2-networking-a-peering/data/subnets/landing/landing-default-ew1.yaml diff --git a/fast/stages/02-networking-peering/data/subnets/prod/prod-default-ew1.yaml b/fast/stages/2-networking-a-peering/data/subnets/prod/prod-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/subnets/prod/prod-default-ew1.yaml rename to fast/stages/2-networking-a-peering/data/subnets/prod/prod-default-ew1.yaml diff --git a/fast/stages/02-networking-peering/diagram.png b/fast/stages/2-networking-a-peering/diagram.png similarity index 100% rename from fast/stages/02-networking-peering/diagram.png rename to fast/stages/2-networking-a-peering/diagram.png diff --git a/fast/stages/02-networking-peering/diagram.svg b/fast/stages/2-networking-a-peering/diagram.svg similarity index 100% rename from fast/stages/02-networking-peering/diagram.svg rename to fast/stages/2-networking-a-peering/diagram.svg diff --git a/fast/stages/02-networking-peering/dns-dev.tf b/fast/stages/2-networking-a-peering/dns-dev.tf similarity index 100% rename from fast/stages/02-networking-peering/dns-dev.tf rename to fast/stages/2-networking-a-peering/dns-dev.tf diff --git a/fast/stages/02-networking-peering/dns-landing.tf b/fast/stages/2-networking-a-peering/dns-landing.tf similarity index 100% rename from fast/stages/02-networking-peering/dns-landing.tf rename to fast/stages/2-networking-a-peering/dns-landing.tf diff --git a/fast/stages/02-networking-peering/dns-prod.tf b/fast/stages/2-networking-a-peering/dns-prod.tf similarity index 100% rename from fast/stages/02-networking-peering/dns-prod.tf rename to fast/stages/2-networking-a-peering/dns-prod.tf diff --git a/fast/stages/02-networking-peering/landing.tf b/fast/stages/2-networking-a-peering/landing.tf similarity index 82% rename from fast/stages/02-networking-peering/landing.tf rename to fast/stages/2-networking-a-peering/landing.tf index 83a0d509a..37e3adfd4 100644 --- a/fast/stages/02-networking-peering/landing.tf +++ b/fast/stages/2-networking-a-peering/landing.tf @@ -63,7 +63,7 @@ module "landing-vpc" { next_hop = "default-internet-gateway" } } - data_folder = "${var.data_dir}/subnets/landing" + data_folder = "${var.factories_config.data_dir}/subnets/landing" } module "landing-firewall" { @@ -74,18 +74,23 @@ module "landing-firewall" { disabled = true } factories_config = { - cidr_tpl_file = "${var.data_dir}/cidrs.yaml" - rules_folder = "${var.data_dir}/firewall-rules/landing" + cidr_tpl_file = "${var.factories_config.data_dir}/cidrs.yaml" + rules_folder = "${var.factories_config.data_dir}/firewall-rules/landing" } } -module "landing-nat-ew1" { +moved { + from = module.landing-nat-ew1 + to = module.landing-nat-primary +} + +module "landing-nat-primary" { source = "../../../modules/net-cloudnat" project_id = module.landing-project.project_id - region = "europe-west1" - name = "ew1" + region = var.regions.primary + name = local.region_shortnames[var.regions.primary] router_create = true - router_name = "prod-nat-ew1" + router_name = "prod-nat-${local.region_shortnames[var.regions.primary]}" router_network = module.landing-vpc.name router_asn = 4200001024 } diff --git a/fast/stages/02-networking-vpn/main.tf b/fast/stages/2-networking-a-peering/main.tf similarity index 76% rename from fast/stages/02-networking-vpn/main.tf rename to fast/stages/2-networking-a-peering/main.tf index f68d39eb8..5888752d7 100644 --- a/fast/stages/02-networking-vpn/main.tf +++ b/fast/stages/2-networking-a-peering/main.tf @@ -17,14 +17,14 @@ # tfdoc:file:description Networking folder and hierarchical policy. locals { + # combine all regions from variables and subnets + regions = distinct(concat( + values(var.regions), + values(module.dev-spoke-vpc.subnet_regions), + values(module.landing-vpc.subnet_regions), + values(module.prod-spoke-vpc.subnet_regions), + )) custom_roles = coalesce(var.custom_roles, {}) - l7ilb_subnets = { - for env, v in var.l7ilb_subnets : env => [ - for s in v : merge(s, { - active = true - name = "${env}-l7ilb-${s.region}" - })] - } stage3_sas_delegated_grants = [ "roles/composer.sharedVpcAgent", "roles/compute.networkUser", @@ -46,9 +46,9 @@ module "folder" { folder_create = var.folder_ids.networking == null id = var.folder_ids.networking firewall_policy_factory = { - cidr_file = "${var.data_dir}/cidrs.yaml" - policy_name = null - rules_file = "${var.data_dir}/hierarchical-policy-rules.yaml" + cidr_file = "${var.factories_config.data_dir}/cidrs.yaml" + policy_name = var.factories_config.firewall_policy_name + rules_file = "${var.factories_config.data_dir}/hierarchical-policy-rules.yaml" } firewall_policy_association = { factory-policy = "factory" diff --git a/fast/stages/02-networking-vpn/monitoring.tf b/fast/stages/2-networking-a-peering/monitoring.tf similarity index 93% rename from fast/stages/02-networking-vpn/monitoring.tf rename to fast/stages/2-networking-a-peering/monitoring.tf index 7b8b70c51..be3a47faa 100644 --- a/fast/stages/02-networking-vpn/monitoring.tf +++ b/fast/stages/2-networking-a-peering/monitoring.tf @@ -17,7 +17,7 @@ # tfdoc:file:description Network monitoring dashboards. locals { - dashboard_path = "${var.data_dir}/dashboards" + dashboard_path = "${var.factories_config.data_dir}/dashboards" dashboard_files = fileset(local.dashboard_path, "*.json") dashboards = { for filename in local.dashboard_files : diff --git a/fast/stages/02-networking-peering/outputs.tf b/fast/stages/2-networking-a-peering/outputs.tf similarity index 93% rename from fast/stages/02-networking-peering/outputs.tf rename to fast/stages/2-networking-a-peering/outputs.tf index 3b97b7f25..628c706b3 100644 --- a/fast/stages/02-networking-peering/outputs.tf +++ b/fast/stages/2-networking-a-peering/outputs.tf @@ -48,13 +48,13 @@ locals { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/02-networking.auto.tfvars.json" + filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket - name = "tfvars/02-networking.auto.tfvars.json" + name = "tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } @@ -89,8 +89,8 @@ output "tfvars" { output "vpn_gateway_endpoints" { description = "External IP Addresses for the GCP VPN gateways." value = local.enable_onprem_vpn == false ? null : { - onprem-ew1 = { - for v in module.landing-to-onprem-ew1-vpn[0].gateway.vpn_interfaces : + onprem-primary = { + for v in module.landing-to-onprem-primary-vpn[0].gateway.vpn_interfaces : v.id => v.ip_address } } diff --git a/fast/stages/02-networking-peering/peerings.tf b/fast/stages/2-networking-a-peering/peerings.tf similarity index 100% rename from fast/stages/02-networking-peering/peerings.tf rename to fast/stages/2-networking-a-peering/peerings.tf diff --git a/fast/stages/2-networking-a-peering/regions.tf b/fast/stages/2-networking-a-peering/regions.tf new file mode 100644 index 000000000..53514afa9 --- /dev/null +++ b/fast/stages/2-networking-a-peering/regions.tf @@ -0,0 +1,42 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Compute short names for regions. + +locals { + # only map when the first character would not work + _region_cardinal = { + southeast = "se" + } + # only map when the first character would not work + _region_geo = { + australia = "o" + } + # split in [geo, cardinal, number] tokens + _region_tokens = { + for v in local.regions : v => regexall("(?:[a-z]+)|(?:[0-9]+)", v) + } + region_shortnames = { + for k, v in local._region_tokens : k => join("", [ + # first token via geo alias map or first character + lookup(local._region_geo, v.0, substr(v.0, 0, 1)), + # first token via cardinal alias map or first character + lookup(local._region_cardinal, v.1, substr(v.1, 0, 1)), + # region number as is + v.2 + ]) + } +} diff --git a/fast/stages/02-networking-peering/spoke-dev.tf b/fast/stages/2-networking-a-peering/spoke-dev.tf similarity index 84% rename from fast/stages/02-networking-peering/spoke-dev.tf rename to fast/stages/2-networking-a-peering/spoke-dev.tf index e67cfb70d..ed7d13cb0 100644 --- a/fast/stages/02-networking-peering/spoke-dev.tf +++ b/fast/stages/2-networking-a-peering/spoke-dev.tf @@ -16,6 +16,19 @@ # tfdoc:file:description Dev spoke VPC and related resources. +locals { + _l7ilb_subnets_dev = [ + for v in var.l7ilb_subnets.dev : merge(v, { + active = true + region = lookup(var.regions, v.region, v.region) + })] + l7ilb_subnets_dev = [ + for v in local._l7ilb_subnets_dev : merge(v, { + name = "dev-l7ilb-${local.region_shortnames[v.region]}" + }) + ] +} + module "dev-spoke-project" { source = "../../../modules/project" billing_account = var.billing_account.id @@ -48,9 +61,9 @@ module "dev-spoke-vpc" { project_id = module.dev-spoke-project.project_id name = "dev-spoke-0" mtu = 1500 - data_folder = "${var.data_dir}/subnets/dev" + data_folder = "${var.factories_config.data_dir}/subnets/dev" psa_config = try(var.psa_ranges.dev, null) - subnets_proxy_only = local.l7ilb_subnets.dev + subnets_proxy_only = local.l7ilb_subnets_dev # set explicit routes for googleapis in case the default route is deleted routes = { private-googleapis = { @@ -74,8 +87,8 @@ module "dev-spoke-firewall" { disabled = true } factories_config = { - cidr_tpl_file = "${var.data_dir}/cidrs.yaml" - rules_folder = "${var.data_dir}/firewall-rules/dev" + cidr_tpl_file = "${var.factories_config.data_dir}/cidrs.yaml" + rules_folder = "${var.factories_config.data_dir}/firewall-rules/dev" } } @@ -84,7 +97,7 @@ module "dev-spoke-cloudnat" { source = "../../../modules/net-cloudnat" project_id = module.dev-spoke-project.project_id region = each.value - name = "dev-nat-${var.region_trigram[each.value]}" + name = "dev-nat-${local.region_shortnames[each.value]}" router_create = true router_network = module.dev-spoke-vpc.name router_asn = 4200001024 diff --git a/fast/stages/02-networking-vpn/spoke-prod.tf b/fast/stages/2-networking-a-peering/spoke-prod.tf similarity index 84% rename from fast/stages/02-networking-vpn/spoke-prod.tf rename to fast/stages/2-networking-a-peering/spoke-prod.tf index cf49152fa..f584b32da 100644 --- a/fast/stages/02-networking-vpn/spoke-prod.tf +++ b/fast/stages/2-networking-a-peering/spoke-prod.tf @@ -16,6 +16,19 @@ # tfdoc:file:description Production spoke VPC and related resources. +locals { + _l7ilb_subnets_prod = [ + for v in var.l7ilb_subnets.prod : merge(v, { + active = true + region = lookup(var.regions, v.region, v.region) + })] + l7ilb_subnets_prod = [ + for v in local._l7ilb_subnets_prod : merge(v, { + name = "prod-l7ilb-${local.region_shortnames[v.region]}" + }) + ] +} + module "prod-spoke-project" { source = "../../../modules/project" billing_account = var.billing_account.id @@ -48,9 +61,9 @@ module "prod-spoke-vpc" { project_id = module.prod-spoke-project.project_id name = "prod-spoke-0" mtu = 1500 - data_folder = "${var.data_dir}/subnets/prod" + data_folder = "${var.factories_config.data_dir}/subnets/prod" psa_config = try(var.psa_ranges.prod, null) - subnets_proxy_only = local.l7ilb_subnets.prod + subnets_proxy_only = local.l7ilb_subnets_prod # set explicit routes for googleapis in case the default route is deleted routes = { private-googleapis = { @@ -74,8 +87,8 @@ module "prod-spoke-firewall" { disabled = true } factories_config = { - cidr_tpl_file = "${var.data_dir}/cidrs.yaml" - rules_folder = "${var.data_dir}/firewall-rules/prod" + cidr_tpl_file = "${var.factories_config.data_dir}/cidrs.yaml" + rules_folder = "${var.factories_config.data_dir}/firewall-rules/prod" } } @@ -84,7 +97,7 @@ module "prod-spoke-cloudnat" { source = "../../../modules/net-cloudnat" project_id = module.prod-spoke-project.project_id region = each.value - name = "prod-nat-${var.region_trigram[each.value]}" + name = "prod-nat-${local.region_shortnames[each.value]}" router_create = true router_network = module.prod-spoke-vpc.name router_asn = 4200001024 diff --git a/fast/stages/02-networking-peering/test-resources.tf b/fast/stages/2-networking-a-peering/test-resources.tf similarity index 76% rename from fast/stages/02-networking-peering/test-resources.tf rename to fast/stages/2-networking-a-peering/test-resources.tf index 204971fec..67073845d 100644 --- a/fast/stages/02-networking-peering/test-resources.tf +++ b/fast/stages/2-networking-a-peering/test-resources.tf @@ -19,11 +19,11 @@ # module "test-vm-landing-0" { # source = "../../../modules/compute-vm" # project_id = module.landing-project.project_id -# zone = "europe-west1-b" +# zone = "${var.regions.primary}-b" # name = "test-vm-0" # network_interfaces = [{ # network = module.landing-vpc.self_link -# subnetwork = module.landing-vpc.subnet_self_links["europe-west1/landing-default-ew1"] +# subnetwork = module.landing-vpc.subnet_self_links["${var.regions.primary}/landing-default-${local.region_shortnames[var.regions.primary]}"] # }] # tags = ["ssh"] # service_account_create = true @@ -31,8 +31,8 @@ # image = "projects/debian-cloud/global/images/family/debian-10" # } # options = { -# spot = true -# termination_action = "STOP" +# spot = true +# termination_action = "STOP" # } # metadata = { # startup-script = < { bgp_peer = { address = cidrhost(t.session_range, 1) asn = t.peer_asn } - bgp_peer_options = local.bgp_peer_options_onprem.landing-ew1 + bgp_peer_options = local.bgp_peer_options_onprem.landing-primary bgp_session_range = "${cidrhost(t.session_range, 2)}/30" peer_external_gateway_interface = t.peer_external_gateway_interface shared_secret = t.secret diff --git a/fast/stages/02-networking-separate-envs/.gitignore b/fast/stages/2-networking-b-vpn/.gitignore similarity index 100% rename from fast/stages/02-networking-separate-envs/.gitignore rename to fast/stages/2-networking-b-vpn/.gitignore diff --git a/fast/stages/02-networking-separate-envs/IAM.md b/fast/stages/2-networking-b-vpn/IAM.md similarity index 100% rename from fast/stages/02-networking-separate-envs/IAM.md rename to fast/stages/2-networking-b-vpn/IAM.md diff --git a/fast/stages/02-networking-vpn/README.md b/fast/stages/2-networking-b-vpn/README.md similarity index 71% rename from fast/stages/02-networking-vpn/README.md rename to fast/stages/2-networking-b-vpn/README.md index 047a1189c..7a8983d81 100644 --- a/fast/stages/02-networking-vpn/README.md +++ b/fast/stages/2-networking-b-vpn/README.md @@ -15,6 +15,33 @@ The following diagram illustrates the high-level design, and should be used as a Networking diagram

+## Table of contents + +- [Design overview and choices](#design-overview-and-choices) + - [VPC design](#vpc-design) + - [External connectivity](#external-connectivity) + - [Internal connectivity](#internal-connectivity) + - [IP ranges, subnetting, routing](#ip-ranges-subnetting-routing) + - [Internet egress](#internet-egress) + - [VPC and Hierarchical Firewall](#vpc-and-hierarchical-firewall) + - [DNS](#dns) +- [Stage structure and files layout](#stage-structure-and-files-layout) + - [VPCs](#vpcs) + - [VPNs](#vpns) + - [Routing and BGP](#routing-and-bgp) + - [Firewall](#firewall) + - [DNS architecture](#dns-architecture) + - [Private Google Access](#private-google-access) +- [How to run this stage](#how-to-run-this-stage) + - [Provider and Terraform variables](#provider-and-terraform-variables) + - [Impersonating the automation service account](#impersonating-the-automation-service-account) + - [Variable configuration](#variable-configuration) + - [Running the stage](#running-the-stage) + - [Post-deployment activities](#post-deployment-activities) +- [Customizations](#customizations) + - [Changing default regions](#changing-default-regions) + - [Adding an environment](#adding-an-environment) + ## Design overview and choices ### VPC design @@ -45,10 +72,10 @@ This is a summary of the main options: - [HA VPN](https://cloud.google.com/network-connectivity/docs/vpn/concepts/topologies) (implemented here) - Pros: simple compatibility with GCP services that leverage peering internally, better control on routes, avoids peering groups shared quotas and limits - Cons: additional cost, marginal increase in latency, requires multiple tunnels for full bandwidth -- [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering) (implemented by [02-networking-peering](../02-networking-peering/)) +- [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering) (implemented by [2-networking-peering](../2-networking-a-peering/)) - Pros: no additional costs, full bandwidth with no configurations, no extra latency - Cons: no transitivity (e.g. to GKE masters, Cloud SQL, etc.), no selective exchange of routes, several quotas and limits shared between VPCs in a peering group -- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (implemented by [02-networking-nva](../02-networking-nva/)) +- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (implemented by [2-networking-nva](../2-networking-c-nva/)) - Pros: additional security features (e.g. IPS), potentially better integration with on-prem systems by using the same vendor - Cons: complex HA/failover setup, limited by VM bandwidth and scale, additional costs for VMs and licenses, out of band management of a critical cloud component @@ -126,58 +153,7 @@ From cloud, the `example.com` domain (used as a placeholder) is forwarded to on- This configuration is battle-tested, and flexible enough to lend itself to simple modifications without subverting its design, for example by forwarding and peering root zones to bypass Cloud DNS external resolution. -## How to run this stage - -This stage is meant to be executed after the [resman](../01-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../00-bootstrap) stage. - -It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. - -Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. - -### Providers configuration - -The default way of making sure you have the right permissions, is to use the identity of the service account pre-created for this stage during the [resource management](../01-resman) stage, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). - -To simplify setup, the previous stage pre-configures a valid providers file in its output, and optionally writes it to a local file if the `outputs_location` variable is set to a valid path. - -If you have set a valid value for `outputs_location` in the bootstrap stage, simply link the relevant `providers.tf` file from this stage's folder in the path you specified: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/providers/02-networking-providers.tf . -``` - -If you have not configured `outputs_location` in bootstrap, you can derive the providers file from that stage's outputs: - -```bash -cd ../01-resman -terraform output -json providers | jq -r '.["02-networking"]' \ - > ../02-networking/providers.tf -``` - -### Variable configuration - -There are two broad sets of variables you will need to fill in: - -- variables shared by other stages (org id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) -- variables specific to resources managed by this stage - -To avoid the tedious job of filling in the first group of variables with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. - -If you have set a valid value for `outputs_location` in the bootstrap and in the resman stage, simply link the relevant `*.auto.tfvars.json` files from this stage's folder in the path you specified. -The `*` above is set to the name of the stage that produced it, except for `globals.auto.tfvars.json` which is also generated by the bootstrap stage, containing global values compiled manually for the bootstrap stage. -For this stage, link the following files: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/tfvars/globals.auto.tfvars.json . -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . -``` - -A second set of variables is specific to this stage, they are all optional so if you need to customize them, create an extra `terraform.tfvars` file. - -Please refer to the [Variables](#variables) table below for a map of the variable origins, and to the sections below on how to adapt this stage to your networking configuration. +## Stage structure and files layout ### VPCs @@ -238,7 +214,72 @@ DNS queries sent to the on-premises infrastructure come from the `35.199.192.0/1 The [Inbound DNS Policy](https://cloud.google.com/dns/docs/server-policies-overview#dns-server-policy-in) defined in module `landing-vpc` ([`landing.tf`](./landing.tf)) automatically reserves the first available IP address on each created subnet (typically the third one in a CIDR) to expose the Cloud DNS service so that it can be consumed from outside of GCP. -### Private Google Access +## How to run this stage + +This stage is meant to be executed after the [resource management](../1-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../0-bootstrap) stage. + +It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. + +Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. + +### Provider and Terraform variables + +As all other FAST stages, the [mechanism used to pass variable values and pre-built provider files from one stage to the next](../0-bootstrap/README.md#output-files-and-cross-stage-variables) is also leveraged here. + +The commands to link or copy the provider and terraform variable files can be easily derived from the `stage-links.sh` script in the FAST root folder, passing it a single argument with the local output files folder (if configured) or the GCS output bucket in the automation project (derived from stage 0 outputs). The following examples demonstrate both cases, and the resulting commands that then need to be copy/pasted and run. + +```bash +../../stage-links.sh ~/fast-config + +# copy and paste the following commands for '2-networking-a-peering' + +ln -s ~/fast-config/providers/2-networking-providers.tf ./ +ln -s ~/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json ./ +``` + +```bash +../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '2-networking-a-peering' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/2-networking-providers.tf ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/globals.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/1-resman.auto.tfvars.json ./ +``` + +### Impersonating the automation service account + +The preconfigured provider file uses impersonation to run with this stage's automation service account's credentials. The `gcp-devops` and `organization-admins` groups have the necessary IAM bindings in place to do that, so make sure the current user is a member of one of those groups. + +### Variable configuration + +Variables in this stage -- like most other FAST stages -- are broadly divided into three separate sets: + +- variables which refer to global values for the whole organization (org id, billing account id, prefix, etc.), which are pre-populated via the `globals.auto.tfvars.json` file linked or copied above +- variables which refer to resources managed by previous stage, which are prepopulated here via the `0-bootstrap.auto.tfvars.json` and `1-resman.auto.tfvars.json` files linked or copied above +- and finally variables that optionally control this stage's behaviour and customizations, and can to be set in a custom `terraform.tfvars` file + +The latter set is explained in the [Customization](#customizations) sections below, and the full list can be found in the [Variables](#variables) table at the bottom of this document. + +### Running the stage + +Once provider and variable values are in place and the correct user is configured, the stage can be run: + +```bash +terraform init +terraform apply +``` + +### Post-deployment activities + +- On-prem routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recomment aggregating routes as much as possible. +- On-prem routers should accept BGP sessions from their cloud peers. +- On-prem DNS servers should have forward zones for GCP-managed ones. + +#### Private Google Access [Private Google Access](https://cloud.google.com/vpc/docs/private-google-access) (or PGA) enables VMs and on-prem systems to consume Google APIs from within the Google network, and is already fully configured on this environment. @@ -250,24 +291,17 @@ Subnets created by the `net-vpc` module are PGA-enabled by default. - 199.36.153.4/30 (`restricted.googleapis.com`) and 199.36.153.8/30 (`private.googleapis.com`) should be routed from on-prem to VPC, and from there to the `default-internet-gateway`. \ Per variable `vpn_onprem_configs` such ranges are advertised to onprem - furthermore every VPC (e.g. see `landing-vpc` in [`landing.tf`](./landing.tf)) has explicit routes set in case the `0.0.0.0/0` route is changed. -- A private DNS zone for `googleapis.com` should be created and configured per [this article](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid#config-domain), as implemented in module `googleapis-private-zone` in [dns-landing.tf](./dns-landing.tf) - -### Preliminary activities - -Before running `terraform apply` on this stage, make sure to adapt all of `variables.tf` and `vpn-variables.tf` to your needs, to update all references to regions (e.g. `europe-west1` or `ew1`) in the whole directory to match your preferences. - -If you're not using FAST, you'll also need to create a `providers.tf` file to configure the GCS backend and the service account to use to run the deployment. - -You're now ready to run `terraform init` and `apply`. - -### Post-deployment activities - -- On-prem routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recomment aggregating routes as much as possible. -- On-prem routers should accept BGP sessions from their cloud peers. -- On-prem DNS servers should have forward zones for GCP-managed ones. +- A private DNS zone for `googleapis.com` should be created and configured per [this article](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid#config-domain), as implemented in module `googleapis-private-zone` in [`dns-landing.tf`](./dns-landing.tf) ## Customizations +### Changing default regions + +Regions are defined via the `regions` variable which sets up a mapping between the `regions.primary` and `regions.secondary` logical names and actual GCP region names. If you need to change regions from the defaults: + +- change the values of the mappings in the `regions` variable to the regions you are going to use +- change the regions in the factory subnet files in the `data` folder + ### Adding an environment To create a new environment (e.g. `staging`), a few changes are required. @@ -306,6 +340,7 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS | [main.tf](./main.tf) | Networking folder and hierarchical policy. | folder | | | [monitoring.tf](./monitoring.tf) | Network monitoring dashboards. | | google_monitoring_dashboard | | [outputs.tf](./outputs.tf) | Module outputs. | | google_storage_bucket_object · local_file | +| [regions.tf](./regions.tf) | Compute short names for regions. | | | | [spoke-dev.tf](./spoke-dev.tf) | Dev spoke VPC and related resources. | net-cloudnat · net-vpc · net-vpc-firewall · project | google_project_iam_binding | | [spoke-prod.tf](./spoke-prod.tf) | Production spoke VPC and related resources. | net-cloudnat · net-vpc · net-vpc-firewall · project | google_project_iam_binding | | [test-resources.tf](./test-resources.tf) | temporary instances for testing | compute-vm | | @@ -313,31 +348,31 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS | [variables.tf](./variables.tf) | Module variables. | | | | [vpn-onprem.tf](./vpn-onprem.tf) | VPN between landing and onprem. | net-vpn-ha | | | [vpn-spoke-dev.tf](./vpn-spoke-dev.tf) | VPN between landing and development spoke. | net-vpn-ha | | -| [vpn-spoke-prod-ew1.tf](./vpn-spoke-prod-ew1.tf) | VPN between landing and production spoke in ew1. | net-vpn-ha | | -| [vpn-spoke-prod-ew4.tf](./vpn-spoke-prod-ew4.tf) | VPN between landing and production spoke in ew4. | net-vpn-ha | | +| [vpn-spoke-prod-primary.tf](./vpn-spoke-prod-primary.tf) | VPN between landing and production spoke in ew1. | net-vpn-ha | | +| [vpn-spoke-prod-secondary.tf](./vpn-spoke-prod-secondary.tf) | VPN between landing and production spoke in ew4. | net-vpn-ha | | ## Variables | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | -| [billing_account](variables.tf#L25) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [folder_ids](variables.tf#L74) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 01-resman | -| [organization](variables.tf#L102) | Organization details. | object({…}) | ✓ | | 00-bootstrap | -| [prefix](variables.tf#L118) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | -| [custom_adv](variables.tf#L34) | Custom advertisement definitions in name => range format. | map(string) | | {…} | | -| [custom_roles](variables.tf#L51) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 00-bootstrap | -| [data_dir](variables.tf#L60) | Relative path for the folder storing configuration data for network resources. | string | | "data" | | -| [dns](variables.tf#L66) | Onprem DNS resolvers. | map(list(string)) | | {…} | | -| [l7ilb_subnets](variables.tf#L84) | Subnets used for L7 ILBs. | map(list(object({…}))) | | {…} | | -| [outputs_location](variables.tf#L112) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | -| [psa_ranges](variables.tf#L129) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | -| [region_trigram](variables.tf#L166) | Short names for GCP regions. | map(string) | | {…} | | -| [router_onprem_configs](variables.tf#L175) | Configurations for routers used for onprem connectivity. | map(object({…})) | | {…} | | -| [router_spoke_configs](variables-vpn.tf#L18) | Configurations for routers used for internal connectivity. | map(object({…})) | | {…} | | -| [service_accounts](variables.tf#L193) | Automation service accounts in name => email format. | object({…}) | | null | 01-resman | -| [vpn_onprem_configs](variables.tf#L207) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | -| [vpn_spoke_configs](variables-vpn.tf#L37) | VPN gateway configuration for spokes. | map(object({…})) | | {…} | | +| [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#L92) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman | +| [organization](variables.tf#L126) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L142) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [custom_adv](variables.tf#L38) | Custom advertisement definitions in name => range format. | map(string) | | {…} | | +| [custom_roles](variables.tf#L55) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | +| [dns](variables.tf#L64) | Onprem DNS resolvers. | map(list(string)) | | {…} | | +| [factories_config](variables.tf#L72) | Configuration for network resource factories. | object({…}) | | {…} | | +| [l7ilb_subnets](variables.tf#L102) | Subnets used for L7 ILBs. | object({…}) | | {…} | | +| [outputs_location](variables.tf#L136) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | +| [psa_ranges](variables.tf#L153) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | +| [regions](variables.tf#L190) | Region definitions. | object({…}) | | {…} | | +| [router_onprem_configs](variables.tf#L202) | Configurations for routers used for onprem connectivity. | map(object({…})) | | {…} | | +| [router_spoke_configs](variables-vpn.tf#L18) | Configurations for routers used for internal connectivity. | map(object({…})) | | {…} | | +| [service_accounts](variables.tf#L220) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | +| [vpn_onprem_configs](variables.tf#L234) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | +| [vpn_spoke_configs](variables-vpn.tf#L37) | VPN gateway configuration for spokes. | map(object({…})) | | {…} | | ## Outputs diff --git a/fast/stages/02-networking-peering/data/cidrs.yaml b/fast/stages/2-networking-b-vpn/data/cidrs.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/cidrs.yaml rename to fast/stages/2-networking-b-vpn/data/cidrs.yaml diff --git a/fast/stages/02-networking-separate-envs/data/dashboards/firewall_insights.json b/fast/stages/2-networking-b-vpn/data/dashboards/firewall_insights.json similarity index 100% rename from fast/stages/02-networking-separate-envs/data/dashboards/firewall_insights.json rename to fast/stages/2-networking-b-vpn/data/dashboards/firewall_insights.json diff --git a/fast/stages/02-networking-separate-envs/data/dashboards/vpn.json b/fast/stages/2-networking-b-vpn/data/dashboards/vpn.json similarity index 100% rename from fast/stages/02-networking-separate-envs/data/dashboards/vpn.json rename to fast/stages/2-networking-b-vpn/data/dashboards/vpn.json diff --git a/fast/stages/02-networking-peering/data/firewall-rules/dev/rules.yaml b/fast/stages/2-networking-b-vpn/data/firewall-rules/dev/rules.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/firewall-rules/dev/rules.yaml rename to fast/stages/2-networking-b-vpn/data/firewall-rules/dev/rules.yaml diff --git a/fast/stages/02-networking-vpn/data/firewall-rules/landing/rules.yaml b/fast/stages/2-networking-b-vpn/data/firewall-rules/landing/rules.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/firewall-rules/landing/rules.yaml rename to fast/stages/2-networking-b-vpn/data/firewall-rules/landing/rules.yaml diff --git a/fast/stages/02-networking-peering/data/hierarchical-policy-rules.yaml b/fast/stages/2-networking-b-vpn/data/hierarchical-policy-rules.yaml similarity index 100% rename from fast/stages/02-networking-peering/data/hierarchical-policy-rules.yaml rename to fast/stages/2-networking-b-vpn/data/hierarchical-policy-rules.yaml diff --git a/fast/stages/02-networking-separate-envs/data/subnets/dev/dev-dataplatform-ew1.yaml b/fast/stages/2-networking-b-vpn/data/subnets/dev/dev-dataplatform-ew1.yaml similarity index 100% rename from fast/stages/02-networking-separate-envs/data/subnets/dev/dev-dataplatform-ew1.yaml rename to fast/stages/2-networking-b-vpn/data/subnets/dev/dev-dataplatform-ew1.yaml diff --git a/fast/stages/02-networking-separate-envs/data/subnets/dev/dev-default-ew1.yaml b/fast/stages/2-networking-b-vpn/data/subnets/dev/dev-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-separate-envs/data/subnets/dev/dev-default-ew1.yaml rename to fast/stages/2-networking-b-vpn/data/subnets/dev/dev-default-ew1.yaml diff --git a/fast/stages/02-networking-vpn/data/subnets/dev/dev-gke-nodes-ew1.yaml b/fast/stages/2-networking-b-vpn/data/subnets/dev/dev-gke-nodes-ew1.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/subnets/dev/dev-gke-nodes-ew1.yaml rename to fast/stages/2-networking-b-vpn/data/subnets/dev/dev-gke-nodes-ew1.yaml diff --git a/fast/stages/02-networking-vpn/data/subnets/landing/landing-default-ew1.yaml b/fast/stages/2-networking-b-vpn/data/subnets/landing/landing-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/subnets/landing/landing-default-ew1.yaml rename to fast/stages/2-networking-b-vpn/data/subnets/landing/landing-default-ew1.yaml diff --git a/fast/stages/02-networking-separate-envs/data/subnets/prod/prod-default-ew1.yaml b/fast/stages/2-networking-b-vpn/data/subnets/prod/prod-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-separate-envs/data/subnets/prod/prod-default-ew1.yaml rename to fast/stages/2-networking-b-vpn/data/subnets/prod/prod-default-ew1.yaml diff --git a/fast/stages/02-networking-vpn/diagram.png b/fast/stages/2-networking-b-vpn/diagram.png similarity index 100% rename from fast/stages/02-networking-vpn/diagram.png rename to fast/stages/2-networking-b-vpn/diagram.png diff --git a/fast/stages/02-networking-vpn/diagram.svg b/fast/stages/2-networking-b-vpn/diagram.svg similarity index 100% rename from fast/stages/02-networking-vpn/diagram.svg rename to fast/stages/2-networking-b-vpn/diagram.svg diff --git a/fast/stages/02-networking-vpn/dns-dev.tf b/fast/stages/2-networking-b-vpn/dns-dev.tf similarity index 100% rename from fast/stages/02-networking-vpn/dns-dev.tf rename to fast/stages/2-networking-b-vpn/dns-dev.tf diff --git a/fast/stages/02-networking-vpn/dns-landing.tf b/fast/stages/2-networking-b-vpn/dns-landing.tf similarity index 100% rename from fast/stages/02-networking-vpn/dns-landing.tf rename to fast/stages/2-networking-b-vpn/dns-landing.tf diff --git a/fast/stages/02-networking-vpn/dns-prod.tf b/fast/stages/2-networking-b-vpn/dns-prod.tf similarity index 100% rename from fast/stages/02-networking-vpn/dns-prod.tf rename to fast/stages/2-networking-b-vpn/dns-prod.tf diff --git a/fast/stages/02-networking-vpn/landing.tf b/fast/stages/2-networking-b-vpn/landing.tf similarity index 82% rename from fast/stages/02-networking-vpn/landing.tf rename to fast/stages/2-networking-b-vpn/landing.tf index 83a0d509a..37e3adfd4 100644 --- a/fast/stages/02-networking-vpn/landing.tf +++ b/fast/stages/2-networking-b-vpn/landing.tf @@ -63,7 +63,7 @@ module "landing-vpc" { next_hop = "default-internet-gateway" } } - data_folder = "${var.data_dir}/subnets/landing" + data_folder = "${var.factories_config.data_dir}/subnets/landing" } module "landing-firewall" { @@ -74,18 +74,23 @@ module "landing-firewall" { disabled = true } factories_config = { - cidr_tpl_file = "${var.data_dir}/cidrs.yaml" - rules_folder = "${var.data_dir}/firewall-rules/landing" + cidr_tpl_file = "${var.factories_config.data_dir}/cidrs.yaml" + rules_folder = "${var.factories_config.data_dir}/firewall-rules/landing" } } -module "landing-nat-ew1" { +moved { + from = module.landing-nat-ew1 + to = module.landing-nat-primary +} + +module "landing-nat-primary" { source = "../../../modules/net-cloudnat" project_id = module.landing-project.project_id - region = "europe-west1" - name = "ew1" + region = var.regions.primary + name = local.region_shortnames[var.regions.primary] router_create = true - router_name = "prod-nat-ew1" + router_name = "prod-nat-${local.region_shortnames[var.regions.primary]}" router_network = module.landing-vpc.name router_asn = 4200001024 } diff --git a/fast/stages/02-networking-peering/main.tf b/fast/stages/2-networking-b-vpn/main.tf similarity index 76% rename from fast/stages/02-networking-peering/main.tf rename to fast/stages/2-networking-b-vpn/main.tf index f68d39eb8..5888752d7 100644 --- a/fast/stages/02-networking-peering/main.tf +++ b/fast/stages/2-networking-b-vpn/main.tf @@ -17,14 +17,14 @@ # tfdoc:file:description Networking folder and hierarchical policy. locals { + # combine all regions from variables and subnets + regions = distinct(concat( + values(var.regions), + values(module.dev-spoke-vpc.subnet_regions), + values(module.landing-vpc.subnet_regions), + values(module.prod-spoke-vpc.subnet_regions), + )) custom_roles = coalesce(var.custom_roles, {}) - l7ilb_subnets = { - for env, v in var.l7ilb_subnets : env => [ - for s in v : merge(s, { - active = true - name = "${env}-l7ilb-${s.region}" - })] - } stage3_sas_delegated_grants = [ "roles/composer.sharedVpcAgent", "roles/compute.networkUser", @@ -46,9 +46,9 @@ module "folder" { folder_create = var.folder_ids.networking == null id = var.folder_ids.networking firewall_policy_factory = { - cidr_file = "${var.data_dir}/cidrs.yaml" - policy_name = null - rules_file = "${var.data_dir}/hierarchical-policy-rules.yaml" + cidr_file = "${var.factories_config.data_dir}/cidrs.yaml" + policy_name = var.factories_config.firewall_policy_name + rules_file = "${var.factories_config.data_dir}/hierarchical-policy-rules.yaml" } firewall_policy_association = { factory-policy = "factory" diff --git a/fast/stages/02-networking-nva/monitoring.tf b/fast/stages/2-networking-b-vpn/monitoring.tf similarity index 93% rename from fast/stages/02-networking-nva/monitoring.tf rename to fast/stages/2-networking-b-vpn/monitoring.tf index 7b8b70c51..be3a47faa 100644 --- a/fast/stages/02-networking-nva/monitoring.tf +++ b/fast/stages/2-networking-b-vpn/monitoring.tf @@ -17,7 +17,7 @@ # tfdoc:file:description Network monitoring dashboards. locals { - dashboard_path = "${var.data_dir}/dashboards" + dashboard_path = "${var.factories_config.data_dir}/dashboards" dashboard_files = fileset(local.dashboard_path, "*.json") dashboards = { for filename in local.dashboard_files : diff --git a/fast/stages/02-networking-vpn/outputs.tf b/fast/stages/2-networking-b-vpn/outputs.tf similarity index 93% rename from fast/stages/02-networking-vpn/outputs.tf rename to fast/stages/2-networking-b-vpn/outputs.tf index 3b97b7f25..628c706b3 100644 --- a/fast/stages/02-networking-vpn/outputs.tf +++ b/fast/stages/2-networking-b-vpn/outputs.tf @@ -48,13 +48,13 @@ locals { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/02-networking.auto.tfvars.json" + filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket - name = "tfvars/02-networking.auto.tfvars.json" + name = "tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } @@ -89,8 +89,8 @@ output "tfvars" { output "vpn_gateway_endpoints" { description = "External IP Addresses for the GCP VPN gateways." value = local.enable_onprem_vpn == false ? null : { - onprem-ew1 = { - for v in module.landing-to-onprem-ew1-vpn[0].gateway.vpn_interfaces : + onprem-primary = { + for v in module.landing-to-onprem-primary-vpn[0].gateway.vpn_interfaces : v.id => v.ip_address } } diff --git a/fast/stages/2-networking-b-vpn/regions.tf b/fast/stages/2-networking-b-vpn/regions.tf new file mode 100644 index 000000000..53514afa9 --- /dev/null +++ b/fast/stages/2-networking-b-vpn/regions.tf @@ -0,0 +1,42 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Compute short names for regions. + +locals { + # only map when the first character would not work + _region_cardinal = { + southeast = "se" + } + # only map when the first character would not work + _region_geo = { + australia = "o" + } + # split in [geo, cardinal, number] tokens + _region_tokens = { + for v in local.regions : v => regexall("(?:[a-z]+)|(?:[0-9]+)", v) + } + region_shortnames = { + for k, v in local._region_tokens : k => join("", [ + # first token via geo alias map or first character + lookup(local._region_geo, v.0, substr(v.0, 0, 1)), + # first token via cardinal alias map or first character + lookup(local._region_cardinal, v.1, substr(v.1, 0, 1)), + # region number as is + v.2 + ]) + } +} diff --git a/fast/stages/02-networking-vpn/spoke-dev.tf b/fast/stages/2-networking-b-vpn/spoke-dev.tf similarity index 84% rename from fast/stages/02-networking-vpn/spoke-dev.tf rename to fast/stages/2-networking-b-vpn/spoke-dev.tf index e67cfb70d..ed7d13cb0 100644 --- a/fast/stages/02-networking-vpn/spoke-dev.tf +++ b/fast/stages/2-networking-b-vpn/spoke-dev.tf @@ -16,6 +16,19 @@ # tfdoc:file:description Dev spoke VPC and related resources. +locals { + _l7ilb_subnets_dev = [ + for v in var.l7ilb_subnets.dev : merge(v, { + active = true + region = lookup(var.regions, v.region, v.region) + })] + l7ilb_subnets_dev = [ + for v in local._l7ilb_subnets_dev : merge(v, { + name = "dev-l7ilb-${local.region_shortnames[v.region]}" + }) + ] +} + module "dev-spoke-project" { source = "../../../modules/project" billing_account = var.billing_account.id @@ -48,9 +61,9 @@ module "dev-spoke-vpc" { project_id = module.dev-spoke-project.project_id name = "dev-spoke-0" mtu = 1500 - data_folder = "${var.data_dir}/subnets/dev" + data_folder = "${var.factories_config.data_dir}/subnets/dev" psa_config = try(var.psa_ranges.dev, null) - subnets_proxy_only = local.l7ilb_subnets.dev + subnets_proxy_only = local.l7ilb_subnets_dev # set explicit routes for googleapis in case the default route is deleted routes = { private-googleapis = { @@ -74,8 +87,8 @@ module "dev-spoke-firewall" { disabled = true } factories_config = { - cidr_tpl_file = "${var.data_dir}/cidrs.yaml" - rules_folder = "${var.data_dir}/firewall-rules/dev" + cidr_tpl_file = "${var.factories_config.data_dir}/cidrs.yaml" + rules_folder = "${var.factories_config.data_dir}/firewall-rules/dev" } } @@ -84,7 +97,7 @@ module "dev-spoke-cloudnat" { source = "../../../modules/net-cloudnat" project_id = module.dev-spoke-project.project_id region = each.value - name = "dev-nat-${var.region_trigram[each.value]}" + name = "dev-nat-${local.region_shortnames[each.value]}" router_create = true router_network = module.dev-spoke-vpc.name router_asn = 4200001024 diff --git a/fast/stages/02-networking-peering/spoke-prod.tf b/fast/stages/2-networking-b-vpn/spoke-prod.tf similarity index 84% rename from fast/stages/02-networking-peering/spoke-prod.tf rename to fast/stages/2-networking-b-vpn/spoke-prod.tf index cf49152fa..f584b32da 100644 --- a/fast/stages/02-networking-peering/spoke-prod.tf +++ b/fast/stages/2-networking-b-vpn/spoke-prod.tf @@ -16,6 +16,19 @@ # tfdoc:file:description Production spoke VPC and related resources. +locals { + _l7ilb_subnets_prod = [ + for v in var.l7ilb_subnets.prod : merge(v, { + active = true + region = lookup(var.regions, v.region, v.region) + })] + l7ilb_subnets_prod = [ + for v in local._l7ilb_subnets_prod : merge(v, { + name = "prod-l7ilb-${local.region_shortnames[v.region]}" + }) + ] +} + module "prod-spoke-project" { source = "../../../modules/project" billing_account = var.billing_account.id @@ -48,9 +61,9 @@ module "prod-spoke-vpc" { project_id = module.prod-spoke-project.project_id name = "prod-spoke-0" mtu = 1500 - data_folder = "${var.data_dir}/subnets/prod" + data_folder = "${var.factories_config.data_dir}/subnets/prod" psa_config = try(var.psa_ranges.prod, null) - subnets_proxy_only = local.l7ilb_subnets.prod + subnets_proxy_only = local.l7ilb_subnets_prod # set explicit routes for googleapis in case the default route is deleted routes = { private-googleapis = { @@ -74,8 +87,8 @@ module "prod-spoke-firewall" { disabled = true } factories_config = { - cidr_tpl_file = "${var.data_dir}/cidrs.yaml" - rules_folder = "${var.data_dir}/firewall-rules/prod" + cidr_tpl_file = "${var.factories_config.data_dir}/cidrs.yaml" + rules_folder = "${var.factories_config.data_dir}/firewall-rules/prod" } } @@ -84,7 +97,7 @@ module "prod-spoke-cloudnat" { source = "../../../modules/net-cloudnat" project_id = module.prod-spoke-project.project_id region = each.value - name = "prod-nat-${var.region_trigram[each.value]}" + name = "prod-nat-${local.region_shortnames[each.value]}" router_create = true router_network = module.prod-spoke-vpc.name router_asn = 4200001024 diff --git a/fast/stages/02-networking-vpn/test-resources.tf b/fast/stages/2-networking-b-vpn/test-resources.tf similarity index 76% rename from fast/stages/02-networking-vpn/test-resources.tf rename to fast/stages/2-networking-b-vpn/test-resources.tf index 204971fec..67073845d 100644 --- a/fast/stages/02-networking-vpn/test-resources.tf +++ b/fast/stages/2-networking-b-vpn/test-resources.tf @@ -19,11 +19,11 @@ # module "test-vm-landing-0" { # source = "../../../modules/compute-vm" # project_id = module.landing-project.project_id -# zone = "europe-west1-b" +# zone = "${var.regions.primary}-b" # name = "test-vm-0" # network_interfaces = [{ # network = module.landing-vpc.self_link -# subnetwork = module.landing-vpc.subnet_self_links["europe-west1/landing-default-ew1"] +# subnetwork = module.landing-vpc.subnet_self_links["${var.regions.primary}/landing-default-${local.region_shortnames[var.regions.primary]}"] # }] # tags = ["ssh"] # service_account_create = true @@ -31,8 +31,8 @@ # image = "projects/debian-cloud/global/images/family/debian-10" # } # options = { -# spot = true -# termination_action = "STOP" +# spot = true +# termination_action = "STOP" # } # metadata = { # startup-script = < { bgp_peer = { address = cidrhost(t.session_range, 1) asn = t.peer_asn } - bgp_peer_options = local.bgp_peer_options_onprem.landing-ew1 + bgp_peer_options = local.bgp_peer_options_onprem.landing-primary bgp_session_range = "${cidrhost(t.session_range, 2)}/30" peer_external_gateway_interface = t.peer_external_gateway_interface shared_secret = t.secret diff --git a/fast/stages/02-networking-vpn/vpn-spoke-dev.tf b/fast/stages/2-networking-b-vpn/vpn-spoke-dev.tf similarity index 62% rename from fast/stages/02-networking-vpn/vpn-spoke-dev.tf rename to fast/stages/2-networking-b-vpn/vpn-spoke-dev.tf index 317560af0..09cc31e03 100644 --- a/fast/stages/02-networking-vpn/vpn-spoke-dev.tf +++ b/fast/stages/2-networking-b-vpn/vpn-spoke-dev.tf @@ -33,26 +33,31 @@ locals { # development spoke -module "landing-to-dev-ew1-vpn" { +moved { + from = module.landing-to-dev-ew1-vpn + to = module.landing-to-dev-primary-vpn +} + +module "landing-to-dev-primary-vpn" { source = "../../../modules/net-vpn-ha" project_id = module.landing-project.project_id network = module.landing-vpc.self_link - region = "europe-west1" - name = "vpn-to-dev-ew1" + region = var.regions.primary + name = "vpn-to-dev-${local.region_shortnames[var.regions.primary]}" router_config = { # The router used for this VPN is managed in vpn-prod.tf create = false - name = "landing-vpn-ew1" - asn = var.router_spoke_configs.landing-ew1.asn + name = "landing-vpn-${local.region_shortnames[var.regions.primary]}" + asn = var.router_spoke_configs.landing-primary.asn } - peer_gateway = { gcp = module.dev-to-landing-ew1-vpn.self_link } + peer_gateway = { gcp = module.dev-to-landing-primary-vpn.self_link } tunnels = { 0 = { bgp_peer = { address = cidrhost("169.254.0.0/27", 1) - asn = var.router_spoke_configs.spoke-dev-ew1.asn + asn = var.router_spoke_configs.spoke-dev-primary.asn } - bgp_peer_options = local.vpn_spoke_bgp_peer_options.landing-ew1 + bgp_peer_options = local.vpn_spoke_bgp_peer_options.landing-primary bgp_session_range = "${ cidrhost("169.254.0.0/27", 2) }/30" @@ -61,9 +66,9 @@ module "landing-to-dev-ew1-vpn" { 1 = { bgp_peer = { address = cidrhost("169.254.0.0/27", 5) - asn = var.router_spoke_configs.spoke-dev-ew1.asn + asn = var.router_spoke_configs.spoke-dev-primary.asn } - bgp_peer_options = local.vpn_spoke_bgp_peer_options.landing-ew1 + bgp_peer_options = local.vpn_spoke_bgp_peer_options.landing-primary bgp_session_range = "${ cidrhost("169.254.0.0/27", 6) }/30" @@ -71,44 +76,49 @@ module "landing-to-dev-ew1-vpn" { } } depends_on = [ - module.landing-to-prod-ew1-vpn.router + module.landing-to-prod-primary-vpn.router ] } -module "dev-to-landing-ew1-vpn" { +moved { + from = module.dev-to-landing-ew1-vpn + to = module.dev-to-landing-primary-vpn +} + +module "dev-to-landing-primary-vpn" { source = "../../../modules/net-vpn-ha" project_id = module.dev-spoke-project.project_id network = module.dev-spoke-vpc.self_link - region = "europe-west1" - name = "vpn-to-landing-ew1" + region = var.regions.primary + name = "vpn-to-landing-${local.region_shortnames[var.regions.primary]}" router_config = { - name = "dev-spoke-vpn-ew1" - asn = var.router_spoke_configs.spoke-dev-ew1.asn + name = "dev-spoke-vpn-${local.region_shortnames[var.regions.primary]}" + asn = var.router_spoke_configs.spoke-dev-primary.asn } - peer_gateway = { gcp = module.landing-to-dev-ew1-vpn.self_link } + peer_gateway = { gcp = module.landing-to-dev-primary-vpn.self_link } tunnels = { 0 = { bgp_peer = { address = cidrhost("169.254.0.0/27", 2) - asn = var.router_spoke_configs.landing-ew1.asn + asn = var.router_spoke_configs.landing-primary.asn } - bgp_peer_options = local.vpn_spoke_bgp_peer_options.dev-ew1 + bgp_peer_options = local.vpn_spoke_bgp_peer_options.dev-primary bgp_session_range = "${ cidrhost("169.254.0.0/27", 1) }/30" - shared_secret = module.landing-to-dev-ew1-vpn.random_secret + shared_secret = module.landing-to-dev-primary-vpn.random_secret vpn_gateway_interface = 0 } 1 = { bgp_peer = { address = cidrhost("169.254.0.0/27", 6) - asn = var.router_spoke_configs.landing-ew1.asn + asn = var.router_spoke_configs.landing-primary.asn } - bgp_peer_options = local.vpn_spoke_bgp_peer_options.dev-ew1 + bgp_peer_options = local.vpn_spoke_bgp_peer_options.dev-primary bgp_session_range = "${ cidrhost("169.254.0.0/27", 5) }/30" - shared_secret = module.landing-to-dev-ew1-vpn.random_secret + shared_secret = module.landing-to-dev-primary-vpn.random_secret vpn_gateway_interface = 1 } } diff --git a/fast/stages/02-networking-vpn/vpn-spoke-prod-ew1.tf b/fast/stages/2-networking-b-vpn/vpn-spoke-prod-primary.tf similarity index 58% rename from fast/stages/02-networking-vpn/vpn-spoke-prod-ew1.tf rename to fast/stages/2-networking-b-vpn/vpn-spoke-prod-primary.tf index a215ad4ef..071b1d054 100644 --- a/fast/stages/02-networking-vpn/vpn-spoke-prod-ew1.tf +++ b/fast/stages/2-networking-b-vpn/vpn-spoke-prod-primary.tf @@ -18,24 +18,29 @@ # local.vpn_spoke_bgp_peer_options is defined in the dev VPN file -module "landing-to-prod-ew1-vpn" { +moved { + from = module.landing-to-prod-ew1-vpn + to = module.landing-to-prod-primary-vpn +} + +module "landing-to-prod-primary-vpn" { source = "../../../modules/net-vpn-ha" project_id = module.landing-project.project_id network = module.landing-vpc.self_link - region = "europe-west1" - name = "vpn-to-prod-ew1" + region = var.regions.primary + name = "vpn-to-prod-${local.region_shortnames[var.regions.primary]}" router_config = { - name = "landing-vpn-ew1" - asn = var.router_spoke_configs.landing-ew1.asn + name = "landing-vpn-${local.region_shortnames[var.regions.primary]}" + asn = var.router_spoke_configs.landing-primary.asn } - peer_gateway = { gcp = module.prod-to-landing-ew1-vpn.self_link } + peer_gateway = { gcp = module.prod-to-landing-primary-vpn.self_link } tunnels = { 0 = { bgp_peer = { address = cidrhost("169.254.0.64/27", 1) - asn = var.router_spoke_configs.spoke-prod-ew1.asn + asn = var.router_spoke_configs.spoke-prod-primary.asn } - bgp_peer_options = local.vpn_spoke_bgp_peer_options.landing-ew1 + bgp_peer_options = local.vpn_spoke_bgp_peer_options.landing-primary bgp_session_range = "${ cidrhost("169.254.0.64/27", 2) }/30" @@ -44,9 +49,9 @@ module "landing-to-prod-ew1-vpn" { 1 = { bgp_peer = { address = cidrhost("169.254.0.64/27", 5) - asn = var.router_spoke_configs.spoke-prod-ew1.asn + asn = var.router_spoke_configs.spoke-prod-primary.asn } - bgp_peer_options = local.vpn_spoke_bgp_peer_options.landing-ew1 + bgp_peer_options = local.vpn_spoke_bgp_peer_options.landing-primary bgp_session_range = "${ cidrhost("169.254.0.64/27", 6) }/30" @@ -55,40 +60,45 @@ module "landing-to-prod-ew1-vpn" { } } -module "prod-to-landing-ew1-vpn" { +moved { + from = module.prod-to-landing-ew1-vpn + to = module.prod-to-landing-primary-vpn +} + +module "prod-to-landing-primary-vpn" { source = "../../../modules/net-vpn-ha" project_id = module.prod-spoke-project.project_id network = module.prod-spoke-vpc.self_link - region = "europe-west1" - name = "vpn-to-landing-ew1" + region = var.regions.primary + name = "vpn-to-landing-${local.region_shortnames[var.regions.primary]}" router_config = { - name = "prod-spoke-vpn-ew1" - asn = var.router_spoke_configs.spoke-prod-ew1.asn + name = "prod-spoke-vpn-${local.region_shortnames[var.regions.primary]}" + asn = var.router_spoke_configs.spoke-prod-primary.asn } - peer_gateway = { gcp = module.landing-to-prod-ew1-vpn.self_link } + peer_gateway = { gcp = module.landing-to-prod-primary-vpn.self_link } tunnels = { 0 = { bgp_peer = { address = cidrhost("169.254.0.64/27", 2) - asn = var.router_spoke_configs.landing-ew1.asn + asn = var.router_spoke_configs.landing-primary.asn } - bgp_peer_options = local.vpn_spoke_bgp_peer_options.prod-ew1 + bgp_peer_options = local.vpn_spoke_bgp_peer_options.prod-primary bgp_session_range = "${ cidrhost("169.254.0.64/27", 1) }/30" - shared_secret = module.landing-to-prod-ew1-vpn.random_secret + shared_secret = module.landing-to-prod-primary-vpn.random_secret vpn_gateway_interface = 0 } 1 = { bgp_peer = { address = cidrhost("169.254.0.64/27", 6) - asn = var.router_spoke_configs.landing-ew1.asn + asn = var.router_spoke_configs.landing-primary.asn } - bgp_peer_options = local.vpn_spoke_bgp_peer_options.prod-ew1 + bgp_peer_options = local.vpn_spoke_bgp_peer_options.prod-primary bgp_session_range = "${ cidrhost("169.254.0.64/27", 5) }/30" - shared_secret = module.landing-to-prod-ew1-vpn.random_secret + shared_secret = module.landing-to-prod-primary-vpn.random_secret vpn_gateway_interface = 1 } } diff --git a/fast/stages/02-networking-vpn/vpn-spoke-prod-ew4.tf b/fast/stages/2-networking-b-vpn/vpn-spoke-prod-secondary.tf similarity index 57% rename from fast/stages/02-networking-vpn/vpn-spoke-prod-ew4.tf rename to fast/stages/2-networking-b-vpn/vpn-spoke-prod-secondary.tf index 994fba0b1..a7c0e0fe1 100644 --- a/fast/stages/02-networking-vpn/vpn-spoke-prod-ew4.tf +++ b/fast/stages/2-networking-b-vpn/vpn-spoke-prod-secondary.tf @@ -18,24 +18,29 @@ # local.vpn_spoke_bgp_peer_options is defined in the dev VPN file -module "landing-to-prod-ew4-vpn" { +moved { + from = module.landing-to-prod-ew4-vpn + to = module.landing-to-prod-secondary-vpn +} + +module "landing-to-prod-secondary-vpn" { source = "../../../modules/net-vpn-ha" project_id = module.landing-project.project_id network = module.landing-vpc.self_link - region = "europe-west4" - name = "vpn-to-prod-ew4" + region = var.regions.secondary + name = "vpn-to-prod-${local.region_shortnames[var.regions.secondary]}" router_config = { - name = "landing-vpn-ew4" - asn = var.router_spoke_configs.landing-ew4.asn + name = "landing-vpn-${local.region_shortnames[var.regions.secondary]}" + asn = var.router_spoke_configs.landing-secondary.asn } - peer_gateway = { gcp = module.prod-to-landing-ew4-vpn.self_link } + peer_gateway = { gcp = module.prod-to-landing-secondary-vpn.self_link } tunnels = { 0 = { bgp_peer = { address = cidrhost("169.254.0.96/27", 1) - asn = var.router_spoke_configs.spoke-prod-ew4.asn + asn = var.router_spoke_configs.spoke-prod-secondary.asn } - bgp_peer_options = local.vpn_spoke_bgp_peer_options.landing-ew4 + bgp_peer_options = local.vpn_spoke_bgp_peer_options.landing-secondary bgp_session_range = "${ cidrhost("169.254.0.96/27", 2) }/30" @@ -44,9 +49,9 @@ module "landing-to-prod-ew4-vpn" { 1 = { bgp_peer = { address = cidrhost("169.254.0.96/27", 5) - asn = var.router_spoke_configs.spoke-prod-ew4.asn + asn = var.router_spoke_configs.spoke-prod-secondary.asn } - bgp_peer_options = local.vpn_spoke_bgp_peer_options.landing-ew4 + bgp_peer_options = local.vpn_spoke_bgp_peer_options.landing-secondary bgp_session_range = "${ cidrhost("169.254.0.96/27", 6) }/30" @@ -55,40 +60,45 @@ module "landing-to-prod-ew4-vpn" { } } -module "prod-to-landing-ew4-vpn" { +moved { + from = module.prod-to-landing-ew4-vpn + to = module.prod-to-landing-secondary-vpn +} + +module "prod-to-landing-secondary-vpn" { source = "../../../modules/net-vpn-ha" project_id = module.prod-spoke-project.project_id network = module.prod-spoke-vpc.self_link - region = "europe-west4" - name = "vpn-to-landing-ew4" + region = var.regions.secondary + name = "vpn-to-landing-${local.region_shortnames[var.regions.secondary]}" router_config = { - name = "prod-spoke-vpn-ew4" - asn = var.router_spoke_configs.spoke-prod-ew4.asn + name = "prod-spoke-vpn-${local.region_shortnames[var.regions.secondary]}" + asn = var.router_spoke_configs.spoke-prod-secondary.asn } - peer_gateway = { gcp = module.landing-to-prod-ew4-vpn.self_link } + peer_gateway = { gcp = module.landing-to-prod-secondary-vpn.self_link } tunnels = { 0 = { bgp_peer = { address = cidrhost("169.254.0.96/27", 2) - asn = var.router_spoke_configs.landing-ew4.asn + asn = var.router_spoke_configs.landing-secondary.asn } - bgp_peer_options = local.vpn_spoke_bgp_peer_options.prod-ew4 + bgp_peer_options = local.vpn_spoke_bgp_peer_options.prod-secondary bgp_session_range = "${ cidrhost("169.254.0.96/27", 1) }/30" - shared_secret = module.landing-to-prod-ew4-vpn.random_secret + shared_secret = module.landing-to-prod-secondary-vpn.random_secret vpn_gateway_interface = 0 } 1 = { bgp_peer = { address = cidrhost("169.254.0.96/27", 6) - asn = var.router_spoke_configs.landing-ew4.asn + asn = var.router_spoke_configs.landing-secondary.asn } - bgp_peer_options = local.vpn_spoke_bgp_peer_options.prod-ew4 + bgp_peer_options = local.vpn_spoke_bgp_peer_options.prod-secondary bgp_session_range = "${ cidrhost("169.254.0.96/27", 5) }/30" - shared_secret = module.landing-to-prod-ew4-vpn.random_secret + shared_secret = module.landing-to-prod-secondary-vpn.random_secret vpn_gateway_interface = 1 } } diff --git a/fast/stages/02-networking-nva/README.md b/fast/stages/2-networking-c-nva/README.md similarity index 67% rename from fast/stages/02-networking-nva/README.md rename to fast/stages/2-networking-c-nva/README.md index 79c27e8de..d0e62fd62 100644 --- a/fast/stages/02-networking-nva/README.md +++ b/fast/stages/2-networking-c-nva/README.md @@ -21,6 +21,34 @@ The final number of subnets, and their IP addressing will depend on the user-spe Networking diagram

+## Table of contents + +- [Design overview and choices](#design-overview-and-choices) + - [Multi-regional deployment](#multi-regional-deployment) + - [VPC design](#vpc-design) + - [External connectivity](#external-connectivity) + - [Internal connectivity](#internal-connectivity) + - [IP ranges, subnetting, routing](#ip-ranges-subnetting-routing) + - [Internet egress](#internet-egress) + - [VPC and Hierarchical Firewall](#vpc-and-hierarchical-firewall) + - [DNS](#dns) +- [Stage structure and files layout](#stage-structure-and-files-layout) + - [VPCs](#vpcs) + - [VPNs](#vpns) + - [Routing and BGP](#routing-and-bgp) + - [Firewall](#firewall) + - [DNS architecture](#dns-architecture) + - [Private Google Access](#private-google-access) +- [How to run this stage](#how-to-run-this-stage) + - [Provider and Terraform variables](#provider-and-terraform-variables) + - [Impersonating the automation service account](#impersonating-the-automation-service-account) + - [Variable configuration](#variable-configuration) + - [Running the stage](#running-the-stage) + - [Post-deployment activities](#post-deployment-activities) +- [Customizations](#customizations) + - [Changing default regions](#changing-default-regions) + - [Adding an environment](#adding-an-environment) + ## Design overview and choices ### Multi-regional deployment @@ -69,7 +97,7 @@ Internal connectivity (e.g. between the trusted landing VPC and the spokes) is r This is an options summary: -- [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering) (used here to connect the trusted landing VPC with the spokes, also used by [02-networking-vpn](../02-networking-vpn/)) +- [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering) (used here to connect the trusted landing VPC with the spokes, also used by [02-networking-vpn](../2-networking-b-vpn/)) - Pros: no additional costs, full bandwidth with no configurations, no extra latency - Cons: no transitivity (e.g. to GKE masters, Cloud SQL, etc.), no selective exchange of routes, several quotas and limits shared between VPCs in a peering group - [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (used here to connect the trusted landing and untrusted VPCs) @@ -190,58 +218,7 @@ In GCP, a forwarding zone in the landing project is configured to forward querie This configuration is battle-tested, and flexible enough to lend itself to simple modifications without subverting its design. -## How to run this stage - -This stage is meant to be executed after the [resman](../01-resman) stage has run. It leverages the automation service account and the storage bucket created there, and additional resources configured in the [bootstrap](../00-bootstrap) stage. - -It's possible to run this stage in isolation, but that's outside of the scope of this document. Please, refer to the previous stages for the environment requirements. - -Before running this stage, you need to make sure you have the correct credentials and permissions. You'll also need identify the module variables and make sure you assign them the values that match your configuration. - -### Providers configuration - -The default way of making sure you have the right permissions, is to use the identity of the service account pre-created for this stage, during the [resource management](../01-resman) stage, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). - -To simplify the setup, the previous stage pre-configures a valid providers file in its output and optionally writes it to a local file if the `outputs_location` variable is set to a valid path. - -If you have set a valid value for `outputs_location` in the bootstrap stage, simply link the relevant `providers.tf` file from this stage folder in the path you selected: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/providers/02-networking-providers.tf . -``` - -If you have not configured `outputs_location` in bootstrap, you can derive the providers file from that stage outputs: - -```bash -cd ../01-resman -terraform output -json providers | jq -r '.["02-networking"]' \ - > ../02-networking-nva/providers.tf -``` - -### Variable configuration - -There are two broad sets of variables you will need to fill in: - -- variables shared by other stages (org id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) -- variables specific to resources managed by this stage - -To avoid the tedious job of filling in the first group of variables with values derived from other stages outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. - -If you have set a valid value for `outputs_location` in the bootstrap and in the resman stage, simply link the relevant `*.auto.tfvars.json` files from this stage's folder in the path you specified. -The `*` above is set to the name of the stage that produced it, except for `globals.auto.tfvars.json` which is also generated by the bootstrap stage, containing global values compiled manually for the bootstrap stage. -For this stage, link the following files: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/tfvars/globals.auto.tfvars.json . -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . -``` - -A second set of variables is specific to this stage, they are all optional so if you need to customize them, create an extra `terraform.tfvars` file. - -Please, refer to the [variables](#variables) table below for a map of the variable origins, and use the sections below to understand how to adapt this stage to your networking configuration. +## Stage structure and files layout ### VPCs @@ -286,46 +263,104 @@ Cloud DNS manages onprem forwarding, the main GCP zone (in this example `gcp.exa The root DNS zone defined in the landing project acts as the source of truth for DNS within the Cloud environment. The resources defined in the spoke VPCs consume the landing DNS infrastructure through DNS peering (e.g. `prod-landing-root-dns-peering`). The spokes can optionally define private zones (e.g. `prod-dns-private-zone`). Granting visibility both to the trusted and untrusted landing VPCs ensures that the whole cloud environment can query such zones. -#### Cloud to on-premises +#### Cloud to on-prem Leveraging the forwarding zone defined in the landing project (e.g. `onprem-example-dns-forwarding` and `reverse-10-dns-forwarding`), the cloud environment can resolve `in-addr.arpa.` and `onprem.example.com.` using the on-premise DNS infrastructure. On-premise resolver IPs are set in the variable `dns.onprem`. DNS queries sent to the on-premise infrastructure come from the `35.199.192.0/19` source range. -#### On-premises to cloud +#### On-prem to cloud The [Inbound DNS Policy](https://cloud.google.com/dns/docs/server-policies-overview#dns-server-policy-in) defined in the *trusted landing VPC module* ([`landing.tf`](./landing.tf)) automatically reserves the first available IP address on each subnet (typically the third one in a CIDR) to expose the Cloud DNS service, so that it can be consumed from outside of GCP. -### Private Google Access +## How to run this stage -[Private Google Access](https://cloud.google.com/vpc/docs/private-google-access) (or PGA) is configured in this environment. It enables VMs and on-premise systems to consume Google APIs from within the Google network. +This stage is meant to be executed after the [resource management](../1-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../0-bootstrap) stage. + +It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. + +Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. + +### Provider and Terraform variables + +As all other FAST stages, the [mechanism used to pass variable values and pre-built provider files from one stage to the next](../0-bootstrap/README.md#output-files-and-cross-stage-variables) is also leveraged here. + +The commands to link or copy the provider and terraform variable files can be easily derived from the `stage-links.sh` script in the FAST root folder, passing it a single argument with the local output files folder (if configured) or the GCS output bucket in the automation project (derived from stage 0 outputs). The following examples demonstrate both cases, and the resulting commands that then need to be copy/pasted and run. + +```bash +../../stage-links.sh ~/fast-config + +# copy and paste the following commands for '2-networking-a-peering' + +ln -s ~/fast-config/providers/2-networking-providers.tf ./ +ln -s ~/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json ./ +``` + +```bash +../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '2-networking-a-peering' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/2-networking-providers.tf ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/globals.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/1-resman.auto.tfvars.json ./ +``` + +### Impersonating the automation service account + +The preconfigured provider file uses impersonation to run with this stage's automation service account's credentials. The `gcp-devops` and `organization-admins` groups have the necessary IAM bindings in place to do that, so make sure the current user is a member of one of those groups. + +### Variable configuration + +Variables in this stage -- like most other FAST stages -- are broadly divided into three separate sets: + +- variables which refer to global values for the whole organization (org id, billing account id, prefix, etc.), which are pre-populated via the `globals.auto.tfvars.json` file linked or copied above +- variables which refer to resources managed by previous stage, which are prepopulated here via the `0-bootstrap.auto.tfvars.json` and `1-resman.auto.tfvars.json` files linked or copied above +- and finally variables that optionally control this stage's behaviour and customizations, and can to be set in a custom `terraform.tfvars` file + +The latter set is explained in the [Customization](#customizations) sections below, and the full list can be found in the [Variables](#variables) table at the bottom of this document. + +### Running the stage + +Once provider and variable values are in place and the correct user is configured, the stage can be run: + +```bash +terraform init +terraform apply +``` + +### Post-deployment activities + +- On-prem routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recomment aggregating routes as much as possible. +- On-prem routers should accept BGP sessions from their cloud peers. +- On-prem DNS servers should have forward zones for GCP-managed ones. + +#### Private Google Access + +[Private Google Access](https://cloud.google.com/vpc/docs/private-google-access) (or PGA) enables VMs and on-prem systems to consume Google APIs from within the Google network, and is already fully configured on this environment. For PGA to work: - Private Google Access should be enabled on the subnet. \ -Subnets created using the `net-vpc` module are PGA-enabled by default. +Subnets created by the `net-vpc` module are PGA-enabled by default. -- 199.36.153.4/30 (`restricted.googleapis.com`) and 199.36.153.8/30 (`private.googleapis.com`) should be routed from on-premises to the trusted landing VPC, and from there to the `default-internet-gateway`. \ -The `vpn_onprem_configs` variable contains the ranges advertised from GCP to on-premises. Furthermore, the trusted landing VPC (e.g. see `landing-trusted-vpc` in [`landing.tf`](./landing.tf)) has explicit routes to send traffic destined to restricted and private - googleapis.com to the Internet gateway (which works for Google APIs only, and not for the whole Internet, since Cloud NAT is not configured in the trusted landing VPC). +- 199.36.153.4/30 (`restricted.googleapis.com`) and 199.36.153.8/30 (`private.googleapis.com`) should be routed from on-prem to VPC, and from there to the `default-internet-gateway`. \ +Per variable `vpn_onprem_configs` such ranges are advertised to onprem - furthermore every VPC (e.g. see `landing-vpc` in [`landing.tf`](./landing.tf)) has explicit routes set in case the `0.0.0.0/0` route is changed. -- On-premises, a private DNS zone for `googleapis.com` should be created and configured per [this article](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid#config-domain). Its configuration can be copied from the module `googleapis-private-zone` in [`dns-landing.tf`](./dns-landing.tf) - -### Preliminar activities - -Before running `terraform apply`, make sure to adapt `variables.tf` to your needs, to update the variable values using a new `terraform.tfvars` file, and to update the references to the regions in the whole directory, in order to match your preferences (e.g. `europe-west1` or `ew1`). - -If you're not using other FAST stages, you'll also need to create a `providers.tf` file to configure the GCS backend and the service account to use to run the deployment. - -You're now ready to run `terraform init` and `terraform apply`. - -### Post-deployment activities - -- On-premise routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recommend aggregating routes as much as possible -- On-premise routers should accept BGP sessions from their cloud peers -- On-premise DNS servers should have forward zones configured, in order to resolve GCP-managed domains +- A private DNS zone for `googleapis.com` should be created and configured per [this article](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid#config-domain), as implemented in module `googleapis-private-zone` in [`dns-landing.tf`](./dns-landing.tf) ## Customizations +### Changing default regions + +Regions are defined via the `regions` variable which sets up a mapping between the `regions.primary` and `regions.secondary` logical names and actual GCP region names. If you need to change regions from the defaults: + +- change the values of the mappings in the `regions` variable to the regions you are going to use +- change the regions in the factory subnet files in the `data` folder + ### Adding an environment To create a new environment (e.g. `staging`), a few changes are required: @@ -339,9 +374,6 @@ The new VPC requires a set of dedicated CIDRs, one per region, added to variable > Variables managing L7 Internal Load Balancers (`l7ilb_subnets`) and Private Service Access (`psa_ranges`) should also be adapted, and subnets and firewall rules for the new spoke should be added, as described above. -VPC network peering connectivity to the `trusted landing VPC` is managed by the `vpc-peering-*.tf` files. -Copy `vpc-peering-prod.tf` to `vpc-peering-staging.tf` and replace "prod" with "staging", where relevant. - Configure the NVAs deployed or update the sample [NVA config file](data/nva-startup-script.tftpl) making sure they support the new subnets. DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS resolution to Landing through DNS peering, and optionally define a private zone (e.g. `dev.gcp.example.com`) which the landing peers to. To configure DNS for a new environment, copy one of the other environments DNS files [e.g. (dns-dev.tf)](dns-dev.tf) into a new `dns-*.tf` file suffixed with the environment name (e.g. `dns-staging.tf`), and update its content accordingly. Don't forget to add a peering zone from the landing to the newly created environment private zone. @@ -361,6 +393,7 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS | [monitoring.tf](./monitoring.tf) | Network monitoring dashboards. | | google_monitoring_dashboard | | [nva.tf](./nva.tf) | None | compute-mig · compute-vm · simple-nva | | | [outputs.tf](./outputs.tf) | Module outputs. | | google_storage_bucket_object · local_file | +| [regions.tf](./regions.tf) | Compute short names for regions. | | | | [spoke-dev.tf](./spoke-dev.tf) | Dev spoke VPC and related resources. | net-vpc · net-vpc-firewall · net-vpc-peering · project | google_project_iam_binding | | [spoke-prod.tf](./spoke-prod.tf) | Production spoke VPC and related resources. | net-vpc · net-vpc-firewall · net-vpc-peering · project | google_project_iam_binding | | [test-resources.tf](./test-resources.tf) | temporary instances for testing | compute-vm | | @@ -371,23 +404,23 @@ DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | -| [billing_account](variables.tf#L25) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [folder_ids](variables.tf#L79) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 01-resman | -| [organization](variables.tf#L115) | Organization details. | object({…}) | ✓ | | 00-bootstrap | -| [prefix](variables.tf#L131) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | -| [custom_adv](variables.tf#L34) | Custom advertisement definitions in name => range format. | map(string) | | {…} | | -| [custom_roles](variables.tf#L56) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 00-bootstrap | -| [data_dir](variables.tf#L65) | Relative path for the folder storing configuration data for network resources. | string | | "data" | | -| [dns](variables.tf#L71) | Onprem DNS resolvers. | map(list(string)) | | {…} | | -| [l7ilb_subnets](variables.tf#L89) | Subnets used for L7 ILBs. | map(list(object({…}))) | | {…} | | -| [onprem_cidr](variables.tf#L107) | Onprem addresses in name => range format. | map(string) | | {…} | | -| [outputs_location](variables.tf#L125) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | -| [psa_ranges](variables.tf#L142) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | -| [region_trigram](variables.tf#L183) | Short names for GCP regions. | map(string) | | {…} | | -| [router_configs](variables.tf#L192) | Configurations for CRs and onprem routers. | map(object({…})) | | {…} | | -| [service_accounts](variables.tf#L215) | Automation service accounts in name => email format. | object({…}) | | null | 01-resman | -| [vpn_onprem_configs](variables.tf#L229) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | +| [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#L97) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman | +| [organization](variables.tf#L133) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L149) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [custom_adv](variables.tf#L38) | Custom advertisement definitions in name => range format. | map(string) | | {…} | | +| [custom_roles](variables.tf#L60) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | +| [dns](variables.tf#L69) | Onprem DNS resolvers. | map(list(string)) | | {…} | | +| [factories_config](variables.tf#L77) | Configuration for network resource factories. | object({…}) | | {…} | | +| [l7ilb_subnets](variables.tf#L107) | Subnets used for L7 ILBs. | map(list(object({…}))) | | {…} | | +| [onprem_cidr](variables.tf#L125) | Onprem addresses in name => range format. | map(string) | | {…} | | +| [outputs_location](variables.tf#L143) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | +| [psa_ranges](variables.tf#L160) | IP ranges used for Private Service Access (e.g. CloudSQL). Ranges is in name => range format. | object({…}) | | null | | +| [regions](variables.tf#L181) | Region definitions. | object({…}) | | {…} | | +| [router_configs](variables.tf#L193) | Configurations for CRs and onprem routers. | map(object({…})) | | {…} | | +| [service_accounts](variables.tf#L216) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | +| [vpn_onprem_configs](variables.tf#L230) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | ## Outputs diff --git a/fast/stages/02-networking-separate-envs/data/cidrs.yaml b/fast/stages/2-networking-c-nva/data/cidrs.yaml similarity index 100% rename from fast/stages/02-networking-separate-envs/data/cidrs.yaml rename to fast/stages/2-networking-c-nva/data/cidrs.yaml diff --git a/fast/stages/02-networking-nva/data/dashboards/firewall_insights.json b/fast/stages/2-networking-c-nva/data/dashboards/firewall_insights.json similarity index 100% rename from fast/stages/02-networking-nva/data/dashboards/firewall_insights.json rename to fast/stages/2-networking-c-nva/data/dashboards/firewall_insights.json diff --git a/fast/stages/02-networking-nva/data/dashboards/vpn.json b/fast/stages/2-networking-c-nva/data/dashboards/vpn.json similarity index 100% rename from fast/stages/02-networking-nva/data/dashboards/vpn.json rename to fast/stages/2-networking-c-nva/data/dashboards/vpn.json diff --git a/fast/stages/02-networking-vpn/data/firewall-rules/dev/rules.yaml b/fast/stages/2-networking-c-nva/data/firewall-rules/dev/rules.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/firewall-rules/dev/rules.yaml rename to fast/stages/2-networking-c-nva/data/firewall-rules/dev/rules.yaml diff --git a/fast/stages/02-networking-nva/data/firewall-rules/landing-trusted/rules.yaml b/fast/stages/2-networking-c-nva/data/firewall-rules/landing-trusted/rules.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/firewall-rules/landing-trusted/rules.yaml rename to fast/stages/2-networking-c-nva/data/firewall-rules/landing-trusted/rules.yaml diff --git a/fast/stages/02-networking-nva/data/firewall-rules/landing-untrusted/rules.yaml b/fast/stages/2-networking-c-nva/data/firewall-rules/landing-untrusted/rules.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/firewall-rules/landing-untrusted/rules.yaml rename to fast/stages/2-networking-c-nva/data/firewall-rules/landing-untrusted/rules.yaml diff --git a/fast/stages/02-networking-separate-envs/data/hierarchical-policy-rules.yaml b/fast/stages/2-networking-c-nva/data/hierarchical-policy-rules.yaml similarity index 100% rename from fast/stages/02-networking-separate-envs/data/hierarchical-policy-rules.yaml rename to fast/stages/2-networking-c-nva/data/hierarchical-policy-rules.yaml diff --git a/fast/stages/02-networking-nva/data/nva-startup-script.tftpl b/fast/stages/2-networking-c-nva/data/nva-startup-script.tftpl similarity index 100% rename from fast/stages/02-networking-nva/data/nva-startup-script.tftpl rename to fast/stages/2-networking-c-nva/data/nva-startup-script.tftpl diff --git a/fast/stages/02-networking-nva/data/subnets/dev/dev-dataplatform-ew1.yaml b/fast/stages/2-networking-c-nva/data/subnets/dev/dev-dataplatform-ew1.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/dev/dev-dataplatform-ew1.yaml rename to fast/stages/2-networking-c-nva/data/subnets/dev/dev-dataplatform-ew1.yaml diff --git a/fast/stages/02-networking-nva/data/subnets/dev/dev-default-ew1.yaml b/fast/stages/2-networking-c-nva/data/subnets/dev/dev-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/dev/dev-default-ew1.yaml rename to fast/stages/2-networking-c-nva/data/subnets/dev/dev-default-ew1.yaml diff --git a/fast/stages/02-networking-nva/data/subnets/dev/dev-default-ew4.yaml b/fast/stages/2-networking-c-nva/data/subnets/dev/dev-default-ew4.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/dev/dev-default-ew4.yaml rename to fast/stages/2-networking-c-nva/data/subnets/dev/dev-default-ew4.yaml diff --git a/fast/stages/02-networking-nva/data/subnets/landing-trusted/landing-trusted-default-ew1.yaml b/fast/stages/2-networking-c-nva/data/subnets/landing-trusted/landing-trusted-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/landing-trusted/landing-trusted-default-ew1.yaml rename to fast/stages/2-networking-c-nva/data/subnets/landing-trusted/landing-trusted-default-ew1.yaml diff --git a/fast/stages/02-networking-nva/data/subnets/landing-trusted/landing-trusted-default-ew4.yaml b/fast/stages/2-networking-c-nva/data/subnets/landing-trusted/landing-trusted-default-ew4.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/landing-trusted/landing-trusted-default-ew4.yaml rename to fast/stages/2-networking-c-nva/data/subnets/landing-trusted/landing-trusted-default-ew4.yaml diff --git a/fast/stages/02-networking-nva/data/subnets/landing-untrusted/landing-untrusted-default-ew1.yaml b/fast/stages/2-networking-c-nva/data/subnets/landing-untrusted/landing-untrusted-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/landing-untrusted/landing-untrusted-default-ew1.yaml rename to fast/stages/2-networking-c-nva/data/subnets/landing-untrusted/landing-untrusted-default-ew1.yaml diff --git a/fast/stages/02-networking-nva/data/subnets/landing-untrusted/landing-untrusted-default-ew4.yaml b/fast/stages/2-networking-c-nva/data/subnets/landing-untrusted/landing-untrusted-default-ew4.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/landing-untrusted/landing-untrusted-default-ew4.yaml rename to fast/stages/2-networking-c-nva/data/subnets/landing-untrusted/landing-untrusted-default-ew4.yaml diff --git a/fast/stages/02-networking-nva/data/subnets/prod/prod-default-ew1.yaml b/fast/stages/2-networking-c-nva/data/subnets/prod/prod-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/prod/prod-default-ew1.yaml rename to fast/stages/2-networking-c-nva/data/subnets/prod/prod-default-ew1.yaml diff --git a/fast/stages/02-networking-nva/data/subnets/prod/prod-default-ew4.yaml b/fast/stages/2-networking-c-nva/data/subnets/prod/prod-default-ew4.yaml similarity index 100% rename from fast/stages/02-networking-nva/data/subnets/prod/prod-default-ew4.yaml rename to fast/stages/2-networking-c-nva/data/subnets/prod/prod-default-ew4.yaml diff --git a/fast/stages/02-networking-nva/diagram.png b/fast/stages/2-networking-c-nva/diagram.png similarity index 100% rename from fast/stages/02-networking-nva/diagram.png rename to fast/stages/2-networking-c-nva/diagram.png diff --git a/fast/stages/02-networking-nva/diagram.svg b/fast/stages/2-networking-c-nva/diagram.svg similarity index 100% rename from fast/stages/02-networking-nva/diagram.svg rename to fast/stages/2-networking-c-nva/diagram.svg diff --git a/fast/stages/02-networking-nva/dns-dev.tf b/fast/stages/2-networking-c-nva/dns-dev.tf similarity index 100% rename from fast/stages/02-networking-nva/dns-dev.tf rename to fast/stages/2-networking-c-nva/dns-dev.tf diff --git a/fast/stages/02-networking-nva/dns-landing.tf b/fast/stages/2-networking-c-nva/dns-landing.tf similarity index 100% rename from fast/stages/02-networking-nva/dns-landing.tf rename to fast/stages/2-networking-c-nva/dns-landing.tf diff --git a/fast/stages/02-networking-nva/dns-prod.tf b/fast/stages/2-networking-c-nva/dns-prod.tf similarity index 100% rename from fast/stages/02-networking-nva/dns-prod.tf rename to fast/stages/2-networking-c-nva/dns-prod.tf diff --git a/fast/stages/02-networking-nva/landing.tf b/fast/stages/2-networking-c-nva/landing.tf similarity index 76% rename from fast/stages/02-networking-nva/landing.tf rename to fast/stages/2-networking-c-nva/landing.tf index 5a990030f..e66b03db9 100644 --- a/fast/stages/02-networking-nva/landing.tf +++ b/fast/stages/2-networking-c-nva/landing.tf @@ -53,7 +53,7 @@ module "landing-untrusted-vpc" { inbound = false logging = false } - data_folder = "${var.data_dir}/subnets/landing-untrusted" + data_folder = "${var.factories_config.data_dir}/subnets/landing-untrusted" } module "landing-untrusted-firewall" { @@ -64,31 +64,41 @@ module "landing-untrusted-firewall" { disabled = true } factories_config = { - cidr_tpl_file = "${var.data_dir}/cidrs.yaml" - rules_folder = "${var.data_dir}/firewall-rules/landing-untrusted" + cidr_tpl_file = "${var.factories_config.data_dir}/cidrs.yaml" + rules_folder = "${var.factories_config.data_dir}/firewall-rules/landing-untrusted" } } # NAT -module "landing-nat-ew1" { +moved { + from = module.landing-nat-ew1 + to = module.landing-nat-primary +} + +module "landing-nat-primary" { source = "../../../modules/net-cloudnat" project_id = module.landing-project.project_id - region = "europe-west1" - name = "ew1" + region = var.regions.primary + name = local.region_shortnames[var.regions.primary] router_create = true - router_name = "prod-nat-ew1" + router_name = "prod-nat-${local.region_shortnames[var.regions.primary]}" router_network = module.landing-untrusted-vpc.name router_asn = 4200001024 } -module "landing-nat-ew4" { +moved { + from = module.landing-nat-ew4 + to = module.landing-nat-secondary +} + +module "landing-nat-secondary" { source = "../../../modules/net-cloudnat" project_id = module.landing-project.project_id - region = "europe-west4" - name = "ew4" + region = var.regions.secondary + name = local.region_shortnames[var.regions.secondary] router_create = true - router_name = "prod-nat-ew4" + router_name = "prod-nat-${local.region_shortnames[var.regions.secondary]}" router_network = module.landing-untrusted-vpc.name router_asn = 4200001024 } @@ -101,7 +111,10 @@ 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" + dns_policy = { + inbound = true + } # Set explicit routes for googleapis in case the default route is deleted routes = { private-googleapis = { @@ -115,12 +128,6 @@ module "landing-trusted-vpc" { next_hop = "default-internet-gateway" } } - - dns_policy = { - inbound = true - } - - data_folder = "${var.data_dir}/subnets/landing-trusted" } module "landing-trusted-firewall" { @@ -131,7 +138,7 @@ module "landing-trusted-firewall" { disabled = true } factories_config = { - cidr_tpl_file = "${var.data_dir}/cidrs.yaml" - rules_folder = "${var.data_dir}/firewall-rules/landing-trusted" + cidr_tpl_file = "${var.factories_config.data_dir}/cidrs.yaml" + rules_folder = "${var.factories_config.data_dir}/firewall-rules/landing-trusted" } } diff --git a/fast/stages/02-networking-nva/main.tf b/fast/stages/2-networking-c-nva/main.tf similarity index 73% rename from fast/stages/02-networking-nva/main.tf rename to fast/stages/2-networking-c-nva/main.tf index 4db5061ba..e4066ba35 100644 --- a/fast/stages/02-networking-nva/main.tf +++ b/fast/stages/2-networking-c-nva/main.tf @@ -24,6 +24,14 @@ locals { name = "${env}-l7ilb-${s.region}" })] } + # combine all regions from variables and subnets + regions = distinct(concat( + values(var.regions), + values(module.dev-spoke-vpc.subnet_regions), + values(module.landing-trusted-vpc.subnet_regions), + values(module.landing-untrusted-vpc.subnet_regions), + values(module.prod-spoke-vpc.subnet_regions), + )) service_accounts = { for k, v in coalesce(var.service_accounts, {}) : k => "serviceAccount:${v}" if v != null @@ -45,11 +53,11 @@ module "folder" { folder_create = var.folder_ids.networking == null id = var.folder_ids.networking firewall_policy_factory = { - cidr_file = "${var.data_dir}/cidrs.yaml" - policy_name = null - rules_file = "${var.data_dir}/hierarchical-policy-rules.yaml" + cidr_file = "${var.factories_config.data_dir}/cidrs.yaml" + policy_name = var.factories_config.firewall_policy_name + rules_file = "${var.factories_config.data_dir}/hierarchical-policy-rules.yaml" } firewall_policy_association = { - factory-policy = "factory" + factory-policy = var.factories_config.firewall_policy_name } } diff --git a/fast/stages/02-networking-peering/monitoring.tf b/fast/stages/2-networking-c-nva/monitoring.tf similarity index 93% rename from fast/stages/02-networking-peering/monitoring.tf rename to fast/stages/2-networking-c-nva/monitoring.tf index 7b8b70c51..be3a47faa 100644 --- a/fast/stages/02-networking-peering/monitoring.tf +++ b/fast/stages/2-networking-c-nva/monitoring.tf @@ -17,7 +17,7 @@ # tfdoc:file:description Network monitoring dashboards. locals { - dashboard_path = "${var.data_dir}/dashboards" + dashboard_path = "${var.factories_config.data_dir}/dashboards" dashboard_files = fileset(local.dashboard_path, "*.json") dashboards = { for filename in local.dashboard_files : diff --git a/fast/stages/02-networking-nva/nva.tf b/fast/stages/2-networking-c-nva/nva.tf similarity index 66% rename from fast/stages/02-networking-nva/nva.tf rename to fast/stages/2-networking-c-nva/nva.tf index f4f7b9e5e..7ae9c30c8 100644 --- a/fast/stages/02-networking-nva/nva.tf +++ b/fast/stages/2-networking-c-nva/nva.tf @@ -21,29 +21,32 @@ locals { { name = "untrusted" routes = [ - var.custom_adv.gcp_landing_untrusted_ew1, - var.custom_adv.gcp_landing_untrusted_ew4, + var.custom_adv.gcp_landing_untrusted_primary, + var.custom_adv.gcp_landing_untrusted_secondary, ] }, { name = "trusted" routes = [ - var.custom_adv.gcp_dev_ew1, - var.custom_adv.gcp_dev_ew4, - var.custom_adv.gcp_landing_trusted_ew1, - var.custom_adv.gcp_landing_trusted_ew4, - var.custom_adv.gcp_prod_ew1, - var.custom_adv.gcp_prod_ew4, + var.custom_adv.gcp_dev_primary, + var.custom_adv.gcp_dev_secondary, + var.custom_adv.gcp_landing_trusted_primary, + var.custom_adv.gcp_landing_trusted_secondary, + var.custom_adv.gcp_prod_primary, + var.custom_adv.gcp_prod_secondary, ] }, ] nva_locality = { - europe-west1-b = { region = "europe-west1", trigram = "ew1", zone = "b" }, - europe-west1-c = { region = "europe-west1", trigram = "ew1", zone = "c" }, - europe-west4-b = { region = "europe-west4", trigram = "ew4", zone = "b" }, - europe-west4-c = { region = "europe-west4", trigram = "ew4", zone = "c" }, + for v in setproduct(keys(var.regions), local.nva_zones) : + join("-", v) => { + name = v.0 + region = var.regions[v.0] + shortname = local.region_shortnames[var.regions[v.0]] + zone = v.1 + } } - + nva_zones = ["b", "c"] } # NVA config @@ -57,7 +60,7 @@ module "nva-template" { for_each = local.nva_locality source = "../../../modules/compute-vm" project_id = module.landing-project.project_id - name = "nva-template-${each.value.trigram}-${each.value.zone}" + name = "nva-template-${each.key}" zone = "${each.value.region}-${each.value.zone}" instance_type = "e2-standard-2" tags = ["nva"] @@ -66,13 +69,13 @@ module "nva-template" { network_interfaces = [ { network = module.landing-untrusted-vpc.self_link - subnetwork = module.landing-untrusted-vpc.subnet_self_links["${each.value.region}/landing-untrusted-default-${each.value.trigram}"] + subnetwork = module.landing-untrusted-vpc.subnet_self_links["${each.value.region}/landing-untrusted-default-${each.value.shortname}"] nat = false addresses = null }, { network = module.landing-trusted-vpc.self_link - subnetwork = module.landing-trusted-vpc.subnet_self_links["${each.value.region}/landing-trusted-default-${each.value.trigram}"] + subnetwork = module.landing-trusted-vpc.subnet_self_links["${each.value.region}/landing-trusted-default-${each.value.shortname}"] nat = false addresses = null } @@ -98,7 +101,7 @@ module "nva-mig" { source = "../../../modules/compute-mig" project_id = module.landing-project.project_id location = each.value.region - name = "nva-cos-${each.value.trigram}-${each.value.zone}" + name = "nva-cos-${each.key}" instance_template = module.nva-template[each.key].template.self_link target_size = 1 auto_healing_policies = { @@ -113,21 +116,27 @@ module "nva-mig" { } module "ilb-nva-untrusted" { - for_each = { for l in local.nva_locality : l.region => l.trigram... } + for_each = { + for k, v in var.regions : k => { + region = v + shortname = local.region_shortnames[v] + subnet = "${v}/landing-untrusted-default-${local.region_shortnames[v]}" + } + } source = "../../../modules/net-ilb" project_id = module.landing-project.project_id - region = each.key - name = "nva-untrusted-${each.value.0}" + region = each.value.region + name = "nva-untrusted-${each.key}" service_label = var.prefix global_access = true vpc_config = { network = module.landing-untrusted-vpc.self_link - subnetwork = module.landing-untrusted-vpc.subnet_self_links["${each.key}/landing-untrusted-default-${each.value.0}"] + subnetwork = module.landing-untrusted-vpc.subnet_self_links[each.value.subnet] } backends = [ - for key, _ in local.nva_locality : { - group = module.nva-mig[key].group_manager.instance_group - } if local.nva_locality[key].region == each.key + for k, v in module.nva-mig : + { group = v.group_manager.instance_group } + if startswith(k, each.key) ] health_check_config = { enable_logging = true @@ -137,23 +146,28 @@ module "ilb-nva-untrusted" { } } - module "ilb-nva-trusted" { - for_each = { for l in local.nva_locality : l.region => l.trigram... } + for_each = { + for k, v in var.regions : k => { + region = v + shortname = local.region_shortnames[v] + subnet = "${v}/landing-trusted-default-${local.region_shortnames[v]}" + } + } source = "../../../modules/net-ilb" project_id = module.landing-project.project_id - region = each.key - name = "nva-trusted-${each.value.0}" + region = each.value.region + name = "nva-trusted-${each.key}" service_label = var.prefix global_access = true vpc_config = { network = module.landing-trusted-vpc.self_link - subnetwork = module.landing-trusted-vpc.subnet_self_links["${each.key}/landing-trusted-default-${each.value.0}"] + subnetwork = module.landing-trusted-vpc.subnet_self_links[each.value.subnet] } backends = [ - for key, _ in local.nva_locality : { - group = module.nva-mig[key].group_manager.instance_group - } if local.nva_locality[key].region == each.key + for k, v in module.nva-mig : + { group = v.group_manager.instance_group } + if startswith(k, each.key) ] health_check_config = { enable_logging = true @@ -162,4 +176,3 @@ module "ilb-nva-trusted" { } } } - diff --git a/fast/stages/02-networking-nva/outputs.tf b/fast/stages/2-networking-c-nva/outputs.tf similarity index 89% rename from fast/stages/02-networking-nva/outputs.tf rename to fast/stages/2-networking-c-nva/outputs.tf index df324570d..746202427 100644 --- a/fast/stages/02-networking-nva/outputs.tf +++ b/fast/stages/2-networking-c-nva/outputs.tf @@ -43,13 +43,13 @@ locals { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/02-networking.auto.tfvars.json" + filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket - name = "tfvars/02-networking.auto.tfvars.json" + name = "tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } @@ -79,12 +79,12 @@ output "tfvars" { output "vpn_gateway_endpoints" { description = "External IP Addresses for the GCP VPN gateways." value = local.enable_onprem_vpn == false ? null : { - onprem-ew1 = { - for v in module.landing-to-onprem-ew1-vpn[0].gateway.vpn_interfaces : + onprem-primary = { + for v in module.landing-to-onprem-primary-vpn[0].gateway.vpn_interfaces : v.id => v.ip_address } - onprem-ew4 = { - for v in module.landing-to-onprem-ew4-vpn[0].gateway.vpn_interfaces : + onprem-secondary = { + for v in module.landing-to-onprem-secondary-vpn[0].gateway.vpn_interfaces : v.id => v.ip_address } } diff --git a/fast/stages/2-networking-c-nva/regions.tf b/fast/stages/2-networking-c-nva/regions.tf new file mode 100644 index 000000000..53514afa9 --- /dev/null +++ b/fast/stages/2-networking-c-nva/regions.tf @@ -0,0 +1,42 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Compute short names for regions. + +locals { + # only map when the first character would not work + _region_cardinal = { + southeast = "se" + } + # only map when the first character would not work + _region_geo = { + australia = "o" + } + # split in [geo, cardinal, number] tokens + _region_tokens = { + for v in local.regions : v => regexall("(?:[a-z]+)|(?:[0-9]+)", v) + } + region_shortnames = { + for k, v in local._region_tokens : k => join("", [ + # first token via geo alias map or first character + lookup(local._region_geo, v.0, substr(v.0, 0, 1)), + # first token via cardinal alias map or first character + lookup(local._region_cardinal, v.1, substr(v.1, 0, 1)), + # region number as is + v.2 + ]) + } +} diff --git a/fast/stages/02-networking-nva/spoke-dev.tf b/fast/stages/2-networking-c-nva/spoke-dev.tf similarity index 76% rename from fast/stages/02-networking-nva/spoke-dev.tf rename to fast/stages/2-networking-c-nva/spoke-dev.tf index fb88384c4..b22938131 100644 --- a/fast/stages/02-networking-nva/spoke-dev.tf +++ b/fast/stages/2-networking-c-nva/spoke-dev.tf @@ -16,6 +16,19 @@ # tfdoc:file:description Dev spoke VPC and related resources. +locals { + _l7ilb_subnets_dev = [ + for v in var.l7ilb_subnets.dev : merge(v, { + active = true + region = lookup(var.regions, v.region, v.region) + })] + l7ilb_subnets_dev = [ + for v in local._l7ilb_subnets_dev : merge(v, { + name = "dev-l7ilb-${local.region_shortnames[v.region]}" + }) + ] +} + module "dev-spoke-project" { source = "../../../modules/project" billing_account = var.billing_account.id @@ -47,10 +60,10 @@ module "dev-spoke-vpc" { project_id = module.dev-spoke-project.project_id name = "dev-spoke-0" mtu = 1500 - data_folder = "${var.data_dir}/subnets/dev" + data_folder = "${var.factories_config.data_dir}/subnets/dev" delete_default_routes_on_create = true psa_config = try(var.psa_ranges.dev, null) - subnets_proxy_only = local.l7ilb_subnets.dev + subnets_proxy_only = local.l7ilb_subnets_dev # Set explicit routes for googleapis; send everything else to NVAs routes = { private-googleapis = { @@ -65,33 +78,33 @@ module "dev-spoke-vpc" { next_hop_type = "gateway" next_hop = "default-internet-gateway" } - nva-ew1-to-ew1 = { + nva-primary-to-primary = { dest_range = "0.0.0.0/0" priority = 1000 - tags = ["ew1"] + tags = ["primary"] next_hop_type = "ilb" - next_hop = module.ilb-nva-trusted["europe-west1"].forwarding_rule_address + next_hop = module.ilb-nva-trusted["primary"].forwarding_rule_address } - nva-ew4-to-ew4 = { + nva-secondary-to-secondary = { dest_range = "0.0.0.0/0" priority = 1000 - tags = ["ew4"] + tags = ["secondary"] next_hop_type = "ilb" - next_hop = module.ilb-nva-trusted["europe-west4"].forwarding_rule_address + next_hop = module.ilb-nva-trusted["secondary"].forwarding_rule_address } - nva-ew1-to-ew4 = { + nva-primary-to-secondary = { dest_range = "0.0.0.0/0" priority = 1001 - tags = ["ew1"] + tags = ["primary"] next_hop_type = "ilb" - next_hop = module.ilb-nva-trusted["europe-west4"].forwarding_rule_address + next_hop = module.ilb-nva-trusted["primary"].forwarding_rule_address } - nva-ew4-to-ew1 = { + nva-secondary-to-primary = { dest_range = "0.0.0.0/0" priority = 1001 - tags = ["ew4"] + tags = ["secondary"] next_hop_type = "ilb" - next_hop = module.ilb-nva-trusted["europe-west1"].forwarding_rule_address + next_hop = module.ilb-nva-trusted["secondary"].forwarding_rule_address } } } @@ -104,8 +117,8 @@ module "dev-spoke-firewall" { disabled = true } factories_config = { - cidr_tpl_file = "${var.data_dir}/cidrs.yaml" - rules_folder = "${var.data_dir}/firewall-rules/dev" + cidr_tpl_file = "${var.factories_config.data_dir}/cidrs.yaml" + rules_folder = "${var.factories_config.data_dir}/firewall-rules/dev" } } diff --git a/fast/stages/02-networking-nva/spoke-prod.tf b/fast/stages/2-networking-c-nva/spoke-prod.tf similarity index 76% rename from fast/stages/02-networking-nva/spoke-prod.tf rename to fast/stages/2-networking-c-nva/spoke-prod.tf index 484550aca..51ad29745 100644 --- a/fast/stages/02-networking-nva/spoke-prod.tf +++ b/fast/stages/2-networking-c-nva/spoke-prod.tf @@ -16,6 +16,19 @@ # tfdoc:file:description Production spoke VPC and related resources. +locals { + _l7ilb_subnets_prod = [ + for v in var.l7ilb_subnets.prod : merge(v, { + active = true + region = lookup(var.regions, v.region, v.region) + })] + l7ilb_subnets_prod = [ + for v in local._l7ilb_subnets_prod : merge(v, { + name = "prod-l7ilb-${local.region_shortnames[v.region]}" + }) + ] +} + module "prod-spoke-project" { source = "../../../modules/project" billing_account = var.billing_account.id @@ -47,10 +60,10 @@ module "prod-spoke-vpc" { project_id = module.prod-spoke-project.project_id name = "prod-spoke-0" mtu = 1500 - data_folder = "${var.data_dir}/subnets/prod" + data_folder = "${var.factories_config.data_dir}/subnets/prod" delete_default_routes_on_create = true psa_config = try(var.psa_ranges.prod, null) - subnets_proxy_only = local.l7ilb_subnets.prod + subnets_proxy_only = local.l7ilb_subnets_prod # Set explicit routes for googleapis; send everything else to NVAs routes = { private-googleapis = { @@ -65,33 +78,33 @@ module "prod-spoke-vpc" { next_hop_type = "gateway" next_hop = "default-internet-gateway" } - nva-ew1-to-ew1 = { + nva-primary-to-primary = { dest_range = "0.0.0.0/0" priority = 1000 - tags = ["ew1"] + tags = ["primary"] next_hop_type = "ilb" - next_hop = module.ilb-nva-trusted["europe-west1"].forwarding_rule_address + next_hop = module.ilb-nva-trusted["primary"].forwarding_rule_address } - nva-ew4-to-ew4 = { + nva-secondary-to-secondary = { dest_range = "0.0.0.0/0" priority = 1000 - tags = ["ew4"] + tags = ["secondary"] next_hop_type = "ilb" - next_hop = module.ilb-nva-trusted["europe-west4"].forwarding_rule_address + next_hop = module.ilb-nva-trusted["secondary"].forwarding_rule_address } - nva-ew1-to-ew4 = { + nva-primary-to-secondary = { dest_range = "0.0.0.0/0" priority = 1001 - tags = ["ew1"] + tags = ["primary"] next_hop_type = "ilb" - next_hop = module.ilb-nva-trusted["europe-west4"].forwarding_rule_address + next_hop = module.ilb-nva-trusted["secondary"].forwarding_rule_address } - nva-ew4-to-ew1 = { + nva-secondary-to-primary = { dest_range = "0.0.0.0/0" priority = 1001 - tags = ["ew4"] + tags = ["secondary"] next_hop_type = "ilb" - next_hop = module.ilb-nva-trusted["europe-west1"].forwarding_rule_address + next_hop = module.ilb-nva-trusted["primary"].forwarding_rule_address } } } @@ -104,8 +117,8 @@ module "prod-spoke-firewall" { disabled = true } factories_config = { - cidr_tpl_file = "${var.data_dir}/cidrs.yaml" - rules_folder = "${var.data_dir}/firewall-rules/prod" + cidr_tpl_file = "${var.factories_config.data_dir}/cidrs.yaml" + rules_folder = "${var.factories_config.data_dir}/firewall-rules/prod" } } diff --git a/fast/stages/02-networking-nva/test-resources.tf b/fast/stages/2-networking-c-nva/test-resources.tf similarity index 63% rename from fast/stages/02-networking-nva/test-resources.tf rename to fast/stages/2-networking-c-nva/test-resources.tf index d3b6109e3..14676e811 100644 --- a/fast/stages/02-networking-nva/test-resources.tf +++ b/fast/stages/2-networking-c-nva/test-resources.tf @@ -18,23 +18,23 @@ # # Untrusted (Landing) -# module "test-vm-landing-untrusted-ew1-0" { +# module "test-vm-landing-untrusted-primary-0" { # source = "../../../modules/compute-vm" # project_id = module.landing-project.project_id -# zone = "europe-west1-b" -# name = "test-vm-lnd-unt-ew1-0" +# zone = "${var.regions.primary}-b" +# name = "test-vm-lnd-unt-primary-0" # network_interfaces = [{ # network = module.landing-untrusted-vpc.self_link -# subnetwork = module.landing-untrusted-vpc.subnet_self_links["europe-west1/landing-untrusted-default-ew1"] +# subnetwork = module.landing-untrusted-vpc.subnet_self_links["${var.regions.primary}/landing-untrusted-default-${local.region_shortnames[var.regions.primary]}"] # }] -# tags = ["ew1", "ssh"] +# tags = ["primary", "ssh"] # service_account_create = true # boot_disk = { # image = "projects/debian-cloud/global/images/family/debian-10" # } # options = { -# spot = true -# termination_action = "STOP" +# spot = true +# termination_action = "STOP" # } # metadata = { # startup-script = < v.adv == null ? null : { - advertise_groups = [] - advertise_ip_ranges = { - for adv in(v.adv == null ? [] : v.adv.custom) : - var.custom_adv[adv] => adv + custom_advertise = try(v.adv.default, false) ? null : { + all_subnets = false + all_vpc_subnets = false + all_peer_vpc_subnets = false + ip_ranges = { + for adv in(v.adv == null ? [] : v.adv.custom) : + var.custom_adv[adv] => adv + } } - advertise_mode = try(v.adv.default, false) ? "DEFAULT" : "CUSTOM" route_priority = null } } } -module "landing-to-onprem-ew1-vpn" { +moved { + from = module.landing-to-onprem-ew1-vpn + to = module.landing-to-onprem-primary-vpn +} + +module "landing-to-onprem-primary-vpn" { count = local.enable_onprem_vpn ? 1 : 0 source = "../../../modules/net-vpn-ha" project_id = module.landing-project.project_id network = module.landing-trusted-vpc.self_link - region = "europe-west1" - name = "vpn-to-onprem-ew1" + region = var.regions.primary + name = "vpn-to-onprem-${local.region_shortnames[var.regions.primary]}" router_config = { - name = "landing-onprem-vpn-ew1" - asn = var.router_configs.landing-trusted-ew1.asn + name = "landing-onprem-vpn-${local.region_shortnames[var.regions.primary]}" + asn = var.router_configs.landing-trusted-primary.asn } peer_gateway = { - external = var.vpn_onprem_configs.landing-trusted-ew1.peer_external_gateway + external = var.vpn_onprem_configs.landing-trusted-primary.peer_external_gateway } tunnels = { - for t in var.vpn_onprem_configs.landing-trusted-ew1.tunnels : + for t in var.vpn_onprem_configs.landing-trusted-primary.tunnels : "remote-${t.vpn_gateway_interface}-${t.peer_external_gateway_interface}" => { - bgp_peer = { - address = cidrhost(t.session_range, 1) - asn = t.peer_asn - } - bgp_peer_options = local.bgp_peer_options_onprem.landing-trusted-ew1 + bgp_peer = merge( + { address = cidrhost(t.session_range, 1), asn = t.peer_asn }, + local.bgp_peer_options_onprem.landing-trusted-primary + ) bgp_session_range = "${cidrhost(t.session_range, 2)}/30" peer_external_gateway_interface = t.peer_external_gateway_interface shared_secret = t.secret @@ -62,28 +69,32 @@ module "landing-to-onprem-ew1-vpn" { } } -module "landing-to-onprem-ew4-vpn" { +moved { + from = module.landing-to-onprem-ew4-vpn + to = module.landing-to-onprem-secondary-vpn +} + +module "landing-to-onprem-secondary-vpn" { count = local.enable_onprem_vpn ? 1 : 0 source = "../../../modules/net-vpn-ha" project_id = module.landing-project.project_id network = module.landing-trusted-vpc.self_link - region = "europe-west4" - name = "vpn-to-onprem-ew4" + region = var.regions.secondary + name = "vpn-to-onprem-${local.region_shortnames[var.regions.secondary]}" router_config = { - name = "landing-onprem-vpn-ew4" - asn = var.router_configs.landing-trusted-ew4.asn + name = "landing-onprem-vpn-${local.region_shortnames[var.regions.secondary]}" + asn = var.router_configs.landing-trusted-secondary.asn } peer_gateway = { - external = var.vpn_onprem_configs.landing-trusted-ew4.peer_external_gateway + external = var.vpn_onprem_configs.landing-trusted-secondary.peer_external_gateway } tunnels = { - for t in var.vpn_onprem_configs.landing-trusted-ew4.tunnels : + for t in var.vpn_onprem_configs.landing-trusted-secondary.tunnels : "remote-${t.vpn_gateway_interface}-${t.peer_external_gateway_interface}" => { - bgp_peer = { - address = cidrhost(t.session_range, 1) - asn = t.peer_asn - } - bgp_peer_options = local.bgp_peer_options_onprem.landing-trusted-ew4 + bgp_peer = merge( + { address = cidrhost(t.session_range, 1), asn = t.peer_asn }, + local.bgp_peer_options_onprem.landing-trusted-secondary + ) bgp_session_range = "${cidrhost(t.session_range, 2)}/30" peer_external_gateway_interface = t.peer_external_gateway_interface shared_secret = t.secret diff --git a/fast/stages/02-networking-vpn/.gitignore b/fast/stages/2-networking-d-separate-envs/.gitignore similarity index 100% rename from fast/stages/02-networking-vpn/.gitignore rename to fast/stages/2-networking-d-separate-envs/.gitignore diff --git a/fast/stages/02-networking-vpn/IAM.md b/fast/stages/2-networking-d-separate-envs/IAM.md similarity index 100% rename from fast/stages/02-networking-vpn/IAM.md rename to fast/stages/2-networking-d-separate-envs/IAM.md diff --git a/fast/stages/02-networking-separate-envs/README.md b/fast/stages/2-networking-d-separate-envs/README.md similarity index 65% rename from fast/stages/02-networking-separate-envs/README.md rename to fast/stages/2-networking-d-separate-envs/README.md index 66b31646e..dfc199cd0 100644 --- a/fast/stages/02-networking-separate-envs/README.md +++ b/fast/stages/2-networking-d-separate-envs/README.md @@ -1,4 +1,4 @@ -# Networking +# Networking with separated single environment This stage sets up the shared network infrastructure for the whole organization. It implements a single shared VPC per environment, where each environment is independently connected to the on-premise environment, to maintain a fully separated routing domain on GCP. @@ -14,6 +14,31 @@ The following diagram illustrates the high-level design, and should be used as a Networking diagram

+## Table of contents + +- [Design overview and choices](#design-overview-and-choices) + - [VPC design](#vpc-design) + - [External connectivity](#external-connectivity) + - [IP ranges, subnetting, routing](#ip-ranges-subnetting-routing) + - [Internet egress](#internet-egress) + - [VPC and Hierarchical Firewall](#vpc-and-hierarchical-firewall) + - [DNS](#dns) +- [Stage structure and files layout](#stage-structure-and-files-layout) + - [VPCs](#vpcs) + - [VPNs](#vpns) + - [Routing and BGP](#routing-and-bgp) + - [Firewall](#firewall) + - [DNS architecture](#dns-architecture) + - [Private Google Access](#private-google-access) +- [How to run this stage](#how-to-run-this-stage) + - [Provider and Terraform variables](#provider-and-terraform-variables) + - [Impersonating the automation service account](#impersonating-the-automation-service-account) + - [Variable configuration](#variable-configuration) + - [Running the stage](#running-the-stage) + - [Post-deployment activities](#post-deployment-activities) +- [Customizations](#customizations) + - [Changing default regions](#changing-default-regions) + ## Design overview and choices ### VPC design @@ -87,57 +112,7 @@ From cloud, the `example.com` domain (used as a placeholder) is forwarded to on- This configuration is battle-tested, and flexible enough to lend itself to simple modifications without subverting its design, for example by forwarding and peering root zones to bypass Cloud DNS external resolution. -## How to run this stage - -This stage is meant to be executed after the [resman](../01-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../00-bootstrap) stage. - -It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. - -Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. - -### Providers configuration - -The default way of making sure you have the right permissions, is to use the identity of the service account pre-created for this stage during the [resource management](../01-resman) stage, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). - -To simplify setup, the previous stage pre-configures a valid providers file in its output, and optionally writes it to a local file if the `outputs_location` variable is set to a valid path. - -If you have set a valid value for `outputs_location` in the bootstrap stage, simply link the relevant `providers.tf` file from this stage's folder in the path you specified: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/providers/02-networking-providers.tf . -``` - -If you have not configured `outputs_location` in bootstrap, you can derive the providers file from that stage's outputs: - -```bash -cd ../01-resman -terraform output -json providers | jq -r '.["02-networking"]' \ - > ../02-networking/providers.tf -``` - -### Variable configuration - -There are two broad sets of variables you will need to fill in: - -- variables shared by other stages (org id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) -- variables specific to resources managed by this stage - -To avoid the tedious job of filling in the first group of variables with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. - -If you have set a valid value for `outputs_location` in the bootstrap and in the resman stage, simply link the relevant `terraform-*.auto.tfvars.json` files from this stage's folder in the path you specified, where the `*` above is set to the name of the stage that produced it. For this stage, a single `.tfvars` file is available: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ../../configs/example/02-networking/terraform-bootstrap.auto.tfvars.json -ln -s ../../configs/example/02-networking/terraform-resman.auto.tfvars.json -# also copy the tfvars file used for the bootstrap stage -cp ../00-bootstrap/terraform.tfvars . -``` - -A second set of variables is specific to this stage, they are all optional so if you need to customize them, add them to the file copied from bootstrap. - -Please refer to the [Variables](#variables) table below for a map of the variable origins, and to the sections below on how to adapt this stage to your networking configuration. +## Stage structure and files layout ### VPCs @@ -187,7 +162,72 @@ When implementing this architecture, make sure you'll be able to route packets c The [Inbound DNS Policy](https://cloud.google.com/dns/docs/server-policies-overview#dns-server-policy-in) defined on eachVPC automatically reserves the first available IP address on each created subnet (typically the third one in a CIDR) to expose the Cloud DNS service so that it can be consumed from outside of GCP. -### Private Google Access +## How to run this stage + +This stage is meant to be executed after the [resource management](../1-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../0-bootstrap) stage. + +It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. + +Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. + +### Provider and Terraform variables + +As all other FAST stages, the [mechanism used to pass variable values and pre-built provider files from one stage to the next](../0-bootstrap/README.md#output-files-and-cross-stage-variables) is also leveraged here. + +The commands to link or copy the provider and terraform variable files can be easily derived from the `stage-links.sh` script in the FAST root folder, passing it a single argument with the local output files folder (if configured) or the GCS output bucket in the automation project (derived from stage 0 outputs). The following examples demonstrate both cases, and the resulting commands that then need to be copy/pasted and run. + +```bash +../../stage-links.sh ~/fast-config + +# copy and paste the following commands for '2-networking-a-peering' + +ln -s ~/fast-config/providers/2-networking-providers.tf ./ +ln -s ~/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json ./ +``` + +```bash +../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '2-networking-a-peering' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/2-networking-providers.tf ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/globals.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/1-resman.auto.tfvars.json ./ +``` + +### Impersonating the automation service account + +The preconfigured provider file uses impersonation to run with this stage's automation service account's credentials. The `gcp-devops` and `organization-admins` groups have the necessary IAM bindings in place to do that, so make sure the current user is a member of one of those groups. + +### Variable configuration + +Variables in this stage -- like most other FAST stages -- are broadly divided into three separate sets: + +- variables which refer to global values for the whole organization (org id, billing account id, prefix, etc.), which are pre-populated via the `globals.auto.tfvars.json` file linked or copied above +- variables which refer to resources managed by previous stage, which are prepopulated here via the `0-bootstrap.auto.tfvars.json` and `1-resman.auto.tfvars.json` files linked or copied above +- and finally variables that optionally control this stage's behaviour and customizations, and can to be set in a custom `terraform.tfvars` file + +The latter set is explained in the [Customization](#customizations) sections below, and the full list can be found in the [Variables](#variables) table at the bottom of this document. + +### Running the stage + +Once provider and variable values are in place and the correct user is configured, the stage can be run: + +```bash +terraform init +terraform apply +``` + +### Post-deployment activities + +- On-prem routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recomment aggregating routes as much as possible. +- On-prem routers should accept BGP sessions from their cloud peers. +- On-prem DNS servers should have forward zones for GCP-managed ones. + +#### Private Google Access [Private Google Access](https://cloud.google.com/vpc/docs/private-google-access) (or PGA) enables VMs and on-prem systems to consume Google APIs from within the Google network, and is already fully configured on this environment. @@ -199,21 +239,16 @@ Subnets created by the `net-vpc` module are PGA-enabled by default. - 199.36.153.4/30 (`restricted.googleapis.com`) and 199.36.153.8/30 (`private.googleapis.com`) should be routed from on-prem to VPC, and from there to the `default-internet-gateway`. \ Per variable `vpn_onprem_configs` such ranges are advertised to onprem - furthermore every VPC has explicit routes set in case the `0.0.0.0/0` route is changed. -- A private DNS zone for `googleapis.com` should be created and configured per [this article](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid#config-domain), as implemented in module `googleapis-private-zone` in `dns-xxx.tf` +- A private DNS zone for `googleapis.com` should be created and configured per [this article](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid#config-domain) -### Preliminar activities +## Customizations -Before running `terraform apply` on this stage, make sure to adapt all of `variables.tf` to your needs, to update all reference to regions (e.g. `europe-west1` or `ew1`) in the whole directory to match your preferences. +### Changing default regions -If you're not using FAST, you'll also need to create a `providers.tf` file to configure the GCS backend and the service account to use to run the deployment. +Regions are defined via the `regions` variable which sets up a mapping between the `regions.primary` and `regions.secondary` logical names and actual GCP region names. If you need to change regions from the defaults: -You're now ready to run `terraform init` and `apply`. - -### Post-deployment activities - -- On-prem routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recomment aggregating routes as much as possible. -- On-prem routers should accept BGP sessions from their cloud peers. -- On-prem DNS servers should have forward zones for GCP-managed ones. +- change the values of the mappings in the `regions` variable to the regions you are going to use +- change the regions in the factory subnet files in the `data` folder @@ -227,6 +262,7 @@ You're now ready to run `terraform init` and `apply`. | [main.tf](./main.tf) | Networking folder and hierarchical policy. | folder | | | [monitoring.tf](./monitoring.tf) | Network monitoring dashboards. | | google_monitoring_dashboard | | [outputs.tf](./outputs.tf) | Module outputs. | | google_storage_bucket_object · local_file | +| [regions.tf](./regions.tf) | Compute short names for regions. | | | | [spoke-dev.tf](./spoke-dev.tf) | Dev spoke VPC and related resources. | net-cloudnat · net-vpc · net-vpc-firewall · project | google_project_iam_binding | | [spoke-prod.tf](./spoke-prod.tf) | Production spoke VPC and related resources. | net-cloudnat · net-vpc · net-vpc-firewall · project | google_project_iam_binding | | [test-resources.tf](./test-resources.tf) | Temporary instances for testing | compute-vm | | @@ -238,21 +274,22 @@ You're now ready to run `terraform init` and `apply`. | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | -| [billing_account](variables.tf#L25) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [folder_ids](variables.tf#L74) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 01-resman | -| [organization](variables.tf#L102) | Organization details. | object({…}) | ✓ | | 00-bootstrap | -| [prefix](variables.tf#L118) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | -| [custom_adv](variables.tf#L34) | Custom advertisement definitions in name => range format. | map(string) | | {…} | | -| [custom_roles](variables.tf#L50) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 00-bootstrap | -| [data_dir](variables.tf#L59) | Relative path for the folder storing configuration data for network resources. | string | | "data" | | -| [dns](variables.tf#L65) | Onprem DNS resolvers. | map(list(string)) | | {…} | | -| [l7ilb_subnets](variables.tf#L84) | Subnets used for L7 ILBs. | map(list(object({…}))) | | {…} | | -| [outputs_location](variables.tf#L112) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | -| [psa_ranges](variables.tf#L129) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | -| [router_onprem_configs](variables.tf#L166) | Configurations for routers used for onprem connectivity. | map(object({…})) | | {…} | | -| [service_accounts](variables.tf#L189) | Automation service accounts in name => email format. | object({…}) | | null | 01-resman | -| [vpn_onprem_configs](variables.tf#L201) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | +| [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#L92) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman | +| [organization](variables.tf#L118) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L134) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [custom_adv](variables.tf#L38) | Custom advertisement definitions in name => range format. | map(string) | | {…} | | +| [custom_roles](variables.tf#L54) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | +| [dns](variables.tf#L63) | Onprem DNS resolvers. | map(list(string)) | | {…} | | +| [factories_config](variables.tf#L72) | Configuration for network resource factories. | object({…}) | | {…} | | +| [l7ilb_subnets](variables.tf#L102) | Subnets used for L7 ILBs. | map(list(object({…}))) | | {…} | | +| [outputs_location](variables.tf#L128) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | +| [psa_ranges](variables.tf#L145) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…}) | | null | | +| [regions](variables.tf#L182) | Region definitions. | object({…}) | | {…} | | +| [router_onprem_configs](variables.tf#L192) | Configurations for routers used for onprem connectivity. | map(object({…})) | | {…} | | +| [service_accounts](variables.tf#L215) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | +| [vpn_onprem_configs](variables.tf#L227) | VPN gateway configuration for onprem interconnection. | map(object({…})) | | {…} | | ## Outputs diff --git a/fast/stages/02-networking-vpn/data/cidrs.yaml b/fast/stages/2-networking-d-separate-envs/data/cidrs.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/cidrs.yaml rename to fast/stages/2-networking-d-separate-envs/data/cidrs.yaml diff --git a/fast/stages/02-networking-vpn/data/dashboards/firewall_insights.json b/fast/stages/2-networking-d-separate-envs/data/dashboards/firewall_insights.json similarity index 100% rename from fast/stages/02-networking-vpn/data/dashboards/firewall_insights.json rename to fast/stages/2-networking-d-separate-envs/data/dashboards/firewall_insights.json diff --git a/fast/stages/02-networking-vpn/data/dashboards/vpn.json b/fast/stages/2-networking-d-separate-envs/data/dashboards/vpn.json similarity index 100% rename from fast/stages/02-networking-vpn/data/dashboards/vpn.json rename to fast/stages/2-networking-d-separate-envs/data/dashboards/vpn.json diff --git a/fast/stages/02-networking-separate-envs/data/firewall-rules/dev/rules.yaml b/fast/stages/2-networking-d-separate-envs/data/firewall-rules/dev/rules.yaml similarity index 100% rename from fast/stages/02-networking-separate-envs/data/firewall-rules/dev/rules.yaml rename to fast/stages/2-networking-d-separate-envs/data/firewall-rules/dev/rules.yaml diff --git a/fast/stages/02-networking-vpn/data/hierarchical-policy-rules.yaml b/fast/stages/2-networking-d-separate-envs/data/hierarchical-policy-rules.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/hierarchical-policy-rules.yaml rename to fast/stages/2-networking-d-separate-envs/data/hierarchical-policy-rules.yaml diff --git a/fast/stages/02-networking-vpn/data/subnets/dev/dev-dataplatform-ew1.yaml b/fast/stages/2-networking-d-separate-envs/data/subnets/dev/dev-dataplatform-ew1.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/subnets/dev/dev-dataplatform-ew1.yaml rename to fast/stages/2-networking-d-separate-envs/data/subnets/dev/dev-dataplatform-ew1.yaml diff --git a/fast/stages/02-networking-vpn/data/subnets/dev/dev-default-ew1.yaml b/fast/stages/2-networking-d-separate-envs/data/subnets/dev/dev-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/subnets/dev/dev-default-ew1.yaml rename to fast/stages/2-networking-d-separate-envs/data/subnets/dev/dev-default-ew1.yaml diff --git a/fast/stages/02-networking-vpn/data/subnets/prod/prod-default-ew1.yaml b/fast/stages/2-networking-d-separate-envs/data/subnets/prod/prod-default-ew1.yaml similarity index 100% rename from fast/stages/02-networking-vpn/data/subnets/prod/prod-default-ew1.yaml rename to fast/stages/2-networking-d-separate-envs/data/subnets/prod/prod-default-ew1.yaml diff --git a/fast/stages/02-networking-separate-envs/diagram.png b/fast/stages/2-networking-d-separate-envs/diagram.png similarity index 100% rename from fast/stages/02-networking-separate-envs/diagram.png rename to fast/stages/2-networking-d-separate-envs/diagram.png diff --git a/fast/stages/02-networking-separate-envs/diagram.svg b/fast/stages/2-networking-d-separate-envs/diagram.svg similarity index 100% rename from fast/stages/02-networking-separate-envs/diagram.svg rename to fast/stages/2-networking-d-separate-envs/diagram.svg diff --git a/fast/stages/02-networking-separate-envs/dns-dev.tf b/fast/stages/2-networking-d-separate-envs/dns-dev.tf similarity index 100% rename from fast/stages/02-networking-separate-envs/dns-dev.tf rename to fast/stages/2-networking-d-separate-envs/dns-dev.tf diff --git a/fast/stages/02-networking-separate-envs/dns-prod.tf b/fast/stages/2-networking-d-separate-envs/dns-prod.tf similarity index 100% rename from fast/stages/02-networking-separate-envs/dns-prod.tf rename to fast/stages/2-networking-d-separate-envs/dns-prod.tf diff --git a/fast/stages/02-networking-separate-envs/main.tf b/fast/stages/2-networking-d-separate-envs/main.tf similarity index 65% rename from fast/stages/02-networking-separate-envs/main.tf rename to fast/stages/2-networking-d-separate-envs/main.tf index 0b901e330..dda1c252e 100644 --- a/fast/stages/02-networking-separate-envs/main.tf +++ b/fast/stages/2-networking-d-separate-envs/main.tf @@ -18,17 +18,25 @@ locals { custom_roles = coalesce(var.custom_roles, {}) - l7ilb_subnets = { - for env, v in var.l7ilb_subnets : env => [ + _l7ilb_subnets = { + for k, v in var.l7ilb_subnets : k => [ for s in v : merge(s, { active = true - name = "${env}-l7ilb-${s.region}" + region = lookup(var.regions, s.region, s.region) })] } - region_trigram = { - europe-west1 = "ew1" - europe-west3 = "ew3" + l7ilb_subnets = { + for k, v in local._l7ilb_subnets : k => [ + for s in v : merge(s, { + name = "${k}-l7ilb-${local.region_shortnames[s.region]}" + })] } + # combine all regions from variables and subnets + regions = distinct(concat( + values(var.regions), + values(module.dev-spoke-vpc.subnet_regions), + values(module.prod-spoke-vpc.subnet_regions), + )) stage3_sas_delegated_grants = [ "roles/composer.sharedVpcAgent", "roles/compute.networkUser", @@ -47,12 +55,12 @@ module "folder" { folder_create = var.folder_ids.networking == null id = var.folder_ids.networking firewall_policy_factory = { - cidr_file = "${var.data_dir}/cidrs.yaml" - policy_name = null - rules_file = "${var.data_dir}/hierarchical-policy-rules.yaml" + cidr_file = "${var.factories_config.data_dir}/cidrs.yaml" + policy_name = var.factories_config.firewall_policy_name + rules_file = "${var.factories_config.data_dir}/hierarchical-policy-rules.yaml" } firewall_policy_association = { - factory-policy = "factory" + factory-policy = var.factories_config.firewall_policy_name } } diff --git a/fast/stages/02-networking-separate-envs/monitoring.tf b/fast/stages/2-networking-d-separate-envs/monitoring.tf similarity index 94% rename from fast/stages/02-networking-separate-envs/monitoring.tf rename to fast/stages/2-networking-d-separate-envs/monitoring.tf index 463c6a083..01ed0c479 100644 --- a/fast/stages/02-networking-separate-envs/monitoring.tf +++ b/fast/stages/2-networking-d-separate-envs/monitoring.tf @@ -17,7 +17,7 @@ # tfdoc:file:description Network monitoring dashboards. locals { - dashboard_path = "${var.data_dir}/dashboards" + dashboard_path = "${var.factories_config.data_dir}/dashboards" dashboard_files = fileset(local.dashboard_path, "*.json") dashboards = { for filename in local.dashboard_files : diff --git a/fast/stages/02-networking-separate-envs/outputs.tf b/fast/stages/2-networking-d-separate-envs/outputs.tf similarity index 90% rename from fast/stages/02-networking-separate-envs/outputs.tf rename to fast/stages/2-networking-d-separate-envs/outputs.tf index d06d499d6..59d70db6d 100644 --- a/fast/stages/02-networking-separate-envs/outputs.tf +++ b/fast/stages/2-networking-d-separate-envs/outputs.tf @@ -44,13 +44,13 @@ locals { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/02-networking.auto.tfvars.json" + filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket - name = "tfvars/02-networking.auto.tfvars.json" + name = "tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } @@ -90,12 +90,12 @@ output "tfvars" { output "vpn_gateway_endpoints" { description = "External IP Addresses for the GCP VPN gateways." value = local.enable_onprem_vpn == false ? null : { - dev-onprem-ew1 = { - for v in module.dev-to-onprem-ew1-vpn[0].gateway.vpn_interfaces : + dev-onprem-primary = { + for v in module.dev-to-onprem-primary-vpn[0].gateway.vpn_interfaces : v.id => v.ip_address } - prod-onprem-ew1 = { - for v in module.prod-to-onprem-ew1-vpn[0].gateway.vpn_interfaces : + prod-onprem-primary = { + for v in module.prod-to-onprem-primary-vpn[0].gateway.vpn_interfaces : v.id => v.ip_address } } diff --git a/fast/stages/2-networking-d-separate-envs/regions.tf b/fast/stages/2-networking-d-separate-envs/regions.tf new file mode 100644 index 000000000..53514afa9 --- /dev/null +++ b/fast/stages/2-networking-d-separate-envs/regions.tf @@ -0,0 +1,42 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +# tfdoc:file:description Compute short names for regions. + +locals { + # only map when the first character would not work + _region_cardinal = { + southeast = "se" + } + # only map when the first character would not work + _region_geo = { + australia = "o" + } + # split in [geo, cardinal, number] tokens + _region_tokens = { + for v in local.regions : v => regexall("(?:[a-z]+)|(?:[0-9]+)", v) + } + region_shortnames = { + for k, v in local._region_tokens : k => join("", [ + # first token via geo alias map or first character + lookup(local._region_geo, v.0, substr(v.0, 0, 1)), + # first token via cardinal alias map or first character + lookup(local._region_cardinal, v.1, substr(v.1, 0, 1)), + # region number as is + v.2 + ]) + } +} diff --git a/fast/stages/02-networking-separate-envs/spoke-dev.tf b/fast/stages/2-networking-d-separate-envs/spoke-dev.tf similarity index 92% rename from fast/stages/02-networking-separate-envs/spoke-dev.tf rename to fast/stages/2-networking-d-separate-envs/spoke-dev.tf index ca7d8d468..ecd0b0736 100644 --- a/fast/stages/02-networking-separate-envs/spoke-dev.tf +++ b/fast/stages/2-networking-d-separate-envs/spoke-dev.tf @@ -47,7 +47,7 @@ module "dev-spoke-vpc" { project_id = module.dev-spoke-project.project_id name = "dev-spoke-0" mtu = 1500 - data_folder = "${var.data_dir}/subnets/dev" + data_folder = "${var.factories_config.data_dir}/subnets/dev" psa_config = try(var.psa_ranges.dev, null) subnets_proxy_only = local.l7ilb_subnets.dev # set explicit routes for googleapis in case the default route is deleted @@ -73,8 +73,8 @@ module "dev-spoke-firewall" { disabled = true } factories_config = { - cidr_tpl_file = "${var.data_dir}/cidrs.yaml" - rules_folder = "${var.data_dir}/firewall-rules/dev" + cidr_tpl_file = "${var.factories_config.data_dir}/cidrs.yaml" + rules_folder = "${var.factories_config.data_dir}/firewall-rules/dev" } } @@ -83,7 +83,7 @@ module "dev-spoke-cloudnat" { source = "../../../modules/net-cloudnat" project_id = module.dev-spoke-project.project_id region = each.value - name = "dev-nat-${local.region_trigram[each.value]}" + name = "dev-nat-${local.region_shortnames[each.value]}" router_create = true router_network = module.dev-spoke-vpc.name router_asn = 4200001024 diff --git a/fast/stages/02-networking-separate-envs/spoke-prod.tf b/fast/stages/2-networking-d-separate-envs/spoke-prod.tf similarity index 92% rename from fast/stages/02-networking-separate-envs/spoke-prod.tf rename to fast/stages/2-networking-d-separate-envs/spoke-prod.tf index eba530a6c..2e95a09e1 100644 --- a/fast/stages/02-networking-separate-envs/spoke-prod.tf +++ b/fast/stages/2-networking-d-separate-envs/spoke-prod.tf @@ -47,7 +47,7 @@ module "prod-spoke-vpc" { project_id = module.prod-spoke-project.project_id name = "prod-spoke-0" mtu = 1500 - data_folder = "${var.data_dir}/subnets/prod" + data_folder = "${var.factories_config.data_dir}/subnets/prod" psa_config = try(var.psa_ranges.prod, null) subnets_proxy_only = local.l7ilb_subnets.prod # set explicit routes for googleapis in case the default route is deleted @@ -73,8 +73,8 @@ module "prod-spoke-firewall" { disabled = true } factories_config = { - cidr_tpl_file = "${var.data_dir}/cidrs.yaml" - rules_folder = "${var.data_dir}/firewall-rules/prod" + cidr_tpl_file = "${var.factories_config.data_dir}/cidrs.yaml" + rules_folder = "${var.factories_config.data_dir}/firewall-rules/prod" } } @@ -83,7 +83,7 @@ module "prod-spoke-cloudnat" { source = "../../../modules/net-cloudnat" project_id = module.prod-spoke-project.project_id region = each.value - name = "prod-nat-${local.region_trigram[each.value]}" + name = "prod-nat-${local.region_shortnames[each.value]}" router_create = true router_network = module.prod-spoke-vpc.name router_asn = 4200001024 diff --git a/fast/stages/02-networking-separate-envs/test-resources.tf b/fast/stages/2-networking-d-separate-envs/test-resources.tf similarity index 86% rename from fast/stages/02-networking-separate-envs/test-resources.tf rename to fast/stages/2-networking-d-separate-envs/test-resources.tf index ae3ba90a8..55c42c251 100644 --- a/fast/stages/02-networking-separate-envs/test-resources.tf +++ b/fast/stages/2-networking-d-separate-envs/test-resources.tf @@ -19,12 +19,12 @@ # module "test-vm-dev-0" { # source = "../../../modules/compute-vm" # project_id = module.dev-spoke-project.project_id -# zone = "europe-west1-b" +# zone = "${var.regions.primary}-b" # name = "test-vm-0" # network_interfaces = [{ # network = module.dev-spoke-vpc.self_link # # change the subnet name to match the values you are actually using -# subnetwork = module.dev-spoke-vpc.subnet_self_links["europe-west1/dev-default-ew1"] +# subnetwork = module.dev-spoke-vpc.subnet_self_links["${var.regions.primary}/dev-default-${local.region_shortnames[var.regions.primary]}"] # alias_ips = {} # nat = false # addresses = null @@ -52,12 +52,12 @@ # module "test-vm-prod-0" { # source = "../../../modules/compute-vm" # project_id = module.prod-spoke-project.project_id -# zone = "europe-west1-b" +# zone = "${var.regions.primary}-b" # name = "test-vm-0" # network_interfaces = [{ # network = module.prod-spoke-vpc.self_link # # change the subnet name to match the values you are actually using -# subnetwork = module.prod-spoke-vpc.subnet_self_links["europe-west1/prod-default-ew1"] +# subnetwork = module.prod-spoke-vpc.subnet_self_links["${var.regions.primary}/prod-default-${local.region_shortnames[var.regions.primary]}"] # alias_ips = {} # nat = false # addresses = null diff --git a/fast/stages/02-networking-separate-envs/variables.tf b/fast/stages/2-networking-d-separate-envs/variables.tf similarity index 82% rename from fast/stages/02-networking-separate-envs/variables.tf rename to fast/stages/2-networking-d-separate-envs/variables.tf index 019d0b2ed..03d565139 100644 --- a/fast/stages/02-networking-separate-envs/variables.tf +++ b/fast/stages/2-networking-d-separate-envs/variables.tf @@ -15,7 +15,7 @@ */ variable "automation" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Automation resources created by the bootstrap stage." type = object({ outputs_bucket = string @@ -23,12 +23,16 @@ variable "automation" { } variable "billing_account" { - # tfdoc:variable:source 00-bootstrap - description = "Billing account id and organization id ('nnnnnnnn' or null)." + # tfdoc:variable:source 0-bootstrap + description = "Billing account id. If billing account is not part of the same org set `is_org_level` to false." type = object({ - id = string - organization_id = number + id = string + is_org_level = optional(bool, true) }) + validation { + condition = var.billing_account.is_org_level != null + error_message = "Invalid `null` value for `billing_account.is_org_level`." + } } variable "custom_adv" { @@ -48,7 +52,7 @@ variable "custom_adv" { } variable "custom_roles" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Custom roles defined at the org level, in key => id format." type = object({ service_project_network_admin = string @@ -56,12 +60,6 @@ variable "custom_roles" { default = null } -variable "data_dir" { - description = "Relative path for the folder storing configuration data for network resources." - type = string - default = "data" -} - variable "dns" { description = "Onprem DNS resolvers." type = map(list(string)) @@ -71,8 +69,28 @@ variable "dns" { } } +variable "factories_config" { + description = "Configuration for network resource factories." + type = object({ + data_dir = optional(string, "data") + firewall_policy_name = optional(string, "factory") + }) + default = { + data_dir = "data" + } + nullable = false + validation { + condition = var.factories_config.data_dir != null + error_message = "Data folder needs to be non-null." + } + validation { + condition = var.factories_config.firewall_policy_name != null + error_message = "Firewall policy name needs to be non-null." + } +} + variable "folder_ids" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created." type = object({ networking = string @@ -90,17 +108,15 @@ variable "l7ilb_subnets" { default = { prod = [ { ip_cidr_range = "10.128.92.0/24", region = "europe-west1" }, - { ip_cidr_range = "10.128.93.0/24", region = "europe-west4" } ] dev = [ { ip_cidr_range = "10.128.60.0/24", region = "europe-west1" }, - { ip_cidr_range = "10.128.61.0/24", region = "europe-west4" } ] } } variable "organization" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Organization details." type = object({ domain = string @@ -116,7 +132,7 @@ variable "outputs_location" { } variable "prefix" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Prefix used for resources that need unique names. Use 9 characters or less." type = string @@ -163,6 +179,16 @@ variable "psa_ranges" { # } } +variable "regions" { + description = "Region definitions." + type = object({ + primary = string + }) + default = { + primary = "europe-west1" + } +} + variable "router_onprem_configs" { description = "Configurations for routers used for onprem connectivity." type = map(object({ @@ -173,12 +199,12 @@ variable "router_onprem_configs" { asn = number })) default = { - prod-ew1 = { + prod-primary = { asn = "65533" adv = null # adv = { default = false, custom = [] } } - dev-ew1 = { + dev-primary = { asn = "65534" adv = null # adv = { default = false, custom = [] } @@ -187,7 +213,7 @@ variable "router_onprem_configs" { } variable "service_accounts" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Automation service accounts in name => email format." type = object({ data-platform-dev = string @@ -218,7 +244,7 @@ variable "vpn_onprem_configs" { })) })) default = { - dev-ew1 = { + dev-primary = { adv = { default = false custom = [ @@ -247,7 +273,7 @@ variable "vpn_onprem_configs" { } ] } - prod-ew1 = { + prod-primary = { adv = { default = false custom = [ diff --git a/fast/stages/02-networking-separate-envs/vpn-onprem-dev.tf b/fast/stages/2-networking-d-separate-envs/vpn-onprem-dev.tf similarity index 78% rename from fast/stages/02-networking-separate-envs/vpn-onprem-dev.tf rename to fast/stages/2-networking-d-separate-envs/vpn-onprem-dev.tf index 1b0ff1171..e95cd14f9 100644 --- a/fast/stages/02-networking-separate-envs/vpn-onprem-dev.tf +++ b/fast/stages/2-networking-d-separate-envs/vpn-onprem-dev.tf @@ -32,28 +32,33 @@ locals { } } -module "dev-to-onprem-ew1-vpn" { +moved { + from = module.dev-to-onprem-ew1-vpn + to = module.dev-to-onprem-primary-vpn +} + +module "dev-to-onprem-primary-vpn" { count = local.enable_onprem_vpn ? 1 : 0 source = "../../../modules/net-vpn-ha" project_id = module.dev-spoke-project.project_id network = module.dev-spoke-vpc.self_link - region = "europe-west1" - name = "vpn-to-onprem-ew1" + region = var.regions.primary + name = "vpn-to-onprem-${local.region_shortnames[var.regions.primary]}" router_config = { - name = "dev-onprem-vpn-ew1" - asn = var.router_onprem_configs.dev-ew1.asn + name = "dev-onprem-vpn-${local.region_shortnames[var.regions.primary]}" + asn = var.router_onprem_configs.dev-primary.asn } peer_gateway = { - external = var.vpn_onprem_configs.dev-ew1.peer_external_gateway + external = var.vpn_onprem_configs.dev-primary.peer_external_gateway } tunnels = { - for t in var.vpn_onprem_configs.dev-ew1.tunnels : + for t in var.vpn_onprem_configs.dev-primary.tunnels : "remote-${t.vpn_gateway_interface}-${t.peer_external_gateway_interface}" => { bgp_peer = { address = cidrhost(t.session_range, 1) asn = t.peer_asn } - bgp_peer_options = local.bgp_peer_options_onprem.dev-ew1 + bgp_peer_options = local.bgp_peer_options_onprem.dev-primary bgp_session_range = "${cidrhost(t.session_range, 2)}/30" peer_external_gateway_interface = t.peer_external_gateway_interface shared_secret = t.secret diff --git a/fast/stages/02-networking-separate-envs/vpn-onprem-prod.tf b/fast/stages/2-networking-d-separate-envs/vpn-onprem-prod.tf similarity index 73% rename from fast/stages/02-networking-separate-envs/vpn-onprem-prod.tf rename to fast/stages/2-networking-d-separate-envs/vpn-onprem-prod.tf index d4b2af24e..0793e2744 100644 --- a/fast/stages/02-networking-separate-envs/vpn-onprem-prod.tf +++ b/fast/stages/2-networking-d-separate-envs/vpn-onprem-prod.tf @@ -16,28 +16,33 @@ # tfdoc:file:description VPN between prod and onprem. -module "prod-to-onprem-ew1-vpn" { +moved { + from = module.prod-to-onprem-ew1-vpn + to = module.prod-to-onprem-primary-vpn +} + +module "prod-to-onprem-primary-vpn" { count = local.enable_onprem_vpn ? 1 : 0 source = "../../../modules/net-vpn-ha" project_id = module.prod-spoke-project.project_id network = module.prod-spoke-vpc.self_link - region = "europe-west1" - name = "vpn-to-onprem-ew1" + region = var.regions.primary + name = "vpn-to-onprem-${local.region_shortnames[var.regions.primary]}" router_config = { - name = "prod-onprem-vpn-ew1" - asn = var.router_onprem_configs.prod-ew1.asn + name = "prod-onprem-vpn-${local.region_shortnames[var.regions.primary]}" + asn = var.router_onprem_configs.prod-primary.asn } peer_gateway = { - external = var.vpn_onprem_configs.prod-ew1.peer_external_gateway + external = var.vpn_onprem_configs.prod-primary.peer_external_gateway } tunnels = { - for t in var.vpn_onprem_configs.prod-ew1.tunnels : + for t in var.vpn_onprem_configs.prod-primary.tunnels : "remote-${t.vpn_gateway_interface}-${t.peer_external_gateway_interface}" => { bgp_peer = { address = cidrhost(t.session_range, 1) asn = t.peer_asn } - bgp_peer_options = local.bgp_peer_options_onprem.prod-ew1 + bgp_peer_options = local.bgp_peer_options_onprem.prod-primary bgp_session_range = "${cidrhost(t.session_range, 2)}/30" peer_external_gateway_interface = t.peer_external_gateway_interface shared_secret = t.secret diff --git a/fast/stages/02-security/IAM.md b/fast/stages/2-security/IAM.md similarity index 100% rename from fast/stages/02-security/IAM.md rename to fast/stages/2-security/IAM.md diff --git a/fast/stages/02-security/README.md b/fast/stages/2-security/README.md similarity index 74% rename from fast/stages/02-security/README.md rename to fast/stages/2-security/README.md index 026ef7717..6486cd741 100644 --- a/fast/stages/02-security/README.md +++ b/fast/stages/2-security/README.md @@ -12,6 +12,24 @@ The following diagram illustrates the high-level design of created resources and Security diagram

+## Table of contents + +- [Design overview and choices](#design-overview-and-choices) + - [Cloud KMS](#cloud-kms) + - [VPC Service Controls](#vpc-service-controls) +- [How to run this stage](#how-to-run-this-stage) + - [Provider and Terraform variables](#provider-and-terraform-variables) + - [Impersonating the automation service account](#impersonating-the-automation-service-account) + - [Variable configuration](#variable-configuration) + - [Running the stage](#running-the-stage) +- [Customizations](#customizations) + - [KMS keys](#kms-keys) + - [VPC Service Controls configuration](#vpc-service-controls-configuration) + - [Dry-run vs. enforced](#dry-run-vs-enforced) + - [Access levels](#access-levels) + - [Ingress and Egress policies](#ingress-and-egress-policies) + - [Perimeters](#perimeters) + ## Design overview and choices Project-level security resources are grouped into two separate projects, one per environment. This setup matches requirements we frequently observe in real life and provides enough separation without needlessly complicating operations. @@ -42,57 +60,57 @@ Some care needs to be taken with project membership in perimeters, which can onl ## How to run this stage -This stage is meant to be executed after the [resource management](../01-resman) stage has run, as it leverages the folder and automation resources created there. The relevant user groups must also exist, but that's one of the requirements for the previous stages too, so if you ran those successfully, you're good to go. +This stage is meant to be executed after the [resource management](../1-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../0-bootstrap) stage. -It's possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the bootstrap stage for the required roles. +It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. -Before running this stage, you need to ensure you have the correct credentials and permissions, and customize variables by assigning values that match your configuration. +Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. -### Providers configuration +### Provider and Terraform variables -The default way of making sure you have the correct permissions is to use the identity of the service account pre-created for this stage during bootstrap, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). +As all other FAST stages, the [mechanism used to pass variable values and pre-built provider files from one stage to the next](../0-bootstrap/README.md#output-files-and-cross-stage-variables) is also leveraged here. -To simplify setup, the previous stage pre-configures a valid providers file in its output, and optionally writes it to a local file if the `outputs_location` variable is set to a valid path. - -If you have set a valid value for `outputs_location` in the resource management stage, simply link the relevant `providers.tf` file from this stage's folder in the path you specified: +The commands to link or copy the provider and terraform variable files can be easily derived from the `stage-links.sh` script in the FAST root folder, passing it a single argument with the local output files folder (if configured) or the GCS output bucket in the automation project (derived from stage 0 outputs). The following examples demonstrate both cases, and the resulting commands that then need to be copy/pasted and run. ```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/providers/02-security-providers.tf . -``` +../../stage-links.sh ~/fast-config -If you have not configured `outputs_location` in resource management, you can derive the providers file from that stage's outputs: +# copy and paste the following commands for '2-security' + +ln -s ~/fast-config/providers/2-security-providers.tf ./ +ln -s ~/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json ./ +``` ```bash -cd ../01-resman -terraform output -json providers | jq -r '.["02-security"]' \ - > ../02-security/providers.tf +../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '2-security' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/2-security-providers.tf ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/globals.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/1-resman.auto.tfvars.json ./ ``` +### Impersonating the automation service account + +The preconfigured provider file uses impersonation to run with this stage's automation service account's credentials. The `gcp-devops` and `organization-admins` groups have the necessary IAM bindings in place to do that, so make sure the current user is a member of one of those groups. + ### Variable configuration -There are two broad sets of variables you will need to fill in: +Variables in this stage -- like most other FAST stages -- are broadly divided into three separate sets: -- variables shared by other stages (organization id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) -- variables specific to resources managed by this stage +- variables which refer to global values for the whole organization (org id, billing account id, prefix, etc.), which are pre-populated via the `globals.auto.tfvars.json` file linked or copied above +- variables which refer to resources managed by previous stage, which are prepopulated here via the `0-bootstrap.auto.tfvars.json` and `1-resman.auto.tfvars.json` files linked or copied above +- and finally variables that optionally control this stage's behaviour and customizations, and can to be set in a custom `terraform.tfvars` file -To avoid the tedious job of filling in the first group of variables with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. +The latter set is explained in the [Customization](#customizations) sections below, and the full list can be found in the [Variables](#variables) table at the bottom of this document. -If you configured a valid path for `outputs_location` in the previous stages, simply link the relevant `terraform-*.auto.tfvars.json` files from this stage's output folder (under the path you specified), where the `*` above is set to the name of the stage that produced it. For this stage, two `.tfvars` files are available: +### Running the stage -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . -# also copy the tfvars file used for the bootstrap stage -cp ../00-bootstrap/terraform.tfvars . -``` - -A second set of optional variables is specific to this stage. If you need to customize them add them to the file copied from bootstrap. - -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. The sections below also describe some of the possible customizations. - -Once done, you can run this stage: +Once provider and variable values are in place and the correct user is configured, the stage can be run: ```bash terraform init @@ -114,7 +132,7 @@ To support these scenarios, key IAM bindings are configured by default to be add An example of how to configure keys: -```hcl +```tfvars # terraform.tfvars kms_defaults = { @@ -128,14 +146,14 @@ kms_keys = { "user:user1@example.com" ] } - labels = { service = "compute" } - locations = null + labels = { service = "compute" } + locations = null rotation_period = null } storage = { - iam = null - labels = { service = "compute" } - locations = ["europe"] + iam = null + labels = { service = "compute" } + locations = ["europe"] rotation_period = null } } @@ -162,7 +180,7 @@ The VPC SC configuration is set up by default in dry-run mode to allow easy expe Access levels are defined via the `vpc_sc_access_levels` variable, and referenced by key in perimeter definitions: -```hcl +```tfvars vpc_sc_access_levels = { onprem = { conditions = [{ @@ -176,7 +194,7 @@ vpc_sc_access_levels = { Ingress and egress policy are defined via the `vpc_sc_egress_policies` and `vpc_sc_ingress_policies`, and referenced by key in perimeter definitions: -```hcl +```tfvars vpc_sc_egress_policies = { iac-gcs = { from = { @@ -187,7 +205,7 @@ vpc_sc_egress_policies = { to = { operations = [{ method_selectors = ["*"] - service_name = "storage.googleapis.com" + service_name = "storage.googleapis.com" }] resources = ["projects/123456782"] } @@ -217,7 +235,7 @@ Support for independently adding projects to perimeters outside of this Terrafor Access levels and egress/ingress policies are referenced in perimeters via keys. -```hcl +```tfvars vpc_sc_perimeters = { dev = { egress_policies = ["iac-gcs"] @@ -262,20 +280,20 @@ Some references that might be useful in setting up this stage: | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | -| [billing_account](variables.tf#L25) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [folder_ids](variables.tf#L34) | Folder name => id mappings, the 'security' folder name must exist. | object({…}) | ✓ | | 01-resman | -| [organization](variables.tf#L80) | Organization details. | object({…}) | ✓ | | 00-bootstrap | -| [prefix](variables.tf#L96) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | -| [service_accounts](variables.tf#L107) | Automation service accounts that can assign the encrypt/decrypt roles on keys. | object({…}) | ✓ | | 01-resman | -| [groups](variables.tf#L42) | Group names to grant organization-level permissions. | map(string) | | {…} | 00-bootstrap | -| [kms_defaults](variables.tf#L57) | Defaults used for KMS keys. | object({…}) | | {…} | | -| [kms_keys](variables.tf#L69) | KMS keys to create, keyed by name. Null attributes will be interpolated with defaults. | map(object({…})) | | {} | | -| [outputs_location](variables.tf#L90) | 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#L118) | VPC SC access level definitions. | map(object({…})) | | {} | | -| [vpc_sc_egress_policies](variables.tf#L147) | VPC SC egress policy defnitions. | map(object({…})) | | {} | | -| [vpc_sc_ingress_policies](variables.tf#L167) | VPC SC ingress policy defnitions. | map(object({…})) | | {} | | -| [vpc_sc_perimeters](variables.tf#L188) | VPC SC regular perimeter definitions. | object({…}) | | {} | | +| [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 | +| [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 defnitions. | map(object({…})) | | {} | | +| [vpc_sc_ingress_policies](variables.tf#L171) | VPC SC ingress policy defnitions. | map(object({…})) | | {} | | +| [vpc_sc_perimeters](variables.tf#L192) | VPC SC regular perimeter definitions. | object({…}) | | {} | | ## Outputs diff --git a/fast/stages/02-security/core-dev.tf b/fast/stages/2-security/core-dev.tf similarity index 100% rename from fast/stages/02-security/core-dev.tf rename to fast/stages/2-security/core-dev.tf diff --git a/fast/stages/02-security/core-prod.tf b/fast/stages/2-security/core-prod.tf similarity index 100% rename from fast/stages/02-security/core-prod.tf rename to fast/stages/2-security/core-prod.tf diff --git a/fast/stages/02-security/diagram.png b/fast/stages/2-security/diagram.png similarity index 100% rename from fast/stages/02-security/diagram.png rename to fast/stages/2-security/diagram.png diff --git a/fast/stages/02-security/diagram.svg b/fast/stages/2-security/diagram.svg similarity index 100% rename from fast/stages/02-security/diagram.svg rename to fast/stages/2-security/diagram.svg diff --git a/fast/stages/02-security/main.tf b/fast/stages/2-security/main.tf similarity index 100% rename from fast/stages/02-security/main.tf rename to fast/stages/2-security/main.tf diff --git a/fast/stages/02-security/outputs.tf b/fast/stages/2-security/outputs.tf similarity index 96% rename from fast/stages/02-security/outputs.tf rename to fast/stages/2-security/outputs.tf index b7e42e492..ff0c13eda 100644 --- a/fast/stages/02-security/outputs.tf +++ b/fast/stages/2-security/outputs.tf @@ -44,13 +44,13 @@ locals { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${pathexpand(var.outputs_location)}/tfvars/02-security.auto.tfvars.json" + filename = "${pathexpand(var.outputs_location)}/tfvars/2-security.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket - name = "tfvars/02-security.auto.tfvars.json" + name = "tfvars/2-security.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/02-security/variables.tf b/fast/stages/2-security/variables.tf similarity index 91% rename from fast/stages/02-security/variables.tf rename to fast/stages/2-security/variables.tf index 349589c96..e14d63763 100644 --- a/fast/stages/02-security/variables.tf +++ b/fast/stages/2-security/variables.tf @@ -15,7 +15,7 @@ */ variable "automation" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Automation resources created by the bootstrap stage." type = object({ outputs_bucket = string @@ -23,16 +23,20 @@ variable "automation" { } variable "billing_account" { - # tfdoc:variable:source 00-bootstrap - description = "Billing account id and organization id ('nnnnnnnn' or null)." + # tfdoc:variable:source 0-bootstrap + description = "Billing account id. If billing account is not part of the same org set `is_org_level` to false." type = object({ - id = string - organization_id = number + id = string + is_org_level = optional(bool, true) }) + validation { + condition = var.billing_account.is_org_level != null + error_message = "Invalid `null` value for `billing_account.is_org_level`." + } } variable "folder_ids" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Folder name => id mappings, the 'security' folder name must exist." type = object({ security = string @@ -40,7 +44,7 @@ variable "folder_ids" { } variable "groups" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Group names to grant organization-level permissions." type = map(string) # https://cloud.google.com/docs/enterprise/setup-checklist @@ -78,7 +82,7 @@ variable "kms_keys" { } variable "organization" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Organization details." type = object({ domain = string @@ -94,7 +98,7 @@ variable "outputs_location" { } variable "prefix" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Prefix used for resources that need unique names. Use 9 characters or less." type = string @@ -105,7 +109,7 @@ variable "prefix" { } variable "service_accounts" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Automation service accounts that can assign the encrypt/decrypt roles on keys." type = object({ data-platform-dev = string diff --git a/fast/stages/02-security/vpc-sc-restricted-services.yaml b/fast/stages/2-security/vpc-sc-restricted-services.yaml similarity index 100% rename from fast/stages/02-security/vpc-sc-restricted-services.yaml rename to fast/stages/2-security/vpc-sc-restricted-services.yaml diff --git a/fast/stages/02-security/vpc-sc.tf b/fast/stages/2-security/vpc-sc.tf similarity index 85% rename from fast/stages/02-security/vpc-sc.tf rename to fast/stages/2-security/vpc-sc.tf index 60767fe5f..953badf15 100644 --- a/fast/stages/02-security/vpc-sc.tf +++ b/fast/stages/2-security/vpc-sc.tf @@ -37,11 +37,19 @@ locals { ) } # compute spec/status for each perimeter - vpc_sc_perimeters = { + vpc_sc_perimeters_spec_status = { dev = merge(var.vpc_sc_perimeters.dev, { restricted_services = local._vpc_sc_restricted_services vpc_accessible_services = local._vpc_sc_vpc_accessible_services }) + landing = merge(var.vpc_sc_perimeters.landing, { + restricted_services = local._vpc_sc_restricted_services + vpc_accessible_services = local._vpc_sc_vpc_accessible_services + }) + prod = merge(var.vpc_sc_perimeters.prod, { + restricted_services = local._vpc_sc_restricted_services + vpc_accessible_services = local._vpc_sc_vpc_accessible_services + }) } } @@ -98,13 +106,13 @@ module "vpc-sc" { dev = { spec = ( local.vpc_sc_explicit_dry_run_spec - ? var.vpc_sc_perimeters.dev + ? local.vpc_sc_perimeters_spec_status.dev : null ) status = ( local.vpc_sc_explicit_dry_run_spec ? null - : var.vpc_sc_perimeters.dev + : local.vpc_sc_perimeters_spec_status.dev ) use_explicit_dry_run_spec = local.vpc_sc_explicit_dry_run_spec } @@ -114,13 +122,13 @@ module "vpc-sc" { landing = { spec = ( local.vpc_sc_explicit_dry_run_spec - ? var.vpc_sc_perimeters.landing + ? local.vpc_sc_perimeters_spec_status.landing : null ) status = ( local.vpc_sc_explicit_dry_run_spec ? null - : var.vpc_sc_perimeters.landing + : local.vpc_sc_perimeters_spec_status.landing ) use_explicit_dry_run_spec = local.vpc_sc_explicit_dry_run_spec } @@ -130,13 +138,13 @@ module "vpc-sc" { prod = { spec = ( local.vpc_sc_explicit_dry_run_spec - ? var.vpc_sc_perimeters.prod + ? local.vpc_sc_perimeters_spec_status.prod : null ) status = ( local.vpc_sc_explicit_dry_run_spec ? null - : var.vpc_sc_perimeters.prod + : local.vpc_sc_perimeters_spec_status.prod ) use_explicit_dry_run_spec = local.vpc_sc_explicit_dry_run_spec } diff --git a/fast/stages/03-data-platform/README.md b/fast/stages/3-data-platform/README.md similarity index 100% rename from fast/stages/03-data-platform/README.md rename to fast/stages/3-data-platform/README.md diff --git a/fast/stages/03-data-platform/dev/IAM.md b/fast/stages/3-data-platform/dev/IAM.md similarity index 100% rename from fast/stages/03-data-platform/dev/IAM.md rename to fast/stages/3-data-platform/dev/IAM.md diff --git a/fast/stages/3-data-platform/dev/README.md b/fast/stages/3-data-platform/dev/README.md new file mode 100644 index 000000000..48d09eafc --- /dev/null +++ b/fast/stages/3-data-platform/dev/README.md @@ -0,0 +1,218 @@ +# Data Platform + +The Data Platform builds on top of your foundations to create and set up projects (and related resources) to be used for your data platform. + +

+ Data Platform diagram +

+ +## Design overview and choices + +> A more comprehensive description of the Data Platform architecture and approach can be found in the [Data Platform module README](../../../../blueprints/data-solutions/data-platform-foundations/). The module is wrapped and configured here to leverage the FAST flow. + +The Data Platform creates projects in a well-defined context, usually an ad-hoc folder managed by the resource management setup. Resources are organized by environment within this folder. + +Across different data layers environment-specific projects are created to separate resources and IAM roles. + +The Data Platform manages: + +- project creation +- API/Services enablement +- service accounts creation +- IAM role assignment for groups and service accounts +- KMS keys roles assignment +- Shared VPC attachment and subnet IAM binding +- project-level organization policy definitions +- billing setup (billing account attachment and budget configuration) +- data-related resources in the managed projects + +### User groups + +As per our GCP best practices the Data Platform relies on user groups to assign roles to human identities. These are the specific groups used by the Data Platform and their access patterns, from the [module documentation](../../../../blueprints/data-solutions/data-platform-foundations/#groups): + +- *Data Engineers* They handle and run the Data Hub, with read access to all resources in order to troubleshoot possible issues with pipelines. This team can also impersonate any service account. +- *Data Analysts*. They perform analysis on datasets, with read access to the data warehouse Curated or Confidential projects depending on their privileges. +- *Data Security*:. They handle security configurations related to the Data Hub. This team has admin access to the common project to configure Cloud DLP templates or Data Catalog policy tags. + +|Group|Landing|Load|Transformation|Data Warehouse Landing|Data Warehouse Curated|Data Warehouse Confidential|Orchestration|Common| +|-|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:| +|Data Engineers|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`|`ADMIN`| +|Data Analysts|-|-|-|-|-|`READ`|-|-| +|Data Security|-|-|-|-|-|-|-|-|`ADMIN`| + +### Network + +A Shared VPC is used here, either from one of the FAST networking stages (e.g. [hub and spoke via VPN](../../2-networking-b-vpn)) or from an external source. + +### Encryption + +Cloud KMS crypto keys can be configured wither from the [FAST security stage](../../2-security) or from an external source. This step is optional and depends on customer policies and security best practices. + +To configure the use of Cloud KMS on resources, you have to specify the key id on the `service_encryption_keys` variable. Key locations should match resource locations. + +## Data Catalog + +[Data Catalog](https://cloud.google.com/data-catalog) helps you to document your data entry at scale. Data Catalog relies on [tags](https://cloud.google.com/data-catalog/docs/tags-and-tag-templates#tags) and [tag template](https://cloud.google.com/data-catalog/docs/tags-and-tag-templates#tag-templates) to manage metadata for all data entries in a unified and centralized service. To implement [column-level security](https://cloud.google.com/bigquery/docs/column-level-security-intro) on BigQuery, we suggest to use `Tags` and `Tag templates`. + +The default configuration will implement 3 tags: + +- `3_Confidential`: policy tag for columns that include very sensitive information, such as credit card numbers. +- `2_Private`: policy tag for columns that include sensitive personal identifiable information (PII) information, such as a person's first name. +- `1_Sensitive`: policy tag for columns that include data that cannot be made public, such as the credit limit. + +Anything that is not tagged is available to all users who have access to the data warehouse. + +You can configure your tags and roles associated by configuring the `data_catalog_tags` variable. We suggest useing the "[Best practices for using policy tags in BigQuery](https://cloud.google.com/bigquery/docs/best-practices-policy-tags)" article as a guide to designing your tags structure and access pattern. By default, no groups has access to tagged data. + +### VPC-SC + +As is often the case in real-world configurations, [VPC-SC](https://cloud.google.com/vpc-service-controls) is needed to mitigate data exfiltration. VPC-SC can be configured from the [FAST security stage](../../2-security). This step is optional, but highly recomended, and depends on customer policies and security best practices. + +To configure the use of VPC-SC on the data platform, you have to specify the data platform project numbers on the `vpc_sc_perimeter_projects.dev` variable on [FAST security stage](../../2-security#perimeter-resources). + +In the case your Data Warehouse need to handle confidential data and you have the requirement to separate them deeply from other data and IAM is not enough, the suggested configuration is to keep the confidential project in a separate VPC-SC perimeter with the adequate ingress/egress rules needed for the load and tranformation service account. Below you can find an high level diagram describing the configuration. + +

+ Data Platform VPC-SC diagram +

+ +## How to run this stage + +This stage is meant to be executed after the FAST "foundational" stages: bootstrap, resource management, security and networking stages. + +It's of course possible to run this stage in isolation, refer to the *[Running in isolation](#running-in-isolation)* section below for details. + +Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. + +### Provider and Terraform variables + +As all other FAST stages, the [mechanism used to pass variable values and pre-built provider files from one stage to the next](../../0-bootstrap/README.md#output-files-and-cross-stage-variables) is also leveraged here. + +The commands to link or copy the provider and terraform variable files can be easily derived from the `stage-links.sh` script in the FAST root folder, passing it a single argument with the local output files folder (if configured) or the GCS output bucket in the automation project (derived from stage 0 outputs). The following examples demonstrate both cases, and the resulting commands that then need to be copy/pasted and run. + +```bash +../../../stage-links.sh ~/fast-config + +# copy and paste the following commands for '3-data-platform' + +ln -s /home/ludomagno/fast-config/providers/3-data-platform-providers.tf ./ +ln -s /home/ludomagno/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s /home/ludomagno/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +ln -s /home/ludomagno/fast-config/tfvars/1-resman.auto.tfvars.json ./ +ln -s /home/ludomagno/fast-config/tfvars/2-networking.auto.tfvars.json ./ +ln -s /home/ludomagno/fast-config/tfvars/2-security.auto.tfvars.json ./ +``` + +```bash +../../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '3-data-platform' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/3-data-platform-providers.tf ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/globals.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/1-resman.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/2-networking.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/2-security.auto.tfvars.json ./ +``` + +### Impersonating the automation service account + +The preconfigured provider file uses impersonation to run with this stage's automation service account's credentials. The `gcp-devops` and `organization-admins` groups have the necessary IAM bindings in place to do that, so make sure the current user is a member of one of those groups. + +### Variable configuration + +Variables in this stage -- like most other FAST stages -- are broadly divided into three separate sets: + +- variables which refer to global values for the whole organization (org id, billing account id, prefix, etc.), which are pre-populated via the `globals.auto.tfvars.json` file linked or copied above +- variables which refer to resources managed by previous stage, which are prepopulated here via the `*.auto.tfvars.json` files linked or copied above +- and finally variables that optionally control this stage's behaviour and customizations, and can to be set in a custom `terraform.tfvars` file + +The full list can be found in the [Variables](#variables) table at the bottom of this document. + +### Running the stage + +Once provider and variable values are in place and the correct user is configured, the stage can be run: + +```bash +terraform init +terraform apply +``` + +### Running in isolation + +This stage can be run in isolation by providing the necessary variables, but it's really meant to be used as part of the FAST flow after the "foundational stages" ([`0-bootstrap`](../../0-bootstrap), [`1-resman`](../../1-resman), [`2-networking`](../../2-networking-b-vpn) and [`2-security`](../../2-security)). + +When running in isolation, the following roles are needed on the principal used to apply Terraform: + +- on the organization or network folder level + - `roles/xpnAdmin` or a custom role which includes the following permissions + - `"compute.organizations.enableXpnResource"`, + - `"compute.organizations.disableXpnResource"`, + - `"compute.subnetworks.setIamPolicy"`, +- on each folder where projects are created + - `"roles/logging.admin"` + - `"roles/owner"` + - `"roles/resourcemanager.folderAdmin"` + - `"roles/resourcemanager.projectCreator"` +- on the host project for the Shared VPC + - `"roles/browser"` + - `"roles/compute.viewer"` +- on the organization or billing account + - `roles/billing.admin` + +The VPC host project, VPC and subnets should already exist. + +## Demo pipeline + +The application layer is out of scope of this script. As a demo purpuse only, several Cloud Composer DAGs are provided. Demos will import data from the `landing` area to the `DataWarehouse Confidential` dataset suing different features. + +You can find examples in the `[demo](../../../../blueprints/data-solutions/data-platform-foundations/demo)` folder. + + + + +## Files + +| name | description | modules | resources | +|---|---|---|---| +| [main.tf](./main.tf) | Data Platform. | data-platform-foundations | | +| [outputs.tf](./outputs.tf) | Output variables. | | google_storage_bucket_object · local_file | +| [variables.tf](./variables.tf) | Terraform Variables. | | | + +## Variables + +| name | description | type | required | default | producer | +|---|---|:---:|:---:|:---:|:---:| +| [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#L102) | Folder to be used for the networking resources in folders/nnnn format. | object({…}) | ✓ | | 1-resman | +| [host_project_ids](variables.tf#L120) | Shared VPC project ids. | object({…}) | ✓ | | 2-networking | +| [organization](variables.tf#L150) | Organization details. | object({…}) | ✓ | | 00-globals | +| [prefix](variables.tf#L166) | Unique prefix used for resource names. Not used for projects if 'project_create' is null. | string | ✓ | | 00-globals | +| [composer_config](variables.tf#L38) | Cloud Composer configuration options. | object({…}) | | {…} | | +| [data_catalog_tags](variables.tf#L85) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(map(list(string))) | | {…} | | +| [data_force_destroy](variables.tf#L96) | Flag to set 'force_destroy' on data services like BigQery or Cloud Storage. | bool | | false | | +| [groups](variables.tf#L110) | Groups. | map(string) | | {…} | | +| [location](variables.tf#L128) | Location used for multi-regional resources. | string | | "eu" | | +| [network_config_composer](variables.tf#L134) | Network configurations to use for Composer. | object({…}) | | {…} | | +| [outputs_location](variables.tf#L160) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | +| [project_services](variables.tf#L172) | List of core services enabled on all projects. | list(string) | | […] | | +| [region](variables.tf#L183) | Region used for regional resources. | string | | "europe-west1" | | +| [service_encryption_keys](variables.tf#L189) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | null | | +| [subnet_self_links](variables.tf#L201) | Shared VPC subnet self links. | object({…}) | | null | 2-networking | +| [vpc_self_links](variables.tf#L210) | Shared VPC self links. | object({…}) | | null | 2-networking | + +## Outputs + +| name | description | sensitive | consumers | +|---|---|:---:|---| +| [bigquery_datasets](outputs.tf#L42) | BigQuery datasets. | | | +| [demo_commands](outputs.tf#L47) | Demo commands. | | | +| [gcs_buckets](outputs.tf#L52) | GCS buckets. | | | +| [kms_keys](outputs.tf#L57) | Cloud MKS keys. | | | +| [projects](outputs.tf#L62) | GCP Projects informations. | | | +| [vpc_network](outputs.tf#L67) | VPC network. | | | +| [vpc_subnet](outputs.tf#L72) | VPC subnetworks. | | | + + diff --git a/fast/stages/03-data-platform/dev/demo b/fast/stages/3-data-platform/dev/demo similarity index 100% rename from fast/stages/03-data-platform/dev/demo rename to fast/stages/3-data-platform/dev/demo diff --git a/fast/stages/03-data-platform/dev/diagram.png b/fast/stages/3-data-platform/dev/diagram.png similarity index 100% rename from fast/stages/03-data-platform/dev/diagram.png rename to fast/stages/3-data-platform/dev/diagram.png diff --git a/fast/stages/03-data-platform/dev/diagram_vpcsc.png b/fast/stages/3-data-platform/dev/diagram_vpcsc.png similarity index 100% rename from fast/stages/03-data-platform/dev/diagram_vpcsc.png rename to fast/stages/3-data-platform/dev/diagram_vpcsc.png diff --git a/fast/stages/03-data-platform/dev/main.tf b/fast/stages/3-data-platform/dev/main.tf similarity index 97% rename from fast/stages/03-data-platform/dev/main.tf rename to fast/stages/3-data-platform/dev/main.tf index 24abb58d9..53d901d1b 100644 --- a/fast/stages/03-data-platform/dev/main.tf +++ b/fast/stages/3-data-platform/dev/main.tf @@ -37,7 +37,6 @@ module "data-platform" { composer_ip_ranges = { cloudsql = var.network_config_composer.cloudsql_range gke_master = var.network_config_composer.gke_master_range - web_server = var.network_config_composer.web_server_range } composer_secondary_ranges = { pods = var.network_config_composer.gke_pods_name diff --git a/fast/stages/03-data-platform/dev/outputs.tf b/fast/stages/3-data-platform/dev/outputs.tf similarity index 95% rename from fast/stages/03-data-platform/dev/outputs.tf rename to fast/stages/3-data-platform/dev/outputs.tf index d0f79358c..2eb813b4d 100644 --- a/fast/stages/03-data-platform/dev/outputs.tf +++ b/fast/stages/3-data-platform/dev/outputs.tf @@ -27,13 +27,13 @@ locals { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${pathexpand(var.outputs_location)}/tfvars/03-data-platform-dev.auto.tfvars.json" + filename = "${pathexpand(var.outputs_location)}/tfvars/3-data-platform-dev.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket - name = "tfvars/03-data-platform-dev.auto.tfvars.json" + name = "tfvars/3-data-platform-dev.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/03-data-platform/dev/variables.tf b/fast/stages/3-data-platform/dev/variables.tf similarity index 72% rename from fast/stages/03-data-platform/dev/variables.tf rename to fast/stages/3-data-platform/dev/variables.tf index 9495316a9..74a5dbe11 100644 --- a/fast/stages/03-data-platform/dev/variables.tf +++ b/fast/stages/3-data-platform/dev/variables.tf @@ -15,7 +15,7 @@ # tfdoc:file:description Terraform Variables. variable "automation" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Automation resources created by the bootstrap stage." type = object({ outputs_bucket = string @@ -23,25 +23,62 @@ variable "automation" { } variable "billing_account" { - # tfdoc:variable:source 00-globals - description = "Billing account id and organization id ('nnnnnnnn' or null)." + # tfdoc:variable:source 0-bootstrap + description = "Billing account id. If billing account is not part of the same org set `is_org_level` to false." type = object({ - id = string - organization_id = number + id = string + is_org_level = optional(bool, true) }) + validation { + condition = var.billing_account.is_org_level != null + error_message = "Invalid `null` value for `billing_account.is_org_level`." + } } variable "composer_config" { description = "Cloud Composer configuration options." type = object({ - node_count = number - airflow_version = string - env_variables = map(string) + disable_deployment = optional(bool) + environment_size = string + software_config = object({ + airflow_config_overrides = optional(any) + pypi_packages = optional(any) + env_variables = optional(map(string)) + image_version = string + }) + workloads_config = object({ + scheduler = object( + { + cpu = number + memory_gb = number + storage_gb = number + count = number + } + ) + web_server = object( + { + cpu = number + memory_gb = number + storage_gb = number + } + ) + worker = object( + { + cpu = number + memory_gb = number + storage_gb = number + min_count = number + max_count = number + } + ) + }) }) default = { - node_count = 3 - airflow_version = "composer-1.17.5-airflow-2.1.4" - env_variables = {} + environment_size = "ENVIRONMENT_SIZE_SMALL" + software_config = { + image_version = "composer-2-airflow-2" + } + workloads_config = null } } @@ -63,7 +100,7 @@ variable "data_force_destroy" { } variable "folder_ids" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Folder to be used for the networking resources in folders/nnnn format." type = object({ data-platform-dev = string @@ -81,7 +118,7 @@ variable "groups" { } variable "host_project_ids" { - # tfdoc:variable:source 02-networking + # tfdoc:variable:source 2-networking description = "Shared VPC project ids." type = object({ dev-spoke-0 = string @@ -101,14 +138,12 @@ variable "network_config_composer" { gke_master_range = string gke_pods_name = string gke_services_name = string - web_server_range = string }) default = { cloudsql_range = "192.168.254.0/24" gke_master_range = "192.168.255.0/28" gke_pods_name = "pods" gke_services_name = "services" - web_server_range = "192.168.255.16/28" } } @@ -164,7 +199,7 @@ variable "service_encryption_keys" { } variable "subnet_self_links" { - # tfdoc:variable:source 02-networking + # tfdoc:variable:source 2-networking description = "Shared VPC subnet self links." type = object({ dev-spoke-0 = map(string) @@ -173,7 +208,7 @@ variable "subnet_self_links" { } variable "vpc_self_links" { - # tfdoc:variable:source 02-networking + # tfdoc:variable:source 2-networking description = "Shared VPC self links." type = object({ dev-spoke-0 = string diff --git a/fast/stages/03-gke-multitenant/README.md b/fast/stages/3-gke-multitenant/README.md similarity index 71% rename from fast/stages/03-gke-multitenant/README.md rename to fast/stages/3-gke-multitenant/README.md index f08910c83..9f9d9498e 100644 --- a/fast/stages/03-gke-multitenant/README.md +++ b/fast/stages/3-gke-multitenant/README.md @@ -2,7 +2,7 @@ This directory contains a stage that can be used to centralize management of GKE multinenant clusters. -The Terraform code follows the same general approach used for the [project factory](../03-project-factory/) and [data platform](../03-data-platform/) stages, where a "fat module" contains the stage code and is used by thin code wrappers that localize it for each environment or specialized configuration: +The Terraform code follows the same general approach used for the [project factory](../3-project-factory/) and [data platform](../3-data-platform/) stages, where a "fat module" contains the stage code and is used by thin code wrappers that localize it for each environment or specialized configuration: The [`dev` folder](./dev/) contains an example setup for a generic development environment, and can be used as-is or cloned to implement other environments, or more specialized setups diff --git a/fast/stages/03-gke-multitenant/dev/README.md b/fast/stages/3-gke-multitenant/dev/README.md similarity index 73% rename from fast/stages/03-gke-multitenant/dev/README.md rename to fast/stages/3-gke-multitenant/dev/README.md index c446fbcb4..f0460c06c 100644 --- a/fast/stages/03-gke-multitenant/dev/README.md +++ b/fast/stages/3-gke-multitenant/dev/README.md @@ -39,7 +39,68 @@ This stage creates a project containing and as many clusters and node pools as r ## How to run this stage -This stage is meant to be executed after "foundational stages" (i.e., stages [`00-bootstrap`](../../00-bootstrap), [`01-resman`](../../01-resman), 02-networking (either [VPN](../../02-networking-vpn) or [NVA](../../02-networking-nva)) and [`02-security`](../../02-security)) have been run. +This stage is meant to be executed after the FAST "foundational" stages: bootstrap, resource management, security and networking stages. + +It's of course possible to run this stage in isolation, refer to the *[Running in isolation](#running-in-isolation)* section below for details. + +Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. + +### Provider and Terraform variables + +As all other FAST stages, the [mechanism used to pass variable values and pre-built provider files from one stage to the next](../../0-bootstrap/README.md#output-files-and-cross-stage-variables) is also leveraged here. + +The commands to link or copy the provider and terraform variable files can be easily derived from the `stage-links.sh` script in the FAST root folder, passing it a single argument with the local output files folder (if configured) or the GCS output bucket in the automation project (derived from stage 0 outputs). The following examples demonstrate both cases, and the resulting commands that then need to be copy/pasted and run. + +```bash +../../../stage-links.sh ~/fast-config + +# copy and paste the following commands for '3-gke-multitenant' + +ln -s /home/ludomagno/fast-config/providers/3-gke-multitenant-providers.tf ./ +ln -s /home/ludomagno/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s /home/ludomagno/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +ln -s /home/ludomagno/fast-config/tfvars/1-resman.auto.tfvars.json ./ +ln -s /home/ludomagno/fast-config/tfvars/2-networking.auto.tfvars.json ./ +ln -s /home/ludomagno/fast-config/tfvars/2-security.auto.tfvars.json ./ +``` + +```bash +../../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '3-gke-multitenant' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/3-gke-multitenant-providers.tf ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/globals.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/1-resman.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/2-networking.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/2-security.auto.tfvars.json ./ +``` + +### Impersonating the automation service account + +The preconfigured provider file uses impersonation to run with this stage's automation service account's credentials. The `gcp-devops` and `organization-admins` groups have the necessary IAM bindings in place to do that, so make sure the current user is a member of one of those groups. + +### Variable configuration + +Variables in this stage -- like most other FAST stages -- are broadly divided into three separate sets: + +- variables which refer to global values for the whole organization (org id, billing account id, prefix, etc.), which are pre-populated via the `globals.auto.tfvars.json` file linked or copied above +- variables which refer to resources managed by previous stage, which are prepopulated here via the `*.auto.tfvars.json` files linked or copied above +- and finally variables that optionally control this stage's behaviour and customizations, and can to be set in a custom `terraform.tfvars` file + +The latter set is explained in the [Customization](#customizations) sections below, and the full list can be found in the [Variables](#variables) table at the bottom of this document. + +### Running the stage + +Once provider and variable values are in place and the correct user is configured, the stage can be run: + +```bash +terraform init +terraform apply +``` + +### Running in isolation It's of course possible to run this stage in isolation, by making sure the architectural prerequisites are satisfied (e.g., networking), and that the Service Account running the stage is granted the roles/permissions below: @@ -62,39 +123,9 @@ It's of course possible to run this stage in isolation, by making sure the archi The VPC host project, VPC and subnets should already exist. -### Providers configuration +## Customizations -If you're running this on top of FAST, you should run the following commands to create the providers file, and populate the required variables from the previous stage. - -```bash -# Variable `outputs_location` is set to `~/fast-config` in stage 01-resman -$ cd fabric-fast/stages/03-gke-multitenant/dev -ln -s ~/fast-config/providers/03-gke-dev-providers.tf . -``` - -### Variable configuration - -There are two broad sets of variables you will need to fill in: - -- variables shared by other stages (organization id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) -- variables specific to resources managed by this stage - -#### Variables passed in from other stages - -To avoid the tedious job of filling in the first group of variables with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. - -If you configured a valid path for `outputs_location` in the bootstrap and networking stage, simply link the relevant `terraform-*.auto.tfvars.json` files from this stage's outputs folder (under the path you specified), where the `*` above is set to the name of the stage that produced it. For this stage, a single `.tfvars` file is available: - -```bash -# Variable `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . -ln -s ~/fast-config/tfvars/02-networking.auto.tfvars.json . -``` - -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. - -#### Cluster and node pools +### Cluster and node pools This stage is designed with multi-tenancy in mind, and the expectation is that GKE clusters will mostly share a common set of defaults. Variables are designed to support this approach for both clusters and node pools: @@ -105,7 +136,7 @@ This stage is designed with multi-tenancy in mind, and the expectation is that There are two additional variables that influence cluster configuration: `authenticator_security_group` to configure [Google Groups for RBAC](https://cloud.google.com/kubernetes-engine/docs/how-to/google-groups-rbac), `dns_domain` to configure [Cloud DNS for GKE](https://cloud.google.com/kubernetes-engine/docs/how-to/cloud-dns). -#### Fleet management +### Fleet management Fleet management is entirely optional, and uses three separate variables: @@ -116,15 +147,6 @@ Fleet management is entirely optional, and uses three separate variables: Leave all these variables unset (or set to `null`) to disable fleet management. -## Running Terraform - -Once the [provider](#providers-configuration) and [variable](#variable-configuration) configuration is complete, you can apply this stage: - -```bash -terraform init -terraform apply -``` - @@ -140,23 +162,23 @@ terraform apply | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [automation](variables.tf#L21) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | -| [billing_account](variables.tf#L29) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [folder_ids](variables.tf#L149) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 01-resman | -| [host_project_ids](variables.tf#L164) | Host project for the shared VPC. | object({…}) | ✓ | | 02-networking | -| [prefix](variables.tf#L213) | Prefix used for resources that need unique names. | string | ✓ | | | -| [vpc_self_links](variables.tf#L225) | Self link for the shared VPC. | object({…}) | ✓ | | 02-networking | -| [clusters](variables.tf#L38) | Clusters configuration. Refer to the gke-cluster module for type details. | map(object({…})) | | {} | | -| [fleet_configmanagement_clusters](variables.tf#L86) | Config management features enabled on specific sets of member clusters, in config name => [cluster name] format. | map(list(string)) | | {} | | -| [fleet_configmanagement_templates](variables.tf#L94) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(object({…})) | | {} | | -| [fleet_features](variables.tf#L129) | Enable and configue fleet features. Set to null to disable GKE Hub if fleet workload identity is not used. | object({…}) | | null | | -| [fleet_workload_identity](variables.tf#L142) | Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true. | bool | | false | | -| [group_iam](variables.tf#L157) | 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#L172) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | -| [labels](variables.tf#L179) | Project-level labels. | map(string) | | {} | | -| [nodepools](variables.tf#L185) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} | | -| [outputs_location](variables.tf#L207) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | -| [project_services](variables.tf#L218) | Additional project services to enable. | list(string) | | [] | | +| [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#L153) | 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#L168) | Host project for the shared VPC. | object({…}) | ✓ | | 2-networking | +| [prefix](variables.tf#L217) | Prefix used for resources that need unique names. | string | ✓ | | | +| [vpc_self_links](variables.tf#L229) | 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#L90) | Config management features enabled on specific sets of member clusters, in config name => [cluster name] format. | map(list(string)) | | {} | | +| [fleet_configmanagement_templates](variables.tf#L98) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(object({…})) | | {} | | +| [fleet_features](variables.tf#L133) | Enable and configue fleet features. Set to null to disable GKE Hub if fleet workload identity is not used. | object({…}) | | null | | +| [fleet_workload_identity](variables.tf#L146) | Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true. | bool | | false | | +| [group_iam](variables.tf#L161) | 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#L176) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | +| [labels](variables.tf#L183) | Project-level labels. | map(string) | | {} | | +| [nodepools](variables.tf#L189) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…}))) | | {} | | +| [outputs_location](variables.tf#L211) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | +| [project_services](variables.tf#L222) | Additional project services to enable. | list(string) | | [] | | ## Outputs diff --git a/fast/stages/03-gke-multitenant/dev/diagram.png b/fast/stages/3-gke-multitenant/dev/diagram.png similarity index 100% rename from fast/stages/03-gke-multitenant/dev/diagram.png rename to fast/stages/3-gke-multitenant/dev/diagram.png diff --git a/fast/stages/03-gke-multitenant/dev/main.tf b/fast/stages/3-gke-multitenant/dev/main.tf similarity index 100% rename from fast/stages/03-gke-multitenant/dev/main.tf rename to fast/stages/3-gke-multitenant/dev/main.tf diff --git a/fast/stages/03-gke-multitenant/dev/outputs.tf b/fast/stages/3-gke-multitenant/dev/outputs.tf similarity index 96% rename from fast/stages/03-gke-multitenant/dev/outputs.tf rename to fast/stages/3-gke-multitenant/dev/outputs.tf index 87b0ca737..3f231c682 100644 --- a/fast/stages/03-gke-multitenant/dev/outputs.tf +++ b/fast/stages/3-gke-multitenant/dev/outputs.tf @@ -42,13 +42,13 @@ locals { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } file_permission = "0644" - filename = "${pathexpand(var.outputs_location)}/tfvars/03-gke-dev.auto.tfvars.json" + filename = "${pathexpand(var.outputs_location)}/tfvars/3-gke-dev.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket - name = "tfvars/03-gke-dev.auto.tfvars.json" + name = "tfvars/3-gke-dev.auto.tfvars.json" content = jsonencode(local.tfvars) } diff --git a/fast/stages/03-gke-multitenant/dev/variables.tf b/fast/stages/3-gke-multitenant/dev/variables.tf similarity index 92% rename from fast/stages/03-gke-multitenant/dev/variables.tf rename to fast/stages/3-gke-multitenant/dev/variables.tf index 6be89126a..2dbf5a6ea 100644 --- a/fast/stages/03-gke-multitenant/dev/variables.tf +++ b/fast/stages/3-gke-multitenant/dev/variables.tf @@ -19,7 +19,7 @@ # cloud dns for gke? variable "automation" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Automation resources created by the bootstrap stage." type = object({ outputs_bucket = string @@ -27,12 +27,16 @@ variable "automation" { } variable "billing_account" { - # tfdoc:variable:source 00-bootstrap - description = "Billing account id and organization id ('nnnnnnnn' or null)." + # tfdoc:variable:source 0-bootstrap + description = "Billing account id. If billing account is not part of the same org set `is_org_level` to false." type = object({ - id = string - organization_id = number + id = string + is_org_level = optional(bool, true) }) + validation { + condition = var.billing_account.is_org_level != null + error_message = "Invalid `null` value for `billing_account.is_org_level`." + } } variable "clusters" { @@ -147,7 +151,7 @@ variable "fleet_workload_identity" { } variable "folder_ids" { - # tfdoc:variable:source 01-resman + # tfdoc:variable:source 1-resman description = "Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created." type = object({ gke-dev = string @@ -162,7 +166,7 @@ variable "group_iam" { } variable "host_project_ids" { - # tfdoc:variable:source 02-networking + # tfdoc:variable:source 2-networking description = "Host project for the shared VPC." type = object({ dev-spoke-0 = string @@ -223,7 +227,7 @@ variable "project_services" { } variable "vpc_self_links" { - # tfdoc:variable:source 02-networking + # tfdoc:variable:source 2-networking description = "Self link for the shared VPC." type = object({ dev-spoke-0 = string diff --git a/fast/stages/03-project-factory/README.md b/fast/stages/3-project-factory/README.md similarity index 100% rename from fast/stages/03-project-factory/README.md rename to fast/stages/3-project-factory/README.md diff --git a/fast/stages/03-project-factory/dev/README.md b/fast/stages/3-project-factory/dev/README.md similarity index 85% rename from fast/stages/03-project-factory/dev/README.md rename to fast/stages/3-project-factory/dev/README.md index 8fe213cee..2d95f918d 100644 --- a/fast/stages/03-project-factory/dev/README.md +++ b/fast/stages/3-project-factory/dev/README.md @@ -28,7 +28,7 @@ The project factory takes care of the following activities: ## How to run this stage -This stage is meant to be executed after "foundational stages" (i.e., stages [`00-bootstrap`](../../00-bootstrap), [`01-resman`](../../01-resman), 02-networking (either [VPN](../../02-networking-vpn) or [NVA](../../02-networking-nva)) and [`02-security`](../../02-security)) have been run. +This stage is meant to be executed after "foundational stages" (i.e., stages [`00-bootstrap`](../../0-bootstrap), [`01-resman`](../../1-resman), 02-networking (either [VPN](../../2-networking-b-vpn) or [NVA](../../2-networking-c-nva)) and [`02-security`](../../2-security)) have been run. It's of course possible to run this stage in isolation, by making sure the architectural prerequisites are satisfied (e.g., networking), and that the Service Account running the stage is granted the roles/permissions below: @@ -108,13 +108,13 @@ terraform apply | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [billing_account](variables.tf#L19) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [prefix](variables.tf#L56) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | -| [data_dir](variables.tf#L28) | Relative path for the folder storing configuration data. | string | | "data/projects" | | -| [defaults_file](variables.tf#L34) | Relative path for the file storing the project factory configuration. | string | | "data/defaults.yaml" | | -| [environment_dns_zone](variables.tf#L40) | DNS zone suffix for environment. | string | | null | 02-networking | -| [host_project_ids](variables.tf#L47) | Host project for the shared VPC. | object({…}) | | null | 02-networking | -| [vpc_self_links](variables.tf#L67) | Self link for the shared VPC. | object({…}) | | null | 02-networking | +| [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 | +| [prefix](variables.tf#L60) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [data_dir](variables.tf#L32) | Relative path for the folder storing configuration data. | string | | "data/projects" | | +| [defaults_file](variables.tf#L38) | Relative path for the file storing the project factory configuration. | string | | "data/defaults.yaml" | | +| [environment_dns_zone](variables.tf#L44) | DNS zone suffix for environment. | string | | null | 2-networking | +| [host_project_ids](variables.tf#L51) | Host project for the shared VPC. | object({…}) | | null | 2-networking | +| [vpc_self_links](variables.tf#L71) | Self link for the shared VPC. | object({…}) | | null | 2-networking | ## Outputs diff --git a/fast/stages/03-project-factory/dev/data/defaults.yaml b/fast/stages/3-project-factory/dev/data/defaults.yaml similarity index 100% rename from fast/stages/03-project-factory/dev/data/defaults.yaml rename to fast/stages/3-project-factory/dev/data/defaults.yaml diff --git a/fast/stages/03-project-factory/dev/data/projects/project.yaml.sample b/fast/stages/3-project-factory/dev/data/projects/project.yaml.sample similarity index 100% rename from fast/stages/03-project-factory/dev/data/projects/project.yaml.sample rename to fast/stages/3-project-factory/dev/data/projects/project.yaml.sample diff --git a/fast/stages/03-project-factory/dev/diagram.png b/fast/stages/3-project-factory/dev/diagram.png similarity index 100% rename from fast/stages/03-project-factory/dev/diagram.png rename to fast/stages/3-project-factory/dev/diagram.png diff --git a/fast/stages/03-project-factory/dev/diagram.svg b/fast/stages/3-project-factory/dev/diagram.svg similarity index 100% rename from fast/stages/03-project-factory/dev/diagram.svg rename to fast/stages/3-project-factory/dev/diagram.svg diff --git a/fast/stages/03-project-factory/dev/main.tf b/fast/stages/3-project-factory/dev/main.tf similarity index 100% rename from fast/stages/03-project-factory/dev/main.tf rename to fast/stages/3-project-factory/dev/main.tf diff --git a/fast/stages/03-project-factory/dev/outputs.tf b/fast/stages/3-project-factory/dev/outputs.tf similarity index 100% rename from fast/stages/03-project-factory/dev/outputs.tf rename to fast/stages/3-project-factory/dev/outputs.tf diff --git a/fast/stages/03-project-factory/dev/variables.tf b/fast/stages/3-project-factory/dev/variables.tf similarity index 76% rename from fast/stages/03-project-factory/dev/variables.tf rename to fast/stages/3-project-factory/dev/variables.tf index 2993bfba7..5ad49f772 100644 --- a/fast/stages/03-project-factory/dev/variables.tf +++ b/fast/stages/3-project-factory/dev/variables.tf @@ -17,12 +17,16 @@ #TODO: tfdoc annotations variable "billing_account" { - # tfdoc:variable:source 00-bootstrap - description = "Billing account id and organization id ('nnnnnnnn' or null)." + # tfdoc:variable:source 0-bootstrap + description = "Billing account id. If billing account is not part of the same org set `is_org_level` to false." type = object({ - id = string - organization_id = number + id = string + is_org_level = optional(bool, true) }) + validation { + condition = var.billing_account.is_org_level != null + error_message = "Invalid `null` value for `billing_account.is_org_level`." + } } variable "data_dir" { @@ -38,14 +42,14 @@ variable "defaults_file" { } variable "environment_dns_zone" { - # tfdoc:variable:source 02-networking + # tfdoc:variable:source 2-networking description = "DNS zone suffix for environment." type = string default = null } variable "host_project_ids" { - # tfdoc:variable:source 02-networking + # tfdoc:variable:source 2-networking description = "Host project for the shared VPC." type = object({ dev-spoke-0 = string @@ -54,7 +58,7 @@ variable "host_project_ids" { } variable "prefix" { - # tfdoc:variable:source 00-bootstrap + # tfdoc:variable:source 0-bootstrap description = "Prefix used for resources that need unique names. Use 9 characters or less." type = string @@ -65,7 +69,7 @@ variable "prefix" { } variable "vpc_self_links" { - # tfdoc:variable:source 02-networking + # tfdoc:variable:source 2-networking description = "Self link for the shared VPC." type = object({ dev-spoke-0 = string diff --git a/fast/stages/CLEANUP.md b/fast/stages/CLEANUP.md index e5f1418f7..4b2667c83 100644 --- a/fast/stages/CLEANUP.md +++ b/fast/stages/CLEANUP.md @@ -1,4 +1,5 @@ # FAST deployment clean up + If you want to destroy a previous FAST deployment in your organization, follow these steps. Destruction must be done in reverse order, from stage 3 to stage 0 @@ -6,15 +7,16 @@ Destruction must be done in reverse order, from stage 3 to stage 0 ## Stage 3 (Project Factory) ```bash -cd $FAST_PWD/03-project-factory/prod/ +cd $FAST_PWD/3-project-factory/dev/ terraform destroy ``` ## Stage 3 (GKE) + Terraform refuses to delete non-empty GCS buckets and BigQuery datasets, so they need to be removed manually from the state. ```bash -cd $FAST_PWD/03-project-factory/prod/ +cd $FAST_PWD/3-gke-multitenant/dev/ # remove BQ dataset manually for x in $(terraform state list | grep google_bigquery_dataset); do @@ -24,16 +26,17 @@ done terraform destroy ``` - ## Stage 2 (Security) + ```bash -cd $FAST_PWD/02-security/ +cd $FAST_PWD/2-security/ terraform destroy ``` ## Stage 2 (Networking) + ```bash -cd $FAST_PWD/02-networking-XXX/ +cd $FAST_PWD/2-networking-XXX/ terraform destroy ``` @@ -43,9 +46,8 @@ A minor glitch can surface running `terraform destroy`, where the service projec Stage 1 is a little more complicated because of the GCS buckets containing your terraform statefiles. By default, Terraform refuses to delete non-empty buckets, which is good to protect your terraform state, but it makes destruction a bit harder. Use the commands below to remove the GCS buckets from the state and then execute `terraform destroy` - ```bash -cd $FAST_PWD/01-resman/ +cd $FAST_PWD/1-resman/ # remove buckets from state since terraform refuses to delete them for x in $(terraform state list | grep google_storage_bucket.bucket); do @@ -62,10 +64,10 @@ terraform destroy Just like before, we manually remove several resources (GCS buckets and BQ datasets). Note that `terrafom destroy` will fail. This is expected; just continue with the rest of the steps. ```bash -cd $FAST_PWD/00-bootstrap/ +cd $FAST_PWD/0-bootstrap/ # remove provider config to execute without SA impersonation -rm 00-bootstrap-providers.tf +rm 0-bootstrap-providers.tf # migrate to local state terraform init -migrate-state @@ -110,5 +112,6 @@ rm -i terraform.tfstate* ``` In case you want to deploy FAST stages again, the make sure to: -* Modify the [prefix](00-bootstrap/variables.tf) variable to allow the deployment of resources that need unique names (eg, projects). -* Modify the [custom_roles](00-bootstrap/variables.tf) variable to allow recently deleted custom roles to be created again. + +* Modify the [prefix](0-bootstrap/variables.tf) variable to allow the deployment of resources that need unique names (eg, projects). +* Modify the [custom_roles](0-bootstrap/variables.tf) variable to allow recently deleted custom roles to be created again. diff --git a/fast/stages/COMPANION.md b/fast/stages/COMPANION.md index e0f6ec621..96506d008 100644 --- a/fast/stages/COMPANION.md +++ b/fast/stages/COMPANION.md @@ -1,32 +1,42 @@ # FAST deployment companion guide -To deploy a GCP Landing Zone using FAST, your organization needs to meet a few prerequisites before starting. This guide serves as quick guide to prepare your GCP organization and also as cheat sheet with the commands and minimal configuration required to deploy FAST. +To deploy a GCP Landing Zone using FAST, your organization needs to meet a few prerequisites before starting. This guide serves as quick guide to prepare your GCP organization and also as cheat sheet with the commands and minimal configuration required to deploy FAST. The detailed explanation of each stage, their configuration, possible modifications and adaptations are included in the README of stage. This document only outlines the minimal configuration to get from an empty organization to a working FAST deployment. **Warning! Executing FAST sets organization policies and authoritative role bindings in your GCP Organization. We recommend using FAST on a clean organization, or to fork and adapt FAST to support your existing Organization needs.** ## Prerequisites -1. FAST uses the recommended groups from the [GCP Enterprise Setup checklist](). Go to [Workspace / Cloud Identity](https://admin.google.com) and ensure all the following groups exist: - - `gcp-billing-admins@` - - `gcp-devops@` - - `gcp-network-admins@` - - `gcp-organization-admins@` - - `gcp-security-admins@` - - `gcp-support@` + +1. FAST uses the recommended groups from the [GCP Enterprise Setup checklist](https://cloud.google.com/docs/enterprise/setup-checklist). Go to [Workspace / Cloud Identity](https://admin.google.com) and ensure all the following groups exist: + +- `gcp-billing-admins@` +- `gcp-devops@` +- `gcp-network-admins@` +- `gcp-organization-admins@` +- `gcp-security-admins@` +- `gcp-support@` + 2. If you already executed FAST in your organization, make you [clean it up](CLEANUP.md) before continuing with the rest of this guide. + 3. Grant your user “Organization Administrator” role in your organization and add it to the `gcp-organization-admins@` group. + 4. Login with your user using gcloud. + ```bash gcloud auth login gcloud auth application-default login ``` + 5. Clone the Fabric repository. + ```bash git clone https://github.com/GoogleCloudPlatform/cloud-foundation-fabric.git cd cloud-foundation-fabric ``` + 6. Grant required roles to your user. + ```bash # set a variable to the fast folder export FAST_PWD="$(pwd)/fast/stages" @@ -49,9 +59,11 @@ gcloud organizations add-iam-policy-binding $FAST_ORG_ID \ --member user:$FAST_BU --role $role done ``` -7. Configure Billing Account permissions. + +7. Configure Billing Account permissions. If you are using a standalone billing account, the user applying this stage for the first time needs to be a Billing Administrator. + ```bash # find your billing account id with gcloud beta billing accounts list # replace with your billing id! @@ -60,20 +72,24 @@ export FAST_BA_ID=XXXXXX-YYYYYY-ZZZZZZ gcloud beta billing accounts add-iam-policy-binding $FAST_BA_ID \ --member user:$FAST_BU --role roles/billing.admin ``` -If you are using a billing account in a different organization, please follow [these steps](00-bootstrap#billing-account-in-a-different-organization) instead. + +If you are using a billing account in a different organization, please follow [these steps](0-bootstrap#billing-account-in-a-different-organization) instead. ## Stage 0 (Bootstrap) + This initial stage will create common projects for IaC, Logging & Billing, and bootstrap IAM policies. ```bash -# move to the 00-bootstrap directory -cd $FAST_PWD/00-bootstrap +# move to the 0-bootstrap directory +cd $FAST_PWD/0-bootstrap # copy the template terraform tfvars file and save as `terraform.tfvars` # then edit to match your environment! edit terraform.tfvars.sample ``` + Here you have a terraform.tfvars example: + ```hcl # fetch the required id by running `gcloud beta billing accounts list` billing_account={ @@ -98,32 +114,38 @@ outputs_location = "~/fast-config" terraform init terraform apply -var bootstrap_user=$FAST_BU -# link the generated provider file -ln -s ~/fast-config/providers/00-bootstrap* . +# link providers file +ln -s ~/fast-config/providers/0-bootstrap-providers.tf ./ # re-run init and apply to remove user-level IAM terraform init -migrate-state + # answer 'yes' to terraform's question terraform apply ``` ## Stage 1 (Resource Management) + This stage performs two important tasks: + - Create the top-level hierarchy of folders, and the associated resources used later on to automate each part of the hierarchy (eg. Networking). - Set organization policies on the organization, and any exception required on specific folders. + ```bash # move to the 01-resman directory -cd $FAST_PWD/01-resman +cd $FAST_PWD/1-resman -# Link providers and variables from previous stages -ln -s ~/fast-config/providers/01-resman-providers.tf . -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . +# link providers and variables from previous stages +ln -s ~/fast-config/providers/1-resman-providers.tf . +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json . ln -s ~/fast-config/tfvars/globals.auto.tfvars.json . -# Edit your terraform.tfvars to append Teams configuration (optional) +# edit your terraform.tfvars to append Teams configuration (optional) edit terraform.tfvars ``` + In the following terraform.tfvars it is shown an example of configuration for teams provisioning: + ```hcl outputs_location = "~/fast-config" @@ -140,6 +162,7 @@ team_folders = { } } ``` + ```bash # run init and apply terraform init @@ -147,28 +170,34 @@ terraform apply ``` ## Stage 2 (Networking) + In this stage, we will deploy one of the 3 available Hub&Spoke networking topologies: + 1. VPC Peering 2. HA VPN 3. Multi-NIC appliances (NVA) + ```bash # move to the 02-networking-XXX directory (where XXX should be one of vpn|peering|nva) -cd $FAST_PWD/02-networking-XXX +cd $FAST_PWD/2-networking-XXX # setup providers and variables from previous stages -ln -s ~/fast-config/providers/02-networking-providers.tf . -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . +ln -s ~/fast-config/providers/2-networking-providers.tf . +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json . +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json . ln -s ~/fast-config/tfvars/globals.auto.tfvars.json . -# Create terraform.tfvars. output_location variable is required to generate networking stage output file +# create terraform.tfvars. output_location variable is required to generate networking stage output file edit terraform.tfvars ``` + In the following terraform.tfvars we configure output_location variable to generate networking stage output file: + ```hcl # path for automatic generation of configs outputs_location = "~/fast-config" ``` + ```bash # run init and apply terraform init @@ -176,21 +205,25 @@ terraform apply ``` ## Stage 2 (Security) + This stage sets up security resources (KMS and VPC-SC) and configurations which impact the whole organization, or are shared across the hierarchy to other projects and teams. + ```bash # move to the 02-security directory cd $FAST_PWD/02-security # link providers and variables from previous stages -ln -s ~/fast-config/providers/02-security-providers.tf . -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . +ln -s ~/fast-config/providers/2-security-providers.tf . +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json . +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json . ln -s ~/fast-config/tfvars/globals.auto.tfvars.json . -# Edit terraform.tfvars to include KMS and/or VPC-SC configuration +# edit terraform.tfvars to include KMS and/or VPC-SC configuration edit terraform.tfvars ``` -Some examples of terraform.tfvars configurations for KMS and VPC-SC can be found [here](02-security#customizations) + +Some examples of terraform.tfvars configurations for KMS and VPC-SC can be found [here](2-security#customizations) + ```bash # run init and apply terraform init @@ -198,21 +231,24 @@ terraform apply ``` ## Stage 3 (Project Factory) -The Project Factory stage builds on top of your foundations to create and set up projects (and related resources) to be used for your workloads. It is organized in folders representing environments (e.g. "dev", "prod"), each implemented by a stand-alone terraform resource factory. -```bash -# Variable `outputs_location` is set to `~/fast-config` -cd $FAST_PWD/03-project-factory/ENVIRONMENT -ln -s ~/fast-config/providers/03-project-factory-ENVIRONMENT-providers.tf . -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . -ln -s ~/fast-config/tfvars/02-networking.auto.tfvars.json . +The Project Factory stage builds on top of your foundations to create and set up projects (and related resources) to be used for your workloads. It is organized in folders representing environments (e.g. "dev", "prod"), each implemented by a stand-alone terraform resource factory. + +```bash +# variable `outputs_location` is set to `~/fast-config` +cd $FAST_PWD/3-project-factory/ENVIRONMENT +ln -s ~/fast-config/providers/3-project-factory-ENVIRONMENT-providers.tf . + +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json . +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json . +ln -s ~/fast-config/tfvars/2-networking.auto.tfvars.json . ln -s ~/fast-config/tfvars/globals.auto.tfvars.json . -# Define your environment default values (eg for billing alerts and labels) +# define your environment default values (eg for billing alerts and labels) edit data/defaults.yaml -# Create one yaml file per project to be created. Yaml file will include project configuration. Projects will be named after the filename +# create one YAML file per project to be created with project configuration +# filenames will be used for project ids cp data/projects/project.yaml.sample data/projects/YOUR_PROJECT_NAME.yaml edit data/projects/YOUR_PROJECT_NAME.yaml diff --git a/fast/stages/FAQ.md b/fast/stages/FAQ.md new file mode 100644 index 000000000..5245c8a96 --- /dev/null +++ b/fast/stages/FAQ.md @@ -0,0 +1,13 @@ +# FAST Mini FAQ + +- **How can the automation, logging and/or billing export projects be placed under specific folders instead of the org?** + - Run the bootstrap stage and let automation, logging and/or billing projects be created under the organization. + - Add the needed folders to the resource manager stage, or create them outside the stage in the console/gcloud or from a custom Terraform setup. + - Once folders have been created go back to the bootstrap stage, and edit your tfvars file by adding their ids to the `project_parent_ids` variable. + - Run the bootstrap stage again, the projects will be moved under the desired folders. +- **Why do we need two separate service accounts when configuring CI/CD pipelines (CI/CD SA and IaC SA)?** + - To have the pipeline workflow follow the same impersonation flow ([CI/CD SA impersonates IaC SA](IaC_SA.png)) used when applying Terraform manually (user impersonates IaC SA), which allows the pipeline to consume the same auto-generated provider files. + - To allow disabling pipeline credentials in case of issues with a single operation, by removing the ability of the CI/CD SA to impersonate the IaC SA. +- **How can I fix permission issues when running Terraform apply?** + - Make sure your account is part of the organization admin group defined in variables. + - Make sure you have configured [application default credentials](https://cloud.google.com/docs/authentication/application-default-credentials), rerun `gcloud auth login --update-adc` to fix them. diff --git a/fast/stages/IaC_SA.png b/fast/stages/IaC_SA.png new file mode 100644 index 000000000..8247f429e Binary files /dev/null and b/fast/stages/IaC_SA.png differ diff --git a/fast/stages/README.md b/fast/stages/README.md index 9b41bf1ca..acb14be6c 100644 --- a/fast/stages/README.md +++ b/fast/stages/README.md @@ -1,4 +1,4 @@ -# Fast stages +# FAST stages Each of the folders contained here is a separate "stage", or Terraform root module. @@ -9,7 +9,7 @@ When combined together, each stage is designed to leverage the previous stage's This has two important consequences - any stage can be swapped out and replaced by different code as long as it respects the contract by providing a predefined set of outputs and optionally accepting a predefined set of variables -- data flow between stages can be partially automated (see [stage 00 documentation on output files](./00-bootstrap/README.md#output-files-and-cross-stage-variables)), reducing the effort and pain required to compile variables by hand +- data flow between stages can be partially automated (see [stage 00 documentation on output files](./0-bootstrap/README.md#output-files-and-cross-stage-variables)), reducing the effort and pain required to compile variables by hand One important assumption is that the flow of data is always forward looking, so no stage needs to depend on outputs generated further down the chain. This greatly simplifies both the logic and the implementation, and allows stages to be effectively independent. @@ -19,28 +19,32 @@ Refer to each stage's documentation for a detailed description of its purpose, t To destroy a previous FAST deployment follow the instructions detailed in [cleanup](CLEANUP.md). -## Organizational level (00-01) +## Organization (0 and 1) -- [Bootstrap](00-bootstrap/README.md) +- [Bootstrap](0-bootstrap/README.md) Enables critical organization-level functionality that depends on broad permissions. It has two primary purposes. The first is to bootstrap the resources needed for automation of this and the following stages (service accounts, GCS buckets). And secondly, it applies the minimum amount of configuration needed at the organization level, to avoid the need of broad permissions later on, and to implement a minimum of security features like sinks and exports from the start.\ Exports: automation variables, organization-level custom roles -- [Resource Management](01-resman/README.md) +- [Resource Management](1-resman/README.md) Creates the base resource hierarchy (folders) and the automation resources required later to delegate deployment of each part of the hierarchy to separate stages. This stage also configures organization-level policies and any exceptions needed by different branches of the resource hierarchy.\ Exports: folder ids, automation service account emails -## Shared resources (02) +## Multitenancy -- [Security](02-security/README.md) +Implemented via separate stages that configure separate FAST-enabled hierarchies for each tenant, check the [multitenant stages folder](../stages-multitenant/). + +## Shared resources (2) + +- [Security](2-security/README.md) Manages centralized security configurations in a separate stage, and is typically owned by the security team. This stage implements VPC Security Controls via separate perimeters for environments and central services, and creates projects to host centralized KMS keys used by the whole organization. It's meant to be easily extended to include other security-related resources which are required, like Secret Manager.\ Exports: KMS key ids -- Networking ([VPN](02-networking-vpn/README.md)/[NVA](02-networking-nva/README.md)/[Peering](02-networking-separate-envs/README.md)/[Separate environments](02-networking-separate-envs/README.md)) - Manages centralized network resources in a separate stage, and is typically owned by the networking team. This stage implements a hub-and-spoke design, and includes connectivity via VPN to on-premises, and YAML-based factories for firewall rules (hierarchical and VPC-level) and subnets. It's currently available in four flavors: [spokes connected via VPN](02-networking-vpn/README.md), [and spokes connected via appliances](02-networking-nva/README.md), [spokes connected via VPC peering](02-networking-peering/README.md), and [separated network environments](02-networking-separate-envs/README.md).\ +- Networking ([Peering](2-networking-a-peering/README.md)/[VPN](2-networking-b-vpn/README.md)/[NVA](2-networking-c-nva/README.md)/[Separate environments](2-networking-d-separate-envs/README.md)) + Manages centralized network resources in a separate stage, and is typically owned by the networking team. This stage implements a hub-and-spoke design, and includes connectivity via VPN to on-premises, and YAML-based factories for firewall rules (hierarchical and VPC-level) and subnets. It's currently available in four flavors: [spokes connected via VPC peering](2-networking-a-peering/README.md), [spokes connected via VPN](2-networking-b-vpn/README.md), [and spokes connected via appliances](2-networking-c-nva/README.md), and [separated network environments](2-networking-d-separate-envs/README.md).\ Exports: host project ids and numbers, vpc self links -## Environment-level resources (03) +## Environment-level resources (3) -- [Project Factory](03-project-factory/dev/) +- [Project Factory](3-project-factory/dev/) YAML-based fatory to create and configure application or team-level projects. Configuration includes VPC-level settings for Shared VPC, service-level configuration for CMEK encryption via centralized keys, and service account creation for workloads and applications. This stage is meant to be used once per environment. -- [Data Platform](03-data-platform/dev/) -- [GKE Multitenant](03-gke-multitenant/dev/) +- [Data Platform](3-data-platform/dev/) +- [GKE Multitenant](3-gke-multitenant/dev/) - GCE Migration (in development) diff --git a/modules/__experimental/net-neg/README.md b/modules/__experimental/net-neg/README.md index a4113f0ca..cb271c505 100644 --- a/modules/__experimental/net-neg/README.md +++ b/modules/__experimental/net-neg/README.md @@ -7,11 +7,11 @@ Note: this module will integrated into a general-purpose load balancing module i ## Example ```hcl module "neg" { - source = "./fabric/modules/net-neg" + source = "./fabric/modules/__experimental/net-neg/" project_id = "myproject" name = "myneg" - network = module.vpc.self_link - subnetwork = module.vpc.subnet_self_links["europe-west1/default"] + network = var.vpc.self_link + subnetwork = var.subnet.self_link zone = "europe-west1-b" endpoints = [ for instance in module.vm.instances : @@ -22,6 +22,7 @@ module "neg" { } ] } +# tftest skip ``` diff --git a/modules/__experimental/net-neg/versions.tf b/modules/__experimental/net-neg/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/__experimental/net-neg/versions.tf +++ b/modules/__experimental/net-neg/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/api-gateway/README.md b/modules/api-gateway/README.md index 7c15f5814..d3c16d38c 100644 --- a/modules/api-gateway/README.md +++ b/modules/api-gateway/README.md @@ -1,4 +1,4 @@ -# Api Gateway +# API Gateway This module allows creating an API with its associated API config and API gateway. It also allows you grant IAM roles on the created resources. # Examples @@ -6,55 +6,55 @@ This module allows creating an API with its associated API config and API gatewa ## Basic example ```hcl module "gateway" { - source = "./fabric/modules/api-gateway" - project_id = "my-project" - api_id = "api" - region = "europe-west1" - spec = < diff --git a/modules/api-gateway/versions.tf b/modules/api-gateway/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/api-gateway/versions.tf +++ b/modules/api-gateway/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/apigee/README.md b/modules/apigee/README.md index 62035e3c5..0f3daa566 100644 --- a/modules/apigee/README.md +++ b/modules/apigee/README.md @@ -27,11 +27,15 @@ module "apigee" { apis-test = { display_name = "APIs test" description = "APIs Test" + deployment_type = "ARCHIVE" + api_proxy_type = "PROGRAMMABLE" envgroups = ["test"] } apis-prod = { display_name = "APIs prod" description = "APIs prod" + deployment_type = "PROXY" + api_proxy_type = "CONFIGURABLE" envgroups = ["prod"] iam = { "roles/viewer" = ["group:devops@myorg.com"] @@ -40,14 +44,16 @@ module "apigee" { } instances = { instance-test-ew1 = { - region = "europe-west1" - environments = ["apis-test"] - psa_ip_cidr_range = "10.0.4.0/22" + region = "europe-west1" + environments = ["apis-test"] + runtime_ip_cidr_range = "10.0.4.0/22" + troubleshooting_ip_cidr_range = "10.1.1.0.0/28" } instance-prod-ew3 = { - region = "europe-west3" - environments = ["apis-prod"] - psa_ip_cidr_range = "10.0.5.0/22" + region = "europe-west3" + environments = ["apis-prod"] + runtime_ip_cidr_range = "10.0.8.0/22" + troubleshooting_ip_cidr_range = "10.1.16.0/28" } } endpoint_attachments = { @@ -71,10 +77,10 @@ module "apigee" { source = "./fabric/modules/apigee" project_id = "my-project" organization = { - display_name = "My Organization" - description = "My Organization" - runtime_type = "HYBRID" - analytics_region = "europe-west1" + display_name = "My Organization" + description = "My Organization" + runtime_type = "HYBRID" + analytics_region = "europe-west1" } envgroups = { test = ["test.example.com"] @@ -82,14 +88,14 @@ module "apigee" { } environments = { apis-test = { - display_name = "APIs test" - description = "APIs Test" - envgroups = ["test"] + display_name = "APIs test" + description = "APIs Test" + envgroups = ["test"] } apis-prod = { - display_name = "APIs prod" - description = "APIs prod" - envgroups = ["prod"] + display_name = "APIs prod" + description = "APIs prod" + envgroups = ["prod"] iam = { "roles/viewer" = ["group:devops@myorg.com"] } @@ -120,9 +126,9 @@ module "apigee" { project_id = "my-project" environments = { apis-test = { - display_name = "APIs test" - description = "APIs Test" - envgroups = ["test"] + display_name = "APIs test" + description = "APIs Test" + envgroups = ["test"] } } } @@ -137,9 +143,10 @@ module "apigee" { project_id = "my-project" instances = { instance-test-ew1 = { - region = "europe-west1" - environments = ["apis-test"] - psa_ip_cidr_range = "10.0.4.0/22" + region = "europe-west1" + environments = ["apis-test"] + runtime_ip_cidr_range = "10.0.4.0/22" + troubleshooting_ip_cidr_range = "10.1.1.0/28" } } } @@ -169,23 +176,24 @@ module "apigee" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [project_id](variables.tf#L75) | Project ID. | string | ✓ | | +| [project_id](variables.tf#L78) | Project ID. | string | ✓ | | | [endpoint_attachments](variables.tf#L17) | Endpoint attachments. | map(object({…})) | | null | | [envgroups](variables.tf#L26) | Environment groups (NAME => [HOSTNAMES]). | map(list(string)) | | null | -| [environments](variables.tf#L32) | Environments. | map(object({…})) | | null | -| [instances](variables.tf#L47) | Instances. | map(object({…})) | | null | -| [organization](variables.tf#L61) | Apigee organization. If set to null the organization must already exist. | object({…}) | | null | +| [environments](variables.tf#L32) | Environments. | map(object({…})) | | null | +| [instances](variables.tf#L49) | Instances. | map(object({…})) | | null | +| [organization](variables.tf#L64) | Apigee organization. If set to null the organization must already exist. | object({…}) | | null | ## Outputs | name | description | sensitive | |---|---|:---:| -| [envgroups](outputs.tf#L17) | Environment groups. | | -| [environments](outputs.tf#L22) | Environment. | | -| [instances](outputs.tf#L27) | Instances. | | -| [org_id](outputs.tf#L32) | Organization ID. | | -| [org_name](outputs.tf#L37) | Organization name. | | -| [organization](outputs.tf#L42) | Organization. | | -| [service_attachments](outputs.tf#L47) | Service attachments. | | +| [endpoint_attachment_hosts](outputs.tf#L17) | Endpoint hosts. | | +| [envgroups](outputs.tf#L22) | Environment groups. | | +| [environments](outputs.tf#L27) | Environment. | | +| [instances](outputs.tf#L32) | Instances. | | +| [org_id](outputs.tf#L37) | Organization ID. | | +| [org_name](outputs.tf#L42) | Organization name. | | +| [organization](outputs.tf#L47) | Organization. | | +| [service_attachments](outputs.tf#L52) | Service attachments. | | diff --git a/modules/apigee/main.tf b/modules/apigee/main.tf index fe34a7382..aa2d076a2 100644 --- a/modules/apigee/main.tf +++ b/modules/apigee/main.tf @@ -40,10 +40,12 @@ resource "google_apigee_envgroup" "envgroups" { } resource "google_apigee_environment" "environments" { - for_each = local.environments - name = each.key - display_name = each.value.display_name - description = each.value.description + for_each = local.environments + name = each.key + display_name = each.value.display_name + description = each.value.description + deployment_type = each.value.deployment_type + api_proxy_type = each.value.api_proxy_type dynamic "node_config" { for_each = try(each.value.node_config, null) != null ? [""] : [] content { @@ -91,7 +93,7 @@ resource "google_apigee_instance" "instances" { description = each.value.description location = each.value.region org_id = local.org_id - ip_range = each.value.psa_ip_cidr_range + ip_range = "${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 } diff --git a/modules/apigee/outputs.tf b/modules/apigee/outputs.tf index a5e703881..74ad9f18d 100644 --- a/modules/apigee/outputs.tf +++ b/modules/apigee/outputs.tf @@ -14,6 +14,11 @@ * limitations under the License. */ +output "endpoint_attachment_hosts" { + description = "Endpoint hosts." + value = { for k, v in google_apigee_endpoint_attachment.endpoint_attachments : k => v.host } +} + output "envgroups" { description = "Environment groups." value = try(google_apigee_envgroup.envgroups, null) diff --git a/modules/apigee/variables.tf b/modules/apigee/variables.tf index 266f0d34e..00961aac2 100644 --- a/modules/apigee/variables.tf +++ b/modules/apigee/variables.tf @@ -32,8 +32,10 @@ variable "envgroups" { variable "environments" { description = "Environments." type = map(object({ - display_name = optional(string) - description = optional(string, "Terraform-managed") + display_name = optional(string) + description = optional(string, "Terraform-managed") + deployment_type = optional(string) + api_proxy_type = optional(string) node_config = optional(object({ min_node_count = optional(number) max_node_count = optional(number) @@ -47,13 +49,14 @@ variable "environments" { variable "instances" { description = "Instances." type = map(object({ - display_name = optional(string) - description = optional(string, "Terraform-managed") - region = string - environments = list(string) - psa_ip_cidr_range = string - disk_encryption_key = optional(string) - consumer_accept_list = optional(list(string)) + display_name = optional(string) + description = optional(string, "Terraform-managed") + region = string + environments = list(string) + runtime_ip_cidr_range = string + troubleshooting_ip_cidr_range = string + disk_encryption_key = optional(string) + consumer_accept_list = optional(list(string)) })) default = null } diff --git a/modules/apigee/versions.tf b/modules/apigee/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/apigee/versions.tf +++ b/modules/apigee/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/artifact-registry/versions.tf b/modules/artifact-registry/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/artifact-registry/versions.tf +++ b/modules/artifact-registry/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/bigquery-dataset/README.md b/modules/bigquery-dataset/README.md index 29acba39c..6dbd120c2 100644 --- a/modules/bigquery-dataset/README.md +++ b/modules/bigquery-dataset/README.md @@ -21,7 +21,7 @@ The access variables are split into `access` and `access_identities` variables, module "bigquery-dataset" { source = "./fabric/modules/bigquery-dataset" project_id = "my-project" - id = "my-dataset" + id = "my-dataset" access = { reader-group = { role = "READER", type = "group" } owner = { role = "OWNER", type = "user" } @@ -46,7 +46,7 @@ Access configuration can also be specified via IAM instead of basic roles via th module "bigquery-dataset" { source = "./fabric/modules/bigquery-dataset" project_id = "my-project" - id = "my-dataset" + id = "my-dataset" iam = { "roles/bigquery.dataOwner" = ["user:user1@example.org"] } @@ -67,6 +67,7 @@ module "bigquery-dataset" { default_table_expiration_ms = 3600000 default_partition_expiration_ms = null delete_contents_on_destroy = false + max_time_travel_hours = 168 } } # tftest modules=1 resources=1 @@ -178,7 +179,7 @@ module "bigquery-dataset" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [id](variables.tf#L69) | Dataset id. | string | ✓ | | -| [project_id](variables.tf#L100) | Id of the project where datasets will be created. | string | ✓ | | +| [project_id](variables.tf#L97) | Id of the project where datasets will be created. | string | ✓ | | | [access](variables.tf#L17) | Map of access rules with role and identity type. Keys are arbitrary and must match those in the `access_identities` variable, types are `domain`, `group`, `special_group`, `user`, `view`. | map(object({…})) | | {} | | [access_identities](variables.tf#L33) | Map of access identities used for basic access roles. View identities have the format 'project_id\|dataset_id\|table_id'. | map(string) | | {} | | [dataset_access](variables.tf#L39) | Set access in the dataset resource instead of using separate resources. | bool | | false | @@ -188,9 +189,9 @@ module "bigquery-dataset" { | [iam](variables.tf#L63) | IAM bindings in {ROLE => [MEMBERS]} format. Mutually exclusive with the access_* variables used for basic roles. | map(list(string)) | | {} | | [labels](variables.tf#L74) | Dataset labels. | map(string) | | {} | | [location](variables.tf#L80) | Dataset location. | string | | "EU" | -| [options](variables.tf#L86) | Dataset options. | object({…}) | | {…} | -| [tables](variables.tf#L105) | Table definitions. Options and partitioning default to null. Partitioning can only use `range` or `time`, set the unused one to null. | map(object({…})) | | {} | -| [views](variables.tf#L133) | View definitions. | map(object({…})) | | {} | +| [options](variables.tf#L86) | Dataset options. | object({…}) | | {} | +| [tables](variables.tf#L102) | Table definitions. Options and partitioning default to null. Partitioning can only use `range` or `time`, set the unused one to null. | map(object({…})) | | {} | +| [views](variables.tf#L130) | View definitions. | map(object({…})) | | {} | ## Outputs diff --git a/modules/bigquery-dataset/main.tf b/modules/bigquery-dataset/main.tf index 47f8fcb53..f832cd85c 100644 --- a/modules/bigquery-dataset/main.tf +++ b/modules/bigquery-dataset/main.tf @@ -42,7 +42,7 @@ resource "google_bigquery_dataset" "default" { delete_contents_on_destroy = var.options.delete_contents_on_destroy default_table_expiration_ms = var.options.default_table_expiration_ms default_partition_expiration_ms = var.options.default_partition_expiration_ms - + max_time_travel_hours = var.options.max_time_travel_hours dynamic "access" { for_each = var.dataset_access ? local.access_domain : {} content { diff --git a/modules/bigquery-dataset/variables.tf b/modules/bigquery-dataset/variables.tf index 5f8028abf..b44b66585 100644 --- a/modules/bigquery-dataset/variables.tf +++ b/modules/bigquery-dataset/variables.tf @@ -86,15 +86,12 @@ variable "location" { variable "options" { description = "Dataset options." type = object({ - default_table_expiration_ms = number - default_partition_expiration_ms = number - delete_contents_on_destroy = bool + default_table_expiration_ms = optional(number, null) + default_partition_expiration_ms = optional(number, null) + delete_contents_on_destroy = optional(bool, false) + max_time_travel_hours = optional(number, 168) }) - default = { - default_table_expiration_ms = null - default_partition_expiration_ms = null - delete_contents_on_destroy = false - } + default = {} } variable "project_id" { diff --git a/modules/bigquery-dataset/versions.tf b/modules/bigquery-dataset/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/bigquery-dataset/versions.tf +++ b/modules/bigquery-dataset/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/bigtable-instance/README.md b/modules/bigtable-instance/README.md index 1e9ade9c9..39e0bc536 100644 --- a/modules/bigtable-instance/README.md +++ b/modules/bigtable-instance/README.md @@ -4,9 +4,7 @@ This module allows managing a single BigTable instance, including access configu ## TODO -- [ ] support bigtable_gc_policy - [ ] support bigtable_app_profile -- [ ] support cluster replicas - [ ] support IAM for tables ## Examples @@ -16,25 +14,147 @@ This module allows managing a single BigTable instance, including access configu ```hcl module "bigtable-instance" { - source = "./fabric/modules/bigtable-instance" - project_id = "my-project" - name = "instance" - cluster_id = "instance" - zone = "europe-west1-b" - tables = { - test1 = null, - test2 = { - split_keys = ["a", "b", "c"] - column_family = null + source = "./fabric/modules/bigtable-instance" + project_id = "my-project" + name = "instance" + clusters = { + my-cluster = { + zone = "europe-west1-b" } } - iam = { + tables = { + test1 = {}, + test2 = { + split_keys = ["a", "b", "c"] + } + } + iam = { "roles/bigtable.user" = ["user:viewer@testdomain.com"] } } # tftest modules=1 resources=4 ``` +### Instance with tables and column families + +```hcl + +module "bigtable-instance" { + source = "./fabric/modules/bigtable-instance" + project_id = "my-project" + name = "instance" + clusters = { + my-cluster = { + zone = "europe-west1-b" + } + } + tables = { + test1 = {}, + test2 = { + split_keys = ["a", "b", "c"] + column_families = { + cf1 = {} + cf2 = {} + cf3 = {} + } + } + test3 = { + column_families = { + cf1 = {} + } + } + } +} +# tftest modules=1 resources=4 +``` + +### Instance with replication enabled + +```hcl + +module "bigtable-instance" { + source = "./fabric/modules/bigtable-instance" + project_id = "my-project" + name = "instance" + clusters = { + first-cluster = { + zone = "europe-west1-b" + } + second-cluster = { + zone = "europe-southwest1-a" + } + third-cluster = { + zone = "us-central1-b" + } + } +} +# tftest modules=1 resources=1 +``` + +### Instance with garbage collection policy + +```hcl + +module "bigtable-instance" { + source = "./fabric/modules/bigtable-instance" + project_id = "my-project" + name = "instance" + clusters = { + my-cluster = { + zone = "europe-west1-b" + } + } + tables = { + test1 = { + column_families = { + cf1 = { + gc_policy = { + deletion_policy = "ABANDON" + max_age = "18h" + } + } + cf2 = {} + } + } + } +} +# tftest modules=1 resources=3 +``` + +### Instance with default garbage collection policy + +The default garbage collection policy is applied to any column family that does +not specify a `gc_policy`. If a column family specifies a `gc_policy`, the +default garbage collection policy is ignored for that column family. + +```hcl + +module "bigtable-instance" { + source = "./fabric/modules/bigtable-instance" + project_id = "my-project" + name = "instance" + clusters = { + my-cluster = { + zone = "europe-west1-b" + } + } + default_gc_policy = { + deletion_policy = "ABANDON" + max_age = "18h" + max_version = 7 + } + tables = { + test1 = { + column_families = { + cf1 = {} + cf2 = {} + } + } + } +} +# tftest modules=1 resources=4 +``` + ### Instance with static number of nodes If you are not using autoscaling settings, you must set a specific number of nodes with the variable `num_nodes`. @@ -45,9 +165,12 @@ module "bigtable-instance" { source = "./fabric/modules/bigtable-instance" project_id = "my-project" name = "instance" - cluster_id = "instance" - zone = "europe-west1-b" - num_nodes = 5 + clusters = { + my-cluster = { + zone = "europe-west1-b" + num_nodes = 5 + } + } } # tftest modules=1 resources=1 ``` @@ -59,16 +182,21 @@ If you use autoscaling, you should not set the variable `num_nodes`. ```hcl module "bigtable-instance" { - source = "./fabric/modules/bigtable-instance" - project_id = "my-project" - name = "instance" - cluster_id = "instance" - zone = "europe-southwest1-b" - autoscaling_config = { - min_nodes = 3 - max_nodes = 7 - cpu_target = 70 + source = "./fabric/modules/bigtable-instance" + project_id = "my-project" + name = "instance" + clusters = { + my-cluster = { + zone = "europe-southwest1-b" + autoscaling = { + min_nodes = 3 + max_nodes = 7 + cpu_target = 70 + } + } } + + } # tftest modules=1 resources=1 ``` @@ -78,17 +206,20 @@ module "bigtable-instance" { ```hcl module "bigtable-instance" { - source = "./fabric/modules/bigtable-instance" - project_id = "my-project" - name = "instance" - cluster_id = "instance" - zone = "europe-southwest1-a" - storage_type = "SSD" - autoscaling_config = { - min_nodes = 3 - max_nodes = 7 - cpu_target = 70 - storage_target = 4096 + source = "./fabric/modules/bigtable-instance" + project_id = "my-project" + name = "instance" + clusters = { + my-cluster = { + zone = "europe-southwest1-a" + storage_type = "SSD" + autoscaling = { + min_nodes = 3 + max_nodes = 7 + cpu_target = 70 + storage_target = 4096 + } + } } } # tftest modules=1 resources=1 @@ -99,19 +230,16 @@ module "bigtable-instance" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L56) | The name of the Cloud Bigtable instance. | string | ✓ | | -| [project_id](variables.tf#L67) | Id of the project where datasets will be created. | string | ✓ | | -| [zone](variables.tf#L99) | The zone to create the Cloud Bigtable cluster in. | string | ✓ | | -| [autoscaling_config](variables.tf#L17) | Settings for autoscaling of the instance. If you set this variable, the variable num_nodes is ignored. | object({…}) | | null | -| [cluster_id](variables.tf#L28) | The ID of the Cloud Bigtable cluster. | string | | "europe-west1" | -| [deletion_protection](variables.tf#L34) | Whether or not to allow Terraform to destroy the instance. Unless this field is set to false in Terraform state, a terraform destroy or terraform apply that would delete the instance will fail. | | | true | -| [display_name](variables.tf#L39) | The human-readable display name of the Bigtable instance. | | | null | -| [iam](variables.tf#L44) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [instance_type](variables.tf#L50) | (deprecated) The instance type to create. One of 'DEVELOPMENT' or 'PRODUCTION'. | string | | null | -| [num_nodes](variables.tf#L61) | The number of nodes in your Cloud Bigtable cluster. This value is ignored if you are using autoscaling. | number | | 1 | -| [storage_type](variables.tf#L72) | The storage type to use. | string | | "SSD" | -| [table_options_defaults](variables.tf#L78) | Default option of tables created in the BigTable instance. | object({…}) | | {…} | -| [tables](variables.tf#L90) | Tables to be created in the BigTable instance, options can be null. | map(object({…})) | | {} | +| [clusters](variables.tf#L17) | Clusters to be created in the BigTable instance. Set more than one cluster to enable replication. If you set autoscaling, num_nodes will be ignored. | map(object({…})) | ✓ | | +| [name](variables.tf#L78) | The name of the Cloud Bigtable instance. | string | ✓ | | +| [project_id](variables.tf#L83) | Id of the project where datasets will be created. | string | ✓ | | +| [default_autoscaling](variables.tf#L33) | Default settings for autoscaling of clusters. This will be the default autoscaling for any cluster not specifying any autoscaling details. | object({…}) | | null | +| [default_gc_policy](variables.tf#L44) | Default garbage collection policy, to be applied to all column families and all tables. Can be override in the tables variable for specific column families. | object({…}) | | null | +| [deletion_protection](variables.tf#L56) | Whether or not to allow Terraform to destroy the instance. Unless this field is set to false in Terraform state, a terraform destroy or terraform apply that would delete the instance will fail. | | | true | +| [display_name](variables.tf#L61) | The human-readable display name of the Bigtable instance. | | | null | +| [iam](variables.tf#L66) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [instance_type](variables.tf#L72) | (deprecated) The instance type to create. One of 'DEVELOPMENT' or 'PRODUCTION'. | string | | null | +| [tables](variables.tf#L88) | Tables to be created in the BigTable instance. | map(object({…})) | | {} | ## Outputs diff --git a/modules/bigtable-instance/main.tf b/modules/bigtable-instance/main.tf index b5764c349..3a00eab89 100644 --- a/modules/bigtable-instance/main.tf +++ b/modules/bigtable-instance/main.tf @@ -15,34 +15,53 @@ */ locals { - tables = { - for k, v in var.tables : k => v != null ? v : var.table_options_defaults + gc_pairs = flatten([ + for table, table_obj in var.tables : [ + for cf, cf_obj in table_obj.column_families : { + table = table + column_family = cf + gc_policy = cf_obj.gc_policy == null ? var.default_gc_policy : cf_obj.gc_policy + } + ] + ]) + + clusters_autoscaling = { + for cluster_id, cluster in var.clusters : cluster_id => { + zone = cluster.zone + storage_type = cluster.storage_type + num_nodes = cluster.autoscaling == null && var.default_autoscaling == null ? cluster.num_nodes : null + autoscaling = cluster.autoscaling == null ? var.default_autoscaling : cluster.autoscaling + } } - num_nodes = var.autoscaling_config == null ? var.num_nodes : null } resource "google_bigtable_instance" "default" { project = var.project_id name = var.name - cluster { - cluster_id = var.cluster_id - zone = var.zone - storage_type = var.storage_type - num_nodes = local.num_nodes - dynamic "autoscaling_config" { - for_each = var.autoscaling_config == null ? [] : [""] - content { - min_nodes = var.autoscaling_config.min_nodes - max_nodes = var.autoscaling_config.max_nodes - cpu_target = var.autoscaling_config.cpu_target - storage_target = var.autoscaling_config.storage_target + + instance_type = var.instance_type + display_name = var.display_name == null ? var.display_name : var.name + deletion_protection = var.deletion_protection + + dynamic "cluster" { + for_each = local.clusters_autoscaling + content { + cluster_id = cluster.key + zone = cluster.value.zone + storage_type = cluster.value.storage_type + num_nodes = cluster.value.num_nodes + + dynamic "autoscaling_config" { + for_each = cluster.value.autoscaling == null ? [] : [""] + content { + min_nodes = cluster.value.autoscaling.min_nodes + max_nodes = cluster.value.autoscaling.max_nodes + cpu_target = cluster.value.autoscaling.cpu_target + storage_target = cluster.value.autoscaling.storage_target + } } } } - instance_type = var.instance_type - - display_name = var.display_name == null ? var.display_name : var.name - deletion_protection = var.deletion_protection } resource "google_bigtable_instance_iam_binding" "default" { @@ -54,17 +73,44 @@ resource "google_bigtable_instance_iam_binding" "default" { } resource "google_bigtable_table" "default" { - for_each = local.tables + for_each = var.tables project = var.project_id instance_name = google_bigtable_instance.default.name name = each.key split_keys = each.value.split_keys dynamic "column_family" { - for_each = each.value.column_family != null ? [""] : [] + for_each = each.value.column_families content { - family = each.value.column_family + family = column_family.key + } + } +} + +resource "google_bigtable_gc_policy" "default" { + for_each = { for k, v in local.gc_pairs : k => v if v.gc_policy != null } + + table = each.value.table + column_family = each.value.column_family + instance_name = google_bigtable_instance.default.name + project = var.project_id + + gc_rules = try(each.value.gc_policy.gc_rules, null) + mode = try(each.value.gc_policy.mode, null) + deletion_policy = try(each.value.gc_policy.deletion_policy, null) + + dynamic "max_age" { + for_each = try(each.value.gc_policy.max_age, null) != null ? [""] : [] + content { + duration = each.value.gc_policy.max_age + } + } + + dynamic "max_version" { + for_each = try(each.value.gc_policy.max_version, null) != null ? [""] : [] + content { + number = each.value.gc_policy.max_version } } } diff --git a/modules/bigtable-instance/variables.tf b/modules/bigtable-instance/variables.tf index 84a6013b6..f7b75c135 100644 --- a/modules/bigtable-instance/variables.tf +++ b/modules/bigtable-instance/variables.tf @@ -14,21 +14,43 @@ * limitations under the License. */ -variable "autoscaling_config" { - description = "Settings for autoscaling of the instance. If you set this variable, the variable num_nodes is ignored." +variable "clusters" { + description = "Clusters to be created in the BigTable instance. Set more than one cluster to enable replication. If you set autoscaling, num_nodes will be ignored." + nullable = false + type = map(object({ + zone = optional(string) + storage_type = optional(string) + num_nodes = optional(number) + autoscaling = optional(object({ + min_nodes = number + max_nodes = number + cpu_target = number + storage_target = optional(number) + })) + })) +} + +variable "default_autoscaling" { + description = "Default settings for autoscaling of clusters. This will be the default autoscaling for any cluster not specifying any autoscaling details." type = object({ min_nodes = number max_nodes = number - cpu_target = number, - storage_target = optional(number, null) + cpu_target = number + storage_target = optional(number) }) default = null } -variable "cluster_id" { - description = "The ID of the Cloud Bigtable cluster." - type = string - default = "europe-west1" +variable "default_gc_policy" { + description = "Default garbage collection policy, to be applied to all column families and all tables. Can be override in the tables variable for specific column families." + type = object({ + deletion_policy = optional(string) + gc_rules = optional(string) + mode = optional(string) + max_age = optional(string) + max_version = optional(string) + }) + default = null } variable "deletion_protection" { @@ -58,45 +80,26 @@ variable "name" { type = string } -variable "num_nodes" { - description = "The number of nodes in your Cloud Bigtable cluster. This value is ignored if you are using autoscaling." - type = number - default = 1 -} - variable "project_id" { description = "Id of the project where datasets will be created." type = string } -variable "storage_type" { - description = "The storage type to use." - type = string - default = "SSD" -} - -variable "table_options_defaults" { - description = "Default option of tables created in the BigTable instance." - type = object({ - split_keys = list(string) - column_family = string - }) - default = { - split_keys = [] - column_family = null - } -} - variable "tables" { - description = "Tables to be created in the BigTable instance, options can be null." + description = "Tables to be created in the BigTable instance." + nullable = false type = map(object({ - split_keys = list(string) - column_family = string + split_keys = optional(list(string), []) + column_families = optional(map(object( + { + gc_policy = optional(object({ + deletion_policy = optional(string) + gc_rules = optional(string) + mode = optional(string) + max_age = optional(string) + max_version = optional(string) + }), null) + })), {}) })) default = {} } - -variable "zone" { - description = "The zone to create the Cloud Bigtable cluster in." - type = string -} diff --git a/modules/bigtable-instance/versions.tf b/modules/bigtable-instance/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/bigtable-instance/versions.tf +++ b/modules/bigtable-instance/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/billing-budget/README.md b/modules/billing-budget/README.md index 44231d049..d995c0e0a 100644 --- a/modules/billing-budget/README.md +++ b/modules/billing-budget/README.md @@ -29,7 +29,7 @@ module "budget" { ] email_recipients = { project_id = "my-project" - emails = ["user@example.com"] + emails = ["user@example.com"] } } # tftest modules=1 resources=2 diff --git a/modules/billing-budget/versions.tf b/modules/billing-budget/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/billing-budget/versions.tf +++ b/modules/billing-budget/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/binauthz/README.md b/modules/binauthz/README.md index fa0cd71ba..960a7da31 100644 --- a/modules/binauthz/README.md +++ b/modules/binauthz/README.md @@ -8,8 +8,8 @@ This module simplifies the creation of a Binary Authorization policy, attestors ```hcl module "binauthz" { - source = "./fabric/modules/binauthz" - project_id = "my_project" + source = "./fabric/modules/binauthz" + project_id = "my_project" global_policy_evaluation_mode = "DISABLE" default_admission_rule = { evaluation_mode = "ALWAYS_DENY" @@ -18,16 +18,16 @@ module "binauthz" { } cluster_admission_rules = { "europe-west1-c.cluster" = { - evaluation_mode = "REQUIRE_ATTESTATION" - enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG" - attestors = [ "test" ] + evaluation_mode = "REQUIRE_ATTESTATION" + enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG" + attestors = ["test"] } } attestors_config = { - "test": { - note_reference = null - pgp_public_keys = [ - < @@ -62,12 +72,10 @@ module "on-prem" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [vpn_config](variables.tf#L35) | VPN configuration, type must be one of 'dynamic' or 'static'. | object({…}) | ✓ | | +| [vpn_config](variables.tf#L35) | VPN configuration, type must be one of 'dynamic' or 'static'. | object({…}) | ✓ | | | [config_variables](variables.tf#L17) | Additional variables used to render the cloud-config and CoreDNS templates. | map(any) | | {} | | [coredns_config](variables.tf#L23) | CoreDNS configuration path, if null default will be used. | string | | null | | [local_ip_cidr_range](variables.tf#L29) | IP CIDR range used for the Docker onprem network. | string | | "192.168.192.0/24" | -| [test_instance](variables-instance.tf#L17) | Test/development instance attributes, leave null to skip creation. | object({…}) | | null | -| [test_instance_defaults](variables-instance.tf#L30) | Test/development instance defaults used for optional configuration. If image is null, COS stable will be used. | object({…}) | | {…} | | [vpn_dynamic_config](variables.tf#L46) | BGP configuration for dynamic VPN, ignored if VPN type is 'static'. | object({…}) | | {…} | | [vpn_static_ranges](variables.tf#L70) | Remote CIDR ranges for static VPN, ignored if VPN type is 'dynamic'. | list(string) | | ["10.0.0.0/8"] | @@ -76,7 +84,5 @@ module "on-prem" { | name | description | sensitive | |---|---|:---:| | [cloud_config](outputs.tf#L17) | Rendered cloud-config file to be passed as user-data instance metadata. | | -| [test_instance](outputs-instance.tf#L17) | Optional test instance name and address. | | - diff --git a/modules/cloud-config-container/onprem/cloud-config.yaml b/modules/cloud-config-container/__need_fixing/onprem/cloud-config.yaml similarity index 100% rename from modules/cloud-config-container/onprem/cloud-config.yaml rename to modules/cloud-config-container/__need_fixing/onprem/cloud-config.yaml diff --git a/modules/cloud-config-container/onprem/docker-images/README.md b/modules/cloud-config-container/__need_fixing/onprem/docker-images/README.md similarity index 100% rename from modules/cloud-config-container/onprem/docker-images/README.md rename to modules/cloud-config-container/__need_fixing/onprem/docker-images/README.md diff --git a/modules/cloud-config-container/onprem/docker-images/strongswan/Dockerfile b/modules/cloud-config-container/__need_fixing/onprem/docker-images/strongswan/Dockerfile similarity index 100% rename from modules/cloud-config-container/onprem/docker-images/strongswan/Dockerfile rename to modules/cloud-config-container/__need_fixing/onprem/docker-images/strongswan/Dockerfile diff --git a/modules/cloud-config-container/onprem/docker-images/strongswan/README.md b/modules/cloud-config-container/__need_fixing/onprem/docker-images/strongswan/README.md similarity index 100% rename from modules/cloud-config-container/onprem/docker-images/strongswan/README.md rename to modules/cloud-config-container/__need_fixing/onprem/docker-images/strongswan/README.md diff --git a/modules/cloud-config-container/onprem/docker-images/strongswan/cloudbuild.yaml b/modules/cloud-config-container/__need_fixing/onprem/docker-images/strongswan/cloudbuild.yaml similarity index 100% rename from modules/cloud-config-container/onprem/docker-images/strongswan/cloudbuild.yaml rename to modules/cloud-config-container/__need_fixing/onprem/docker-images/strongswan/cloudbuild.yaml diff --git a/modules/cloud-config-container/onprem/docker-images/strongswan/entrypoint.sh b/modules/cloud-config-container/__need_fixing/onprem/docker-images/strongswan/entrypoint.sh similarity index 100% rename from modules/cloud-config-container/onprem/docker-images/strongswan/entrypoint.sh rename to modules/cloud-config-container/__need_fixing/onprem/docker-images/strongswan/entrypoint.sh diff --git a/modules/cloud-config-container/onprem/docker-images/strongswan/ipsec-vti.sh b/modules/cloud-config-container/__need_fixing/onprem/docker-images/strongswan/ipsec-vti.sh similarity index 100% rename from modules/cloud-config-container/onprem/docker-images/strongswan/ipsec-vti.sh rename to modules/cloud-config-container/__need_fixing/onprem/docker-images/strongswan/ipsec-vti.sh diff --git a/modules/cloud-config-container/onprem/docker-images/toolbox/Dockerfile b/modules/cloud-config-container/__need_fixing/onprem/docker-images/toolbox/Dockerfile similarity index 100% rename from modules/cloud-config-container/onprem/docker-images/toolbox/Dockerfile rename to modules/cloud-config-container/__need_fixing/onprem/docker-images/toolbox/Dockerfile diff --git a/modules/cloud-config-container/onprem/docker-images/toolbox/README.md b/modules/cloud-config-container/__need_fixing/onprem/docker-images/toolbox/README.md similarity index 100% rename from modules/cloud-config-container/onprem/docker-images/toolbox/README.md rename to modules/cloud-config-container/__need_fixing/onprem/docker-images/toolbox/README.md diff --git a/modules/cloud-config-container/onprem/docker-images/toolbox/cloudbuild.yaml b/modules/cloud-config-container/__need_fixing/onprem/docker-images/toolbox/cloudbuild.yaml similarity index 100% rename from modules/cloud-config-container/onprem/docker-images/toolbox/cloudbuild.yaml rename to modules/cloud-config-container/__need_fixing/onprem/docker-images/toolbox/cloudbuild.yaml diff --git a/modules/cloud-config-container/onprem/docker-images/toolbox/entrypoint.sh b/modules/cloud-config-container/__need_fixing/onprem/docker-images/toolbox/entrypoint.sh similarity index 100% rename from modules/cloud-config-container/onprem/docker-images/toolbox/entrypoint.sh rename to modules/cloud-config-container/__need_fixing/onprem/docker-images/toolbox/entrypoint.sh diff --git a/modules/cloud-config-container/onprem/main.tf b/modules/cloud-config-container/__need_fixing/onprem/main.tf similarity index 100% rename from modules/cloud-config-container/onprem/main.tf rename to modules/cloud-config-container/__need_fixing/onprem/main.tf diff --git a/modules/cloud-config-container/onprem/outputs.tf b/modules/cloud-config-container/__need_fixing/onprem/outputs.tf similarity index 100% rename from modules/cloud-config-container/onprem/outputs.tf rename to modules/cloud-config-container/__need_fixing/onprem/outputs.tf diff --git a/modules/cloud-config-container/onprem/static-vpn-gw-cloud-init.yaml b/modules/cloud-config-container/__need_fixing/onprem/static-vpn-gw-cloud-init.yaml similarity index 100% rename from modules/cloud-config-container/onprem/static-vpn-gw-cloud-init.yaml rename to modules/cloud-config-container/__need_fixing/onprem/static-vpn-gw-cloud-init.yaml diff --git a/modules/cloud-config-container/onprem/variables.tf b/modules/cloud-config-container/__need_fixing/onprem/variables.tf similarity index 94% rename from modules/cloud-config-container/onprem/variables.tf rename to modules/cloud-config-container/__need_fixing/onprem/variables.tf index 3b09e2366..06eb27603 100644 --- a/modules/cloud-config-container/onprem/variables.tf +++ b/modules/cloud-config-container/__need_fixing/onprem/variables.tf @@ -37,9 +37,9 @@ variable "vpn_config" { type = object({ peer_ip = string shared_secret = string - type = string - peer_ip2 = string - shared_secret2 = string + type = optional(string, "static") + peer_ip2 = optional(string) + shared_secret2 = optional(string) }) } diff --git a/modules/cloud-config-container/__need_fixing/onprem/versions.tf b/modules/cloud-config-container/__need_fixing/onprem/versions.tf new file mode 100644 index 000000000..08492c6f9 --- /dev/null +++ b/modules/cloud-config-container/__need_fixing/onprem/versions.tf @@ -0,0 +1,29 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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. + +terraform { + required_version = ">= 1.3.1" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.50.0" # tftest + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.50.0" # tftest + } + } +} + + diff --git a/modules/cloud-config-container/coredns/README.md b/modules/cloud-config-container/coredns/README.md index f5a51a389..4abe69e32 100644 --- a/modules/cloud-config-container/coredns/README.md +++ b/modules/cloud-config-container/coredns/README.md @@ -24,17 +24,30 @@ This example will create a `cloud-config` that uses the module's defaults, creat ```hcl module "cos-coredns" { - source = "./fabric/modules/cloud-config-container/coredns" + source = "./fabric/modules/cloud-config-container/coredns" } -# use it as metadata in a compute instance or template -module "vm-coredns" { - source = "./fabric/modules/compute-vm" +module "vm" { + source = "./fabric/modules/compute-vm" + project_id = "my-project" + zone = "europe-west8-b" + name = "cos-coredns" + network_interfaces = [{ + network = "default" + subnetwork = "gce" + }] metadata = { user-data = module.cos-coredns.cloud_config google-logging-enabled = true } + boot_disk = { + image = "projects/cos-cloud/global/images/family/cos-stable" + type = "pd-ssd" + size = 10 + } + tags = ["dns", "ssh"] } +# tftest modules=1 resources=1 ``` ### Custom CoreDNS configuration @@ -43,7 +56,7 @@ This example will create a `cloud-config` using a custom CoreDNS configuration, ```hcl module "cos-coredns" { - source = "./fabric/modules/cloud-config-container/coredns" + source = "./fabric/modules/cloud-config-container/coredns" coredns_config = "./fabric/modules/cloud-config-container/coredns/Corefile-hosts" files = { "/etc/coredns/example.hosts" = { @@ -51,25 +64,9 @@ module "cos-coredns" { owner = null permissions = "0644" } -} -``` - -### CoreDNS instance - -This example shows how to create the single instance optionally managed by the module, providing all required attributes in the `test_instance` variable. The instance is purposefully kept simple and should only be used in development, or when designing infrastructures. - -```hcl -module "cos-coredns" { - source = "./fabric/modules/cloud-config-container/coredns" - test_instance = { - project_id = "my-project" - zone = "europe-west1-b" - name = "cos-coredns" - type = "f1-micro" - network = "default" - subnetwork = "https://www.googleapis.com/compute/v1/projects/my-project/regions/europe-west1/subnetworks/my-subnet" } } +# tftest modules=0 resources=0 ``` @@ -82,14 +79,11 @@ module "cos-coredns" { | [coredns_config](variables.tf#L29) | CoreDNS configuration path, if null default will be used. | string | | null | | [file_defaults](variables.tf#L35) | Default owner and permissions for files. | object({…}) | | {…} | | [files](variables.tf#L47) | Map of extra files to create on the instance, path as key. Owner and permissions will use defaults if null. | map(object({…})) | | {} | -| [test_instance](variables-instance.tf#L17) | Test/development instance attributes, leave null to skip creation. | object({…}) | | null | -| [test_instance_defaults](variables-instance.tf#L30) | Test/development instance defaults used for optional configuration. If image is null, COS stable will be used. | object({…}) | | {…} | ## Outputs | name | description | sensitive | |---|---|:---:| | [cloud_config](outputs.tf#L17) | Rendered cloud-config file to be passed as user-data instance metadata. | | -| [test_instance](outputs-instance.tf#L17) | Optional test instance name and address. | | diff --git a/modules/cloud-config-container/coredns/instance.tf b/modules/cloud-config-container/coredns/instance.tf deleted file mode 120000 index bdef596b6..000000000 --- a/modules/cloud-config-container/coredns/instance.tf +++ /dev/null @@ -1 +0,0 @@ -../instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/coredns/outputs-instance.tf b/modules/cloud-config-container/coredns/outputs-instance.tf deleted file mode 120000 index ea9e24045..000000000 --- a/modules/cloud-config-container/coredns/outputs-instance.tf +++ /dev/null @@ -1 +0,0 @@ -../outputs-instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/coredns/variables-instance.tf b/modules/cloud-config-container/coredns/variables-instance.tf deleted file mode 120000 index 94af61e4d..000000000 --- a/modules/cloud-config-container/coredns/variables-instance.tf +++ /dev/null @@ -1 +0,0 @@ -../variables-instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/coredns/versions.tf b/modules/cloud-config-container/coredns/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/cloud-config-container/coredns/versions.tf +++ b/modules/cloud-config-container/coredns/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/cloud-config-container/cos-generic-metadata/README.md b/modules/cloud-config-container/cos-generic-metadata/README.md index 16d1935ed..88073986d 100644 --- a/modules/cloud-config-container/cos-generic-metadata/README.md +++ b/modules/cloud-config-container/cos-generic-metadata/README.md @@ -12,31 +12,27 @@ This example will create a `cloud-config` that starts [Envoy Proxy](https://www. ```hcl module "cos-envoy" { - source = "./fabric/modules/cos-generic-metadata" - + source = "./fabric/modules/cloud-config-container/cos-generic-metadata" container_image = "envoyproxy/envoy:v1.14.1" container_name = "envoy" container_args = "-c /etc/envoy/envoy.yaml --log-level info --allow-unknown-static-fields" - container_volumes = [ { host = "/etc/envoy/envoy.yaml", container = "/etc/envoy/envoy.yaml" } ] - docker_args = "--network host --pid host" - + # file paths are mocked to run this example in tests files = { "/var/run/envoy/customize.sh" = { - content = file("customize.sh") + content = file("/dev/null") # file("customize.sh") owner = "root" permissions = "0744" } "/etc/envoy/envoy.yaml" = { - content = file("envoy.yaml") + content = file("/dev/null") # file("envoy.yaml") owner = "root" permissions = "0644" } } - run_commands = [ "iptables -t nat -N ENVOY_IN_REDIRECT", "iptables -t nat -A ENVOY_IN_REDIRECT -p tcp -j REDIRECT --to-port 15001", @@ -46,14 +42,13 @@ module "cos-envoy" { "systemctl daemon-reload", "systemctl start envoy", ] - - users = [ - { - username = "envoy", - uid = 1337 - } - ] + users = [{ + username = "envoy", + uid = 1337 + }] } + +# tftest modules=0 resources=0 ``` diff --git a/modules/cloud-config-container/cos-generic-metadata/versions.tf b/modules/cloud-config-container/cos-generic-metadata/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/cloud-config-container/cos-generic-metadata/versions.tf +++ b/modules/cloud-config-container/cos-generic-metadata/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/cloud-config-container/envoy-traffic-director/README.md b/modules/cloud-config-container/envoy-traffic-director/README.md index 593cfa83c..caa0ec5ec 100644 --- a/modules/cloud-config-container/envoy-traffic-director/README.md +++ b/modules/cloud-config-container/envoy-traffic-director/README.md @@ -11,38 +11,31 @@ This module depends on the [`cos-generic-metadata` module](../cos-generic-metada ### Default configuration ```hcl -# Envoy TD config module "cos-envoy-td" { source = "./fabric/modules/cloud-config-container/envoy-traffic-director" } -# COS VM -module "vm-cos" { +module "vm" { source = "./fabric/modules/compute-vm" - project_id = local.project_id - zone = local.zone + project_id = "my-project" + zone = "europe-west8-b" name = "cos-envoy-td" network_interfaces = [{ - network = local.vpc.self_link, - subnetwork = local.vpc.subnet_self_link, - nat = false, - addresses = null + network = "default" + subnetwork = "gce" }] - tags = ["ssh", "http"] - metadata = { user-data = module.cos-envoy-td.cloud_config google-logging-enabled = true } - boot_disk = { image = "projects/cos-cloud/global/images/family/cos-stable" type = "pd-ssd" size = 10 } - - service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"] + tags = ["http-server", "ssh"] } +# tftest modules=1 resources=1 ``` diff --git a/modules/cloud-config-container/envoy-traffic-director/versions.tf b/modules/cloud-config-container/envoy-traffic-director/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/cloud-config-container/envoy-traffic-director/versions.tf +++ b/modules/cloud-config-container/envoy-traffic-director/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/cloud-config-container/instance.tf b/modules/cloud-config-container/instance.tf deleted file mode 100644 index 7f76f2183..000000000 --- a/modules/cloud-config-container/instance.tf +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -resource "google_service_account" "default" { - count = var.test_instance == null ? 0 : 1 - project = var.test_instance.project_id - account_id = "fabric-container-${var.test_instance.name}" - display_name = "Managed by the cos Terraform module." -} - -resource "google_project_iam_member" "default" { - for_each = ( - var.test_instance == null - ? toset([]) - : toset(var.test_instance_defaults.service_account_roles) - ) - project = var.test_instance.project_id - role = each.value - member = "serviceAccount:${google_service_account.default[0].email}" -} - -resource "google_compute_disk" "disks" { - for_each = ( - var.test_instance == null - ? {} - : var.test_instance_defaults.disks - ) - project = var.test_instance.project_id - zone = var.test_instance.zone - name = each.key - type = "pd-ssd" - size = each.value.size -} - -resource "google_compute_instance" "default" { - count = var.test_instance == null ? 0 : 1 - project = var.test_instance.project_id - zone = var.test_instance.zone - name = var.test_instance.name - description = "Managed by the cos Terraform module." - tags = var.test_instance_defaults.tags - machine_type = ( - var.test_instance.type == null ? "f1-micro" : var.test_instance.type - ) - metadata = merge(var.test_instance_defaults.metadata, { - user-data = local.cloud_config - }) - - dynamic "attached_disk" { - for_each = var.test_instance_defaults.disks - iterator = disk - content { - device_name = disk.key - mode = disk.value.read_only ? "READ_ONLY" : "READ_WRITE" - source = google_compute_disk.disks[disk.key].name - } - } - - boot_disk { - initialize_params { - type = "pd-ssd" - image = ( - var.test_instance_defaults.image == null - ? "projects/cos-cloud/global/images/family/cos-stable" - : var.test_instance_defaults.image - ) - size = 10 - } - } - - network_interface { - network = var.test_instance.network - subnetwork = var.test_instance.subnetwork - dynamic "access_config" { - for_each = var.test_instance_defaults.nat ? [""] : [] - iterator = config - content { - nat_ip = null - } - } - } - - service_account { - email = google_service_account.default[0].email - scopes = ["https://www.googleapis.com/auth/cloud-platform"] - } - -} diff --git a/modules/cloud-config-container/mysql/README.md b/modules/cloud-config-container/mysql/README.md index 535a77afe..31045804d 100644 --- a/modules/cloud-config-container/mysql/README.md +++ b/modules/cloud-config-container/mysql/README.md @@ -26,18 +26,31 @@ This example will create a `cloud-config` that uses the container's default conf ```hcl module "cos-mysql" { - source = "./fabric/modules/cos-container/mysql" + source = "./fabric/modules/cloud-config-container/mysql" mysql_password = "foo" } -# use it as metadata in a compute instance or template -module "vm-mysql" { - source = "./fabric/modules/compute-vm" +module "vm" { + source = "./fabric/modules/compute-vm" + project_id = "my-project" + zone = "europe-west8-b" + name = "cos-mysql" + network_interfaces = [{ + network = "default" + subnetwork = "gce" + }] metadata = { user-data = module.cos-mysql.cloud_config google-logging-enabled = true } + boot_disk = { + image = "projects/cos-cloud/global/images/family/cos-stable" + type = "pd-ssd" + size = 10 + } + tags = ["mysql", "ssh"] } +# tftest modules=1 resources=1 ``` ### Custom MySQL configuration and KMS encrypted password @@ -46,35 +59,17 @@ This example will create a `cloud-config` that uses a custom MySQL configuration ```hcl module "cos-mysql" { - source = "./fabric/modules/cos-container/mysql" + source = "./fabric/modules/cloud-config-container/mysql" mysql_config = "./my.cnf" mysql_password = "CiQAsd7WY==" - kms_config = { + kms_config = { project_id = "my-project" keyring = "test-cos" location = "europe-west1" key = "mysql" } } -``` - -### MySQL instance - -This example shows how to create the single instance optionally managed by the module, providing all required attributes in the `test_instance` variable. The instance is purposefully kept simple and should only be used in development, or when designing infrastructures. - -```hcl -module "cos-mysql" { - source = "./fabric/modules/cos-container/mysql" - mysql_password = "foo" - test_instance = { - project_id = "my-project" - zone = "europe-west1-b" - name = "cos-mysql" - type = "n1-standard-1" - network = "default" - subnetwork = "https://www.googleapis.com/compute/v1/projects/my-project/regions/europe-west1/subnetworks/my-subnet" - } -} +# tftest modules=0 resources=0 ``` @@ -89,14 +84,11 @@ module "cos-mysql" { | [kms_config](variables.tf#L35) | Optional KMS configuration to decrypt passed-in password. Leave null if a plaintext password is used. | object({…}) | | null | | [mysql_config](variables.tf#L46) | MySQL configuration file content, if null container default will be used. | string | | null | | [mysql_data_disk](variables.tf#L52) | MySQL data disk name in /dev/disk/by-id/ including the google- prefix. If null the boot disk will be used for data. | string | | null | -| [test_instance](variables-instance.tf#L17) | Test/development instance attributes, leave null to skip creation. | object({…}) | | null | -| [test_instance_defaults](variables-instance.tf#L30) | Test/development instance defaults used for optional configuration. If image is null, COS stable will be used. | object({…}) | | {…} | ## Outputs | name | description | sensitive | |---|---|:---:| | [cloud_config](outputs.tf#L17) | Rendered cloud-config file to be passed as user-data instance metadata. | | -| [test_instance](outputs-instance.tf#L17) | Optional test instance name and address. | | diff --git a/modules/cloud-config-container/mysql/instance.tf b/modules/cloud-config-container/mysql/instance.tf deleted file mode 120000 index bdef596b6..000000000 --- a/modules/cloud-config-container/mysql/instance.tf +++ /dev/null @@ -1 +0,0 @@ -../instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/mysql/outputs-instance.tf b/modules/cloud-config-container/mysql/outputs-instance.tf deleted file mode 120000 index ea9e24045..000000000 --- a/modules/cloud-config-container/mysql/outputs-instance.tf +++ /dev/null @@ -1 +0,0 @@ -../outputs-instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/mysql/variables-instance.tf b/modules/cloud-config-container/mysql/variables-instance.tf deleted file mode 120000 index 94af61e4d..000000000 --- a/modules/cloud-config-container/mysql/variables-instance.tf +++ /dev/null @@ -1 +0,0 @@ -../variables-instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/mysql/versions.tf b/modules/cloud-config-container/mysql/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/cloud-config-container/mysql/versions.tf +++ b/modules/cloud-config-container/mysql/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/cloud-config-container/nginx-tls/README.md b/modules/cloud-config-container/nginx-tls/README.md index d5790cf23..9dfe9b061 100644 --- a/modules/cloud-config-container/nginx-tls/README.md +++ b/modules/cloud-config-container/nginx-tls/README.md @@ -1,48 +1,37 @@ # Containerized Nginx with self-signed TLS on Container Optimized OS -This module manages a `cloud-config` configuration that starts a containerized Nginx with a self-signed TLS cert on Container Optimized OS. -This can be useful if you need quickly a VM or instance group answering HTTPS for prototyping. +This module manages a `cloud-config` configuration that starts a containerized Nginx with a self-signed TLS cert on Container Optimized OS. This can be useful if you need quickly a VM or instance group answering HTTPS for prototyping. The generated cloud config is rendered in the `cloud_config` output, and is meant to be used in instances or instance templates via the `user-data` metadata. -This module depends on the [`cos-generic-metadata` module](../cos-generic-metadata) being in the parent folder. If you change its location be sure to adjust the `source` attribute in `main.tf`. - -## Examples - -### Default configuration +## Example ```hcl -# Nginx with self-signed TLS config module "cos-nginx-tls" { source = "./fabric/modules/cloud-config-container/nginx-tls" } -# COS VM module "vm-nginx-tls" { source = "./fabric/modules/compute-vm" - project_id = local.project_id - zone = local.zone + project_id = "my-project" + zone = "europe-west8-b" name = "cos-nginx-tls" network_interfaces = [{ - network = local.vpc.self_link, - subnetwork = local.vpc.subnet_self_link, - nat = false, - addresses = null + network = "default" + subnetwork = "gce" }] - metadata = { user-data = module.cos-nginx-tls.cloud_config google-logging-enabled = true } - boot_disk = { image = "projects/cos-cloud/global/images/family/cos-stable" type = "pd-ssd" size = 10 } - - service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"] + tags = ["http-server", "https-server", "ssh"] } +# tftest modules=1 resources=1 ``` @@ -50,11 +39,9 @@ module "vm-nginx-tls" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [files](variables.tf#L17) | Map of extra files to create on the instance, path as key. Owner and permissions will use defaults if null. | map(object({…})) | | null | -| [nginx_image](variables.tf#L27) | Nginx container image to use. | string | | "nginx:1.23.1" | -| [runcmd_post](variables.tf#L33) | Extra commands to run after starting nginx. | list(string) | | [] | -| [runcmd_pre](variables.tf#L39) | Extra commands to run before starting nginx. | list(string) | | [] | -| [users](variables.tf#L45) | Additional list of usernames to be created. | list(object({…})) | | […] | +| [files](variables.tf#L17) | Map of extra files to create on the instance, path as key. Owner and permissions will use defaults if null. | map(object({…})) | | {} | +| [hello](variables.tf#L28) | Behave like the nginx hello image by returning plain text informative responses. | bool | | true | +| [image](variables.tf#L35) | Nginx container image to use. | string | | "nginx:1.23.1" | ## Outputs diff --git a/modules/cloud-config-container/nginx-tls/assets/cloud-config.yaml b/modules/cloud-config-container/nginx-tls/assets/cloud-config.yaml new file mode 100644 index 000000000..2b7ebe847 --- /dev/null +++ b/modules/cloud-config-container/nginx-tls/assets/cloud-config.yaml @@ -0,0 +1,63 @@ +#cloud-config + +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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. + +users: + - name: nginx + uid: 2000 + +write_files: + - path: /var/lib/docker/daemon.json + permissions: "0644" + owner: root + content: | + { + "live-restore": true, + "storage-driver": "overlay2", + "log-opts": { + "max-size": "1024m" + } + } + # nginx container service + - path: /etc/systemd/system/nginx.service + permissions: "0644" + owner: root + content: | + [Unit] + Description=Start nginx container + After=gcr-online.target docker.socket + Wants=gcr-online.target docker.socket docker-events-collector.service + [Service] + Environment="HOME=/home/nginx" + ExecStart=/usr/bin/docker run --rm --name=nginx \ + --network host --pid host \ + -v /etc/nginx/conf.d:/etc/nginx/conf.d \ + -v /etc/ssl:/etc/ssl \ + ${image} + ExecStop=/usr/bin/docker stop nginx +%{ for k, v in files ~} + - path: ${k} + owner: ${v.owner} + permissions: "${v.permissions}" + content: | + ${indent(6, v.content)} +%{ endfor ~} + +runcmd: + - iptables -I INPUT 1 -p tcp -m tcp --dport 80 -m state --state NEW,ESTABLISHED -j ACCEPT + - iptables -I INPUT 1 -p tcp -m tcp --dport 443 -m state --state NEW,ESTABLISHED -j ACCEPT + - /var/run/nginx/customize.sh + - systemctl daemon-reload + - systemctl start nginx diff --git a/modules/cloud-config-container/nginx-tls/files/customize.sh b/modules/cloud-config-container/nginx-tls/assets/customize.sh similarity index 72% rename from modules/cloud-config-container/nginx-tls/files/customize.sh rename to modules/cloud-config-container/nginx-tls/assets/customize.sh index afbf56db9..22b40064b 100644 --- a/modules/cloud-config-container/nginx-tls/files/customize.sh +++ b/modules/cloud-config-container/nginx-tls/assets/customize.sh @@ -13,8 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -FQDN=$(curl -s -H "Metadata-Flavor: Google" http://metadata/computeMetadata/v1/instance/hostname) +FQDN=$(\ + curl -s -H "Metadata-Flavor: Google" \ + http://metadata/computeMetadata/v1/instance/hostname) HOSTNAME=$(echo $FQDN | cut -d"." -f1) -openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj /CN=$HOSTNAME/ -addext "subjectAltName = DNS:$FQDN" -keyout /etc/ssl/self-signed.key -out /etc/ssl/self-signed.crt +openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 \ + -subj /CN=$HOSTNAME/ -addext "subjectAltName = DNS:$FQDN" \ + -keyout /etc/ssl/self-signed.key -out /etc/ssl/self-signed.crt chgrp nginx /etc/ssl/self-signed.key -out /etc/ssl/self-signed.crt sed -i "s/HOSTNAME/${HOSTNAME}/" /etc/nginx/conf.d/default.conf \ No newline at end of file diff --git a/modules/cloud-config-container/nginx-tls/assets/default.conf b/modules/cloud-config-container/nginx-tls/assets/default.conf new file mode 100644 index 000000000..2be98ff27 --- /dev/null +++ b/modules/cloud-config-container/nginx-tls/assets/default.conf @@ -0,0 +1,24 @@ +server { + listen 80; + listen 443 ssl; + server_name HOSTNAME; + ssl_certificate /etc/ssl/self-signed.crt; + ssl_certificate_key /etc/ssl/self-signed.key; + + location / { + {% if hello %} + default_type text/plain; + expires -1; + return 200 'Server address: $server_addr:$server_port\nServer name: $hostname\nDate: $time_local\nURI: $request_uri\nRequest ID: $request_id\n'; + {% else %} + root /usr/share/nginx/html; + index index.html index.htm; + {% endif %} + } + + error_page 500 502 503 504 /50x.html; + + location = /50x.html { + root /usr/share/nginx/html; + } +} \ No newline at end of file diff --git a/modules/cloud-config-container/nginx-tls/files/default.conf b/modules/cloud-config-container/nginx-tls/files/default.conf deleted file mode 100644 index b928902a0..000000000 --- a/modules/cloud-config-container/nginx-tls/files/default.conf +++ /dev/null @@ -1,20 +0,0 @@ -server { - listen 80; - listen 443 ssl; - server_name HOSTNAME; - ssl_certificate /etc/ssl/self-signed.crt; - ssl_certificate_key /etc/ssl/self-signed.key; - - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - } - - error_page 500 502 503 504 /50x.html; - - location = /50x.html { - root /usr/share/nginx/html; - } - -} \ No newline at end of file diff --git a/modules/cloud-config-container/nginx-tls/main.tf b/modules/cloud-config-container/nginx-tls/main.tf deleted file mode 100644 index 809e9a8aa..000000000 --- a/modules/cloud-config-container/nginx-tls/main.tf +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -locals { - default_files = { - "/var/run/nginx/customize.sh" = { - content = file("${path.module}/files/customize.sh") - owner = "root" - permissions = "0744" - } - "/etc/nginx/conf.d/default.conf" = { - content = file("${path.module}/files/default.conf") - owner = "root" - permissions = "0644" - } - } - files = var.files != null ? merge(local.default_files, var.files) : local.default_files -} - -module "cos-envoy-td" { - source = "../cos-generic-metadata" - - authenticate_gcr = true - users = concat([ - { - username = "nginx" - uid = 2000 - } - ], var.users) - run_as_first_user = false - - boot_commands = [ - "systemctl start node-problem-detector", - ] - - container_image = var.nginx_image - container_name = "nginx" - container_args = "" - - container_volumes = [ - { host = "/etc/nginx/conf.d", container = "/etc/nginx/conf.d" }, - { host = "/etc/ssl", container = "/etc/ssl" }, - ] - - docker_args = "--network host --pid host" - - files = local.files - - run_commands = concat(var.runcmd_pre, [ - "iptables -I INPUT 1 -p tcp -m tcp --dport 80 -m state --state NEW,ESTABLISHED -j ACCEPT", - "iptables -I INPUT 1 -p tcp -m tcp --dport 443 -m state --state NEW,ESTABLISHED -j ACCEPT", - "/var/run/nginx/customize.sh", - "systemctl daemon-reload", - "systemctl start nginx", - ], var.runcmd_post) - -} diff --git a/modules/cloud-config-container/nginx-tls/outputs.tf b/modules/cloud-config-container/nginx-tls/outputs.tf index 4ce8d2473..2acd83f64 100644 --- a/modules/cloud-config-container/nginx-tls/outputs.tf +++ b/modules/cloud-config-container/nginx-tls/outputs.tf @@ -16,5 +16,23 @@ output "cloud_config" { description = "Rendered cloud-config file to be passed as user-data instance metadata." - value = module.cos-envoy-td.cloud_config + value = templatefile("${path.module}/assets/cloud-config.yaml", { + files = merge( + { + "/var/run/nginx/customize.sh" = { + content = file("${path.module}/assets/customize.sh") + owner = "root" + permissions = "0744" + } + "/etc/nginx/conf.d/default.conf" = { + content = templatefile( + "${path.module}/assets/default.conf", { hello = var.hello } + ) + owner = "root" + permissions = "0644" + } + }, var.files + ) + image = var.image + }) } diff --git a/modules/cloud-config-container/nginx-tls/variables.tf b/modules/cloud-config-container/nginx-tls/variables.tf index 9ca826266..f3cab5826 100644 --- a/modules/cloud-config-container/nginx-tls/variables.tf +++ b/modules/cloud-config-container/nginx-tls/variables.tf @@ -18,38 +18,22 @@ variable "files" { description = "Map of extra files to create on the instance, path as key. Owner and permissions will use defaults if null." type = map(object({ content = string - owner = string - permissions = string + owner = optional(string, "root") + permissions = optional(string, "0644") })) - default = null + default = {} + nullable = false } -variable "nginx_image" { +variable "hello" { + description = "Behave like the nginx hello image by returning plain text informative responses." + type = bool + default = true + nullable = false +} + +variable "image" { description = "Nginx container image to use." type = string default = "nginx:1.23.1" } - -variable "runcmd_post" { - description = "Extra commands to run after starting nginx." - type = list(string) - default = [] -} - -variable "runcmd_pre" { - description = "Extra commands to run before starting nginx." - type = list(string) - default = [] -} - -variable "users" { - description = "Additional list of usernames to be created." - type = list(object({ - username = string, - uid = number, - })) - default = [ - ] -} - - diff --git a/modules/cloud-config-container/nginx-tls/versions.tf b/modules/cloud-config-container/nginx-tls/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/cloud-config-container/nginx-tls/versions.tf +++ b/modules/cloud-config-container/nginx-tls/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/cloud-config-container/nginx/README.md b/modules/cloud-config-container/nginx/README.md index 12ca3d5d0..c5fbc09bd 100644 --- a/modules/cloud-config-container/nginx/README.md +++ b/modules/cloud-config-container/nginx/README.md @@ -24,35 +24,30 @@ This example will create a `cloud-config` that uses the module's defaults, creat ```hcl module "cos-nginx" { - source = "./fabric/modules/cloud-config-container/nginx" + source = "./fabric/modules/cloud-config-container/nginx" } -# use it as metadata in a compute instance or template -module "vm-nginx" { - source = "./fabric/modules/compute-vm" +module "vm-nginx-tls" { + source = "./fabric/modules/compute-vm" + project_id = "my-project" + zone = "europe-west8-b" + name = "cos-nginx" + network_interfaces = [{ + network = "default" + subnetwork = "gce" + }] metadata = { user-data = module.cos-nginx.cloud_config google-logging-enabled = true } -} -``` - -### Nginx instance - -This example shows how to create the single instance optionally managed by the module, providing all required attributes in the `test_instance` variable. The instance is purposefully kept simple and should only be used in development, or when designing infrastructures. - -```hcl -module "cos-nginx" { - source = "./fabric/modules/cloud-config-container/nginx" - test_instance = { - project_id = "my-project" - zone = "europe-west1-b" - name = "cos-nginx" - type = "f1-micro" - network = "default" - subnetwork = "https://www.googleapis.com/compute/v1/projects/my-project/regions/europe-west1/subnetworks/my-subnet" + boot_disk = { + image = "projects/cos-cloud/global/images/family/cos-stable" + type = "pd-ssd" + size = 10 } + tags = ["http-server", "ssh"] } +# tftest modules=1 resources=1 ``` @@ -68,8 +63,6 @@ module "cos-nginx" { | [nginx_config](variables.tf#L57) | Nginx configuration path, if null container default will be used. | string | | null | | [runcmd_post](variables.tf#L63) | Extra commands to run after starting nginx. | list(string) | | [] | | [runcmd_pre](variables.tf#L69) | Extra commands to run before starting nginx. | list(string) | | [] | -| [test_instance](variables-instance.tf#L17) | Test/development instance attributes, leave null to skip creation. | object({…}) | | null | -| [test_instance_defaults](variables-instance.tf#L30) | Test/development instance defaults used for optional configuration. If image is null, COS stable will be used. | object({…}) | | {…} | | [users](variables.tf#L75) | List of additional usernames to be created. | list(object({…})) | | […] | ## Outputs @@ -77,6 +70,5 @@ module "cos-nginx" { | name | description | sensitive | |---|---|:---:| | [cloud_config](outputs.tf#L17) | Rendered cloud-config file to be passed as user-data instance metadata. | | -| [test_instance](outputs-instance.tf#L17) | Optional test instance name and address. | | diff --git a/modules/cloud-config-container/nginx/instance.tf b/modules/cloud-config-container/nginx/instance.tf deleted file mode 120000 index bdef596b6..000000000 --- a/modules/cloud-config-container/nginx/instance.tf +++ /dev/null @@ -1 +0,0 @@ -../instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/nginx/outputs-instance.tf b/modules/cloud-config-container/nginx/outputs-instance.tf deleted file mode 120000 index ea9e24045..000000000 --- a/modules/cloud-config-container/nginx/outputs-instance.tf +++ /dev/null @@ -1 +0,0 @@ -../outputs-instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/nginx/variables-instance.tf b/modules/cloud-config-container/nginx/variables-instance.tf deleted file mode 120000 index 94af61e4d..000000000 --- a/modules/cloud-config-container/nginx/variables-instance.tf +++ /dev/null @@ -1 +0,0 @@ -../variables-instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/nginx/versions.tf b/modules/cloud-config-container/nginx/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/cloud-config-container/nginx/versions.tf +++ b/modules/cloud-config-container/nginx/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/cloud-config-container/onprem/instance.tf b/modules/cloud-config-container/onprem/instance.tf deleted file mode 120000 index bdef596b6..000000000 --- a/modules/cloud-config-container/onprem/instance.tf +++ /dev/null @@ -1 +0,0 @@ -../instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/onprem/outputs-instance.tf b/modules/cloud-config-container/onprem/outputs-instance.tf deleted file mode 120000 index ea9e24045..000000000 --- a/modules/cloud-config-container/onprem/outputs-instance.tf +++ /dev/null @@ -1 +0,0 @@ -../outputs-instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/onprem/variables-instance.tf b/modules/cloud-config-container/onprem/variables-instance.tf deleted file mode 120000 index 94af61e4d..000000000 --- a/modules/cloud-config-container/onprem/variables-instance.tf +++ /dev/null @@ -1 +0,0 @@ -../variables-instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/outputs-instance.tf b/modules/cloud-config-container/outputs-instance.tf deleted file mode 100644 index 8c657eb05..000000000 --- a/modules/cloud-config-container/outputs-instance.tf +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -output "test_instance" { - description = "Optional test instance name and address." - value = (var.test_instance == null ? {} : { - address = google_compute_instance.default[0].network_interface.0.network_ip - name = google_compute_instance.default[0].name - nat_address = try( - google_compute_instance.default[0].network_interface.0.access_config.0.nat_ip, - null - ) - service_account = google_service_account.default[0].email - }) -} diff --git a/modules/cloud-config-container/simple-nva/README.md b/modules/cloud-config-container/simple-nva/README.md index 0e495df5b..f70842e8c 100644 --- a/modules/cloud-config-container/simple-nva/README.md +++ b/modules/cloud-config-container/simple-nva/README.md @@ -9,7 +9,6 @@ This NVA can be used to interconnect up to 8 VPCs. ### Simple example ```hcl -# Interfaces configuration locals { network_interfaces = [ { @@ -28,41 +27,40 @@ locals { routes = ["10.0.0.0/9"] subnetwork = "prod_vpc_nva_subnet_self_link" } + ] } -# NVA config -module "nva-cloud-config" { - source = "../../../cloud-foundation-fabric/modules/cloud-config-container/simple-nva" +module "cos-nva" { + source = "./fabric/modules/cloud-config-container/simple-nva" enable_health_checks = true network_interfaces = local.network_interfaces - files = { - "/var/lib/cloud/scripts/per-boot/firewall-rules.sh" = { - content = file("./your_path/to/firewall-rules.sh") - owner = "root" - permissions = 0700 - } - } + # files = { + # "/var/lib/cloud/scripts/per-boot/firewall-rules.sh" = { + # content = file("./your_path/to/firewall-rules.sh") + # owner = "root" + # permissions = 0700 + # } + # } } -# COS VM -module "nva" { - source = "../../modules/compute-vm" - project_id = "myproject" - instance_type = "e2-standard-2" - name = "nva" - can_ip_forward = true - zone = "europe-west8-a" - tags = ["nva"] +module "vm" { + source = "./fabric/modules/compute-vm" + project_id = "my-project" + zone = "europe-west8-b" + name = "cos-nva" network_interfaces = local.network_interfaces + metadata = { + user-data = module.cos-nva.cloud_config + google-logging-enabled = true + } boot_disk = { image = "projects/cos-cloud/global/images/family/cos-stable" + type = "pd-ssd" size = 10 - type = "pd-balanced" - } - metadata = { - user-data = module.nva-cloud-config.cloud_config } + tags = ["nva", "ssh"] } +# tftest modules=1 resources=1 ``` @@ -74,14 +72,11 @@ module "nva" { | [cloud_config](variables.tf#L17) | Cloud config template path. If null default will be used. | string | | null | | [enable_health_checks](variables.tf#L23) | Configures routing to enable responses to health check probes. | bool | | false | | [files](variables.tf#L29) | Map of extra files to create on the instance, path as key. Owner and permissions will use defaults if null. | map(object({…})) | | {} | -| [test_instance](variables-instance.tf#L17) | Test/development instance attributes, leave null to skip creation. | object({…}) | | null | -| [test_instance_defaults](variables-instance.tf#L30) | Test/development instance defaults used for optional configuration. If image is null, COS stable will be used. | object({…}) | | {…} | ## Outputs | name | description | sensitive | |---|---|:---:| | [cloud_config](outputs.tf#L17) | Rendered cloud-config file to be passed as user-data instance metadata. | | -| [test_instance](outputs-instance.tf#L17) | Optional test instance name and address. | | diff --git a/modules/cloud-config-container/simple-nva/files/policy_based_routing.sh b/modules/cloud-config-container/simple-nva/files/policy_based_routing.sh index 2e1eb1523..951396d35 100644 --- a/modules/cloud-config-container/simple-nva/files/policy_based_routing.sh +++ b/modules/cloud-config-container/simple-nva/files/policy_based_routing.sh @@ -17,16 +17,18 @@ IF_NAME=$1 IP_LB=$(ip r show table local | grep "$IF_NAME proto 66" | cut -f 2 -d " ") -# If there's a load balancer for this IF... -if [ ! -z $IP_LB ] -then - IF_NUMBER=$(echo $IF_NAME | sed -e s/eth//) - IF_GW=$(curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/$IF_NUMBER/gateway -H "Metadata-Flavor: Google") - IF_IP=$(curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/$IF_NUMBER/ip -H "Metadata-Flavor: Google") - IF_NETMASK=$(curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/$IF_NUMBER/subnetmask -H "Metadata-Flavor: Google") - IF_IP_PREFIX=$(/var/run/nva/ipprefix_by_netmask.sh $IF_NETMASK) - grep -qxF "$((200 + $IF_NUMBER)) hc-$IF_NAME" /etc/iproute2/rt_tables || echo "$((200 + $IF_NUMBER)) hc-$IF_NAME" >>/etc/iproute2/rt_tables - ip route add $IF_GW src $IF_IP dev $IF_NAME table hc-$IF_NAME - ip route add default via $IF_GW dev $IF_NAME table hc-$IF_NAME - ip rule add from $IP_LB/32 table hc-$IF_NAME -fi +# Sleep while there's no load balancer IP route for this IF +while [ -z $IP_LB ] ; do + sleep 2 + IP_LB=$(ip r show table local | grep "$IF_NAME proto 66" | cut -f 2 -d " ") +done + +IF_NUMBER=$(echo $IF_NAME | sed -e s/eth//) +IF_GW=$(curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/$IF_NUMBER/gateway -H "Metadata-Flavor: Google") +IF_IP=$(curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/$IF_NUMBER/ip -H "Metadata-Flavor: Google") +IF_NETMASK=$(curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/$IF_NUMBER/subnetmask -H "Metadata-Flavor: Google") +IF_IP_PREFIX=$(/var/run/nva/ipprefix_by_netmask.sh $IF_NETMASK) +grep -qxF "$((200 + $IF_NUMBER)) hc-$IF_NAME" /etc/iproute2/rt_tables || echo "$((200 + $IF_NUMBER)) hc-$IF_NAME" >>/etc/iproute2/rt_tables +ip route add $IF_GW src $IF_IP dev $IF_NAME table hc-$IF_NAME +ip route add default via $IF_GW dev $IF_NAME table hc-$IF_NAME +ip rule add from $IP_LB/32 table hc-$IF_NAME diff --git a/modules/cloud-config-container/simple-nva/instance.tf b/modules/cloud-config-container/simple-nva/instance.tf deleted file mode 120000 index bdef596b6..000000000 --- a/modules/cloud-config-container/simple-nva/instance.tf +++ /dev/null @@ -1 +0,0 @@ -../instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/simple-nva/outputs-instance.tf b/modules/cloud-config-container/simple-nva/outputs-instance.tf deleted file mode 120000 index ea9e24045..000000000 --- a/modules/cloud-config-container/simple-nva/outputs-instance.tf +++ /dev/null @@ -1 +0,0 @@ -../outputs-instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/simple-nva/variables-instance.tf b/modules/cloud-config-container/simple-nva/variables-instance.tf deleted file mode 120000 index 94af61e4d..000000000 --- a/modules/cloud-config-container/simple-nva/variables-instance.tf +++ /dev/null @@ -1 +0,0 @@ -../variables-instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/simple-nva/versions.tf b/modules/cloud-config-container/simple-nva/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/cloud-config-container/simple-nva/versions.tf +++ b/modules/cloud-config-container/simple-nva/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/cloud-config-container/squid/README.md b/modules/cloud-config-container/squid/README.md index 1c866b25a..4ee29ed91 100644 --- a/modules/cloud-config-container/squid/README.md +++ b/modules/cloud-config-container/squid/README.md @@ -24,39 +24,32 @@ This example will create a `cloud-config` that allows any client in the 10.0.0.0 ```hcl module "cos-squid" { - source = "./fabric/modules/cloud-config-container/squid" - whitelist = [".github.com"] - clients = ["10.0.0.0/8"] + source = "./fabric/modules/cloud-config-container/squid" + allow = [".github.com"] + clients = ["10.0.0.0/8"] } -# use it as metadata in a compute instance or template -module "vm-squid" { - source = "./fabric/modules/compute-vm" +module "vm" { + source = "./fabric/modules/compute-vm" + project_id = "my-project" + zone = "europe-west8-b" + name = "cos-squid" + network_interfaces = [{ + network = "default" + subnetwork = "gce" + }] metadata = { user-data = module.cos-squid.cloud_config google-logging-enabled = true } -} -``` - -### Test Squid instance - -This example shows how to create the single instance optionally managed by the module, providing all required attributes in the `test_instance` variable. The instance is purposefully kept simple and should only be used in development, or when designing infrastructures. - -```hcl -module "cos-squid" { - source = "./fabric/modules/cloud-config-container/squid" - whitelist = ["github.com"] - clients = ["10.0.0.0/8"] - test_instance = { - project_id = "my-project" - zone = "europe-west1-b" - name = "cos-squid" - type = "f1-micro" - network = "default" - subnetwork = "https://www.googleapis.com/compute/v1/projects/my-project/regions/europe-west1/subnetworks/my-subnet" + boot_disk = { + image = "projects/cos-cloud/global/images/family/cos-stable" + type = "pd-ssd" + size = 10 } + tags = ["http-server", "ssh"] } +# tftest modules=1 resources=1 ``` @@ -73,14 +66,11 @@ module "cos-squid" { | [file_defaults](variables.tf#L58) | Default owner and permissions for files. | object({…}) | | {…} | | [files](variables.tf#L70) | Map of extra files to create on the instance, path as key. Owner and permissions will use defaults if null. | map(object({…})) | | {} | | [squid_config](variables.tf#L80) | Squid configuration path, if null default will be used. | string | | null | -| [test_instance](variables-instance.tf#L17) | Test/development instance attributes, leave null to skip creation. | object({…}) | | null | -| [test_instance_defaults](variables-instance.tf#L30) | Test/development instance defaults used for optional configuration. If image is null, COS stable will be used. | object({…}) | | {…} | ## Outputs | name | description | sensitive | |---|---|:---:| | [cloud_config](outputs.tf#L17) | Rendered cloud-config file to be passed as user-data instance metadata. | | -| [test_instance](outputs-instance.tf#L17) | Optional test instance name and address. | | diff --git a/modules/cloud-config-container/squid/instance.tf b/modules/cloud-config-container/squid/instance.tf deleted file mode 120000 index bdef596b6..000000000 --- a/modules/cloud-config-container/squid/instance.tf +++ /dev/null @@ -1 +0,0 @@ -../instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/squid/outputs-instance.tf b/modules/cloud-config-container/squid/outputs-instance.tf deleted file mode 120000 index ea9e24045..000000000 --- a/modules/cloud-config-container/squid/outputs-instance.tf +++ /dev/null @@ -1 +0,0 @@ -../outputs-instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/squid/variables-instance.tf b/modules/cloud-config-container/squid/variables-instance.tf deleted file mode 120000 index 94af61e4d..000000000 --- a/modules/cloud-config-container/squid/variables-instance.tf +++ /dev/null @@ -1 +0,0 @@ -../variables-instance.tf \ No newline at end of file diff --git a/modules/cloud-config-container/squid/versions.tf b/modules/cloud-config-container/squid/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/cloud-config-container/squid/versions.tf +++ b/modules/cloud-config-container/squid/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/cloud-config-container/variables-instance.tf b/modules/cloud-config-container/variables-instance.tf deleted file mode 100644 index 3697133ea..000000000 --- a/modules/cloud-config-container/variables-instance.tf +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -variable "test_instance" { - description = "Test/development instance attributes, leave null to skip creation." - type = object({ - project_id = string - zone = string - name = string - type = string - network = string - subnetwork = string - }) - default = null -} - -variable "test_instance_defaults" { - description = "Test/development instance defaults used for optional configuration. If image is null, COS stable will be used." - type = object({ - disks = map(object({ - read_only = bool - size = number - })) - image = string - metadata = map(string) - nat = bool - service_account_roles = list(string) - tags = list(string) - }) - default = { - disks = {} - image = null - metadata = {} - nat = false - service_account_roles = [ - "roles/logging.logWriter", - "roles/monitoring.metricWriter" - ] - tags = ["ssh"] - } -} diff --git a/modules/cloud-function/README.md b/modules/cloud-function/README.md index 75ef37198..ebf300fcc 100644 --- a/modules/cloud-function/README.md +++ b/modules/cloud-function/README.md @@ -16,10 +16,10 @@ This deploys a Cloud Function with an HTTP endpoint, using a pre-existing GCS bu ```hcl module "cf-http" { - source = "./fabric/modules/cloud-function" - project_id = "my-project" - name = "test-cf-http" - bucket_name = "test-cf-bundles" + source = "./fabric/modules/cloud-function" + project_id = "my-project" + name = "test-cf-http" + bucket_name = "test-cf-bundles" bundle_config = { source_dir = "fabric/assets/" output_path = "bundle.zip" @@ -31,11 +31,11 @@ module "cf-http" { Analogous example using 2nd generation Cloud Functions ```hcl module "cf-http" { - source = "./fabric/modules/cloud-function" - v2 = true - project_id = "my-project" - name = "test-cf-http" - bucket_name = "test-cf-bundles" + source = "./fabric/modules/cloud-function" + v2 = true + project_id = "my-project" + name = "test-cf-http" + bucket_name = "test-cf-bundles" bundle_config = { source_dir = "fabric/assets/" output_path = "bundle.zip" @@ -111,15 +111,15 @@ To allow anonymous access to the function, grant the `roles/cloudfunctions.invok ```hcl module "cf-http" { - source = "./fabric/modules/cloud-function" - project_id = "my-project" - name = "test-cf-http" - bucket_name = "test-cf-bundles" + source = "./fabric/modules/cloud-function" + project_id = "my-project" + name = "test-cf-http" + bucket_name = "test-cf-bundles" bundle_config = { source_dir = "fabric/assets/" output_path = "bundle.zip" } - iam = { + iam = { "roles/cloudfunctions.invoker" = ["allUsers"] } } @@ -132,15 +132,15 @@ You can have the module auto-create the GCS bucket used for deployment via the ` ```hcl module "cf-http" { - source = "./fabric/modules/cloud-function" - project_id = "my-project" - name = "test-cf-http" - bucket_name = "test-cf-bundles" + source = "./fabric/modules/cloud-function" + project_id = "my-project" + name = "test-cf-http" + bucket_name = "test-cf-bundles" bucket_config = { lifecycle_delete_age_days = 1 } bundle_config = { - source_dir = "fabric/assets/" + source_dir = "fabric/assets/" } } # tftest modules=1 resources=3 @@ -152,10 +152,10 @@ To use a custom service account managed by the module, set `service_account_crea ```hcl module "cf-http" { - source = "./fabric/modules/cloud-function" - project_id = "my-project" - name = "test-cf-http" - bucket_name = "test-cf-bundles" + source = "./fabric/modules/cloud-function" + project_id = "my-project" + name = "test-cf-http" + bucket_name = "test-cf-bundles" bundle_config = { source_dir = "fabric/assets/" output_path = "bundle.zip" @@ -169,10 +169,10 @@ To use an externally managed service account, pass its email in `service_account ```hcl module "cf-http" { - source = "./fabric/modules/cloud-function" - project_id = "my-project" - name = "test-cf-http" - bucket_name = "test-cf-bundles" + source = "./fabric/modules/cloud-function" + project_id = "my-project" + name = "test-cf-http" + bucket_name = "test-cf-bundles" bundle_config = { source_dir = "fabric/assets/" output_path = "bundle.zip" @@ -188,10 +188,10 @@ In order to help prevent `archive_zip.output_md5` from changing cross platform ( ```hcl module "cf-http" { - source = "./fabric/modules/cloud-function" - project_id = "my-project" - name = "test-cf-http" - bucket_name = "test-cf-bundles" + source = "./fabric/modules/cloud-function" + project_id = "my-project" + name = "test-cf-http" + bucket_name = "test-cf-bundles" bundle_config = { source_dir = "fabric/assets" output_path = "bundle.zip" @@ -207,10 +207,10 @@ This deploys a Cloud Function with an HTTP endpoint, using a pre-existing GCS bu ```hcl module "cf-http" { - source = "./fabric/modules/cloud-function" - project_id = "my-project" - name = "test-cf-http" - bucket_name = "test-cf-bundles" + source = "./fabric/modules/cloud-function" + project_id = "my-project" + name = "test-cf-http" + bucket_name = "test-cf-bundles" build_worker_pool = "projects/my-project/locations/europe-west1/workerPools/my_build_worker_pool" bundle_config = { source_dir = "fabric/assets" diff --git a/modules/cloud-function/versions.tf b/modules/cloud-function/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/cloud-function/versions.tf +++ b/modules/cloud-function/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/cloud-identity-group/README.md b/modules/cloud-identity-group/README.md index 91c625fa2..c94aa6d8b 100644 --- a/modules/cloud-identity-group/README.md +++ b/modules/cloud-identity-group/README.md @@ -46,7 +46,7 @@ module "group" { ] managers = [ "user3@example.com" - ] + ] } # tftest modules=1 resources=5 ``` diff --git a/modules/cloud-identity-group/versions.tf b/modules/cloud-identity-group/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/cloud-identity-group/versions.tf +++ b/modules/cloud-identity-group/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/cloud-run/README.md b/modules/cloud-run/README.md index e8e2fc1ff..5fde6ed47 100644 --- a/modules/cloud-run/README.md +++ b/modules/cloud-run/README.md @@ -14,18 +14,18 @@ module "cloud_run" { project_id = "my-project" name = "hello" containers = [{ - image = "us-docker.pkg.dev/cloudrun/container/hello" + image = "us-docker.pkg.dev/cloudrun/container/hello" options = { command = null args = null - env = { - "VAR1": "VALUE1", - "VAR2": "VALUE2", + env = { + "VAR1" : "VALUE1", + "VAR2" : "VALUE2", } env_from = null } - ports = null - resources = null + ports = null + resources = null volume_mounts = null }] } @@ -42,18 +42,18 @@ module "cloud_run" { containers = [{ image = "us-docker.pkg.dev/cloudrun/container/hello" options = { - command = null - args = null - env = null - env_from = { - "CREDENTIALS": { + command = null + args = null + env = null + env_from = { + "CREDENTIALS" : { name = "credentials" - key = "1" + key = "1" } } } - ports = null - resources = null + ports = null + resources = null volume_mounts = null }] } @@ -64,26 +64,26 @@ module "cloud_run" { ```hcl module "cloud_run" { - source = "./fabric/modules/cloud-run" - project_id = var.project_id - name = "hello" - region = var.region + source = "./fabric/modules/cloud-run" + project_id = var.project_id + name = "hello" + region = var.region revision_name = "green" containers = [{ - image = "us-docker.pkg.dev/cloudrun/container/hello" - options = null - ports = null - resources = null + image = "us-docker.pkg.dev/cloudrun/container/hello" + options = null + ports = null + resources = null volume_mounts = { - "credentials": "/credentials" + "credentials" : "/credentials" } }] volumes = [ { - name = "credentials" + name = "credentials" secret_name = "credentials" items = [{ - key = "1" + key = "1" path = "v1.txt" }] } @@ -98,9 +98,9 @@ This deploys a Cloud Run service with traffic split between two revisions. ```hcl module "cloud_run" { - source = "./fabric/modules/cloud-run" - project_id = "my-project" - name = "hello" + source = "./fabric/modules/cloud-run" + project_id = "my-project" + name = "hello" revision_name = "green" containers = [{ image = "us-docker.pkg.dev/cloudrun/container/hello" @@ -110,7 +110,7 @@ module "cloud_run" { volume_mounts = null }] traffic = { - "blue" = 25 + "blue" = 25 "green" = 75 } } @@ -159,8 +159,8 @@ module "cloud_run" { }] audit_log_triggers = [ { - service_name = "cloudresourcemanager.googleapis.com" - method_name = "SetIamPolicy" + service_name = "cloudresourcemanager.googleapis.com" + method_name = "SetIamPolicy" } ] } diff --git a/modules/cloud-run/versions.tf b/modules/cloud-run/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/cloud-run/versions.tf +++ b/modules/cloud-run/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/cloudsql-instance/README.md b/modules/cloudsql-instance/README.md index a902f5455..9e05b0b38 100644 --- a/modules/cloudsql-instance/README.md +++ b/modules/cloudsql-instance/README.md @@ -88,7 +88,7 @@ module "db" { # generatea password for user1 user1 = null # assign a password to user2 - user2 = "mypassword" + user2 = "mypassword" } } # tftest modules=1 resources=6 @@ -146,27 +146,29 @@ module "db" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [database_version](variables.tf#L49) | Database type and version to create. | string | ✓ | | -| [name](variables.tf#L102) | Name of primary instance. | string | ✓ | | -| [network](variables.tf#L107) | VPC self link where the instances will be deployed. Private Service Networking must be enabled and configured in this VPC. | string | ✓ | | -| [project_id](variables.tf#L122) | The ID of the project where this instances will be created. | string | ✓ | | -| [region](variables.tf#L127) | Region of the primary instance. | string | ✓ | | -| [tier](variables.tf#L147) | The machine type to use for the instances. | string | ✓ | | -| [authorized_networks](variables.tf#L17) | Map of NAME=>CIDR_RANGE to allow to connect to the database(s). | map(string) | | null | -| [availability_type](variables.tf#L23) | Availability type for the primary replica. Either `ZONAL` or `REGIONAL`. | string | | "ZONAL" | -| [backup_configuration](variables.tf#L29) | Backup settings for primary instance. Will be automatically enabled if using MySQL with one or more replicas. | object({…}) | | {…} | -| [databases](variables.tf#L54) | Databases to create once the primary instance is created. | list(string) | | null | -| [deletion_protection](variables.tf#L60) | Allow terraform to delete instances. | bool | | false | -| [disk_size](variables.tf#L66) | Disk size in GB. Set to null to enable autoresize. | number | | null | -| [disk_type](variables.tf#L72) | The type of data disk: `PD_SSD` or `PD_HDD`. | string | | "PD_SSD" | -| [encryption_key_name](variables.tf#L78) | The full path to the encryption key used for the CMEK disk encryption of the primary instance. | string | | null | -| [flags](variables.tf#L84) | Map FLAG_NAME=>VALUE for database-specific tuning. | map(string) | | null | -| [ipv4_enabled](variables.tf#L90) | Add a public IP address to database instance. | bool | | false | -| [labels](variables.tf#L96) | Labels to be attached to all instances. | map(string) | | null | -| [prefix](variables.tf#L112) | Optional prefix used to generate instance names. | string | | null | -| [replicas](variables.tf#L132) | Map of NAME=> {REGION, KMS_KEY} for additional read replicas. Set to null to disable replica creation. | map(object({…})) | | {} | -| [root_password](variables.tf#L141) | Root password of the Cloud SQL instance. Required for MS SQL Server. | string | | null | -| [users](variables.tf#L152) | Map of users to create in the primary instance (and replicated to other replicas) in the format USER=>PASSWORD. For MySQL, anything afterr the first `@` (if persent) will be used as the user's host. Set PASSWORD to null if you want to get an autogenerated password. | map(string) | | null | +| [database_version](variables.tf#L61) | Database type and version to create. | string | ✓ | | +| [name](variables.tf#L114) | Name of primary instance. | string | ✓ | | +| [network](variables.tf#L119) | VPC self link where the instances will be deployed. Private Service Networking must be enabled and configured in this VPC. | string | ✓ | | +| [project_id](variables.tf#L140) | The ID of the project where this instances will be created. | string | ✓ | | +| [region](variables.tf#L145) | Region of the primary instance. | string | ✓ | | +| [tier](variables.tf#L165) | The machine type to use for the instances. | string | ✓ | | +| [allocated_ip_ranges](variables.tf#L17) | (Optional)The name of the allocated ip range for the private ip CloudSQL instance. For example: \"google-managed-services-default\". If set, the instance ip will be created in the allocated range. The range name must comply with RFC 1035. Specifically, the name must be 1-63 characters long and match the regular expression a-z?. | object({…}) | | {} | +| [authorized_networks](variables.tf#L26) | Map of NAME=>CIDR_RANGE to allow to connect to the database(s). | map(string) | | null | +| [availability_type](variables.tf#L32) | Availability type for the primary replica. Either `ZONAL` or `REGIONAL`. | string | | "ZONAL" | +| [backup_configuration](variables.tf#L38) | Backup settings for primary instance. Will be automatically enabled if using MySQL with one or more replicas. | object({…}) | | {…} | +| [databases](variables.tf#L66) | Databases to create once the primary instance is created. | list(string) | | null | +| [deletion_protection](variables.tf#L72) | Allow terraform to delete instances. | bool | | false | +| [disk_size](variables.tf#L78) | Disk size in GB. Set to null to enable autoresize. | number | | null | +| [disk_type](variables.tf#L84) | The type of data disk: `PD_SSD` or `PD_HDD`. | string | | "PD_SSD" | +| [encryption_key_name](variables.tf#L90) | The full path to the encryption key used for the CMEK disk encryption of the primary instance. | string | | null | +| [flags](variables.tf#L96) | Map FLAG_NAME=>VALUE for database-specific tuning. | map(string) | | null | +| [ipv4_enabled](variables.tf#L102) | Add a public IP address to database instance. | bool | | false | +| [labels](variables.tf#L108) | Labels to be attached to all instances. | map(string) | | null | +| [postgres_client_certificates](variables.tf#L124) | Map of cert keys connect to the application(s) using public IP. | list(string) | | null | +| [prefix](variables.tf#L130) | Optional prefix used to generate instance names. | string | | null | +| [replicas](variables.tf#L150) | Map of NAME=> {REGION, KMS_KEY} for additional read replicas. Set to null to disable replica creation. | map(object({…})) | | {} | +| [root_password](variables.tf#L159) | Root password of the Cloud SQL instance. Required for MS SQL Server. | string | | null | +| [users](variables.tf#L170) | Map of users to create in the primary instance (and replicated to other replicas) in the format USER=>PASSWORD. For MySQL, anything afterr the first `@` (if persent) will be used as the user's host. Set PASSWORD to null if you want to get an autogenerated password. | map(string) | | null | ## Outputs @@ -181,8 +183,9 @@ module "db" { | [ips](outputs.tf#L61) | IP addresses of all instances. | | | [name](outputs.tf#L69) | Name of the primary instance. | | | [names](outputs.tf#L74) | Names of all instances. | | -| [self_link](outputs.tf#L82) | Self link of the primary instance. | | -| [self_links](outputs.tf#L87) | Self links of all instances. | | -| [user_passwords](outputs.tf#L95) | Map of containing the password of all users created through terraform. | ✓ | +| [postgres_client_certificates](outputs.tf#L82) | The CA Certificate used to connect to the SQL Instance via SSL. | ✓ | +| [self_link](outputs.tf#L88) | Self link of the primary instance. | | +| [self_links](outputs.tf#L93) | Self links of all instances. | | +| [user_passwords](outputs.tf#L101) | Map of containing the password of all users created through terraform. | ✓ | diff --git a/modules/cloudsql-instance/main.tf b/modules/cloudsql-instance/main.tf index f15b386bb..2419dc0d6 100644 --- a/modules/cloudsql-instance/main.tf +++ b/modules/cloudsql-instance/main.tf @@ -61,8 +61,9 @@ resource "google_sql_database_instance" "primary" { user_labels = var.labels ip_configuration { - ipv4_enabled = var.ipv4_enabled - private_network = var.network + ipv4_enabled = var.ipv4_enabled + private_network = var.network + allocated_ip_range = var.allocated_ip_ranges.primary dynamic "authorized_networks" { for_each = var.authorized_networks != null ? var.authorized_networks : {} iterator = network @@ -87,6 +88,7 @@ resource "google_sql_database_instance" "primary" { ) start_time = var.backup_configuration.start_time location = var.backup_configuration.location + point_in_time_recovery_enabled = var.backup_configuration.point_in_time_recovery_enabled transaction_log_retention_days = var.backup_configuration.log_retention_days backup_retention_settings { retained_backups = var.backup_configuration.retention_count @@ -126,8 +128,9 @@ resource "google_sql_database_instance" "replicas" { user_labels = var.labels ip_configuration { - ipv4_enabled = var.ipv4_enabled - private_network = var.network + ipv4_enabled = var.ipv4_enabled + private_network = var.network + allocated_ip_range = var.allocated_ip_ranges.replica dynamic "authorized_networks" { for_each = var.authorized_networks != null ? var.authorized_networks : {} iterator = network @@ -175,3 +178,10 @@ resource "google_sql_user" "users" { host = each.value.host password = each.value.password } + +resource "google_sql_ssl_cert" "postgres_client_certificates" { + for_each = var.postgres_client_certificates != null ? toset(var.postgres_client_certificates) : toset([]) + provider = google-beta + instance = google_sql_database_instance.primary.name + common_name = each.key +} diff --git a/modules/cloudsql-instance/outputs.tf b/modules/cloudsql-instance/outputs.tf index 38bfc951b..8c814c06a 100644 --- a/modules/cloudsql-instance/outputs.tf +++ b/modules/cloudsql-instance/outputs.tf @@ -79,6 +79,12 @@ output "names" { } } +output "postgres_client_certificates" { + description = "The CA Certificate used to connect to the SQL Instance via SSL." + value = google_sql_ssl_cert.postgres_client_certificates + sensitive = true +} + output "self_link" { description = "Self link of the primary instance." value = google_sql_database_instance.primary.self_link diff --git a/modules/cloudsql-instance/variables.tf b/modules/cloudsql-instance/variables.tf index 04bff546b..c7c4ef8d0 100644 --- a/modules/cloudsql-instance/variables.tf +++ b/modules/cloudsql-instance/variables.tf @@ -14,6 +14,15 @@ * limitations under the License. */ +variable "allocated_ip_ranges" { + description = "(Optional)The name of the allocated ip range for the private ip CloudSQL instance. For example: \"google-managed-services-default\". If set, the instance ip will be created in the allocated range. The range name must comply with RFC 1035. Specifically, the name must be 1-63 characters long and match the regular expression a-z?." + type = object({ + primary = optional(string) + replica = optional(string) + }) + default = {} + nullable = false +} variable "authorized_networks" { description = "Map of NAME=>CIDR_RANGE to allow to connect to the database(s)." type = map(string) @@ -28,21 +37,24 @@ variable "availability_type" { variable "backup_configuration" { description = "Backup settings for primary instance. Will be automatically enabled if using MySQL with one or more replicas." + nullable = false type = object({ - enabled = bool - binary_log_enabled = bool - start_time = string - location = string - log_retention_days = number - retention_count = number + enabled = optional(bool, false) + binary_log_enabled = optional(bool, false) + start_time = optional(string, "23:00") + location = optional(string) + log_retention_days = optional(number, 7) + point_in_time_recovery_enabled = optional(bool) + retention_count = optional(number, 7) }) default = { - enabled = false - binary_log_enabled = false - start_time = "23:00" - location = null - log_retention_days = 7 - retention_count = 7 + enabled = false + binary_log_enabled = false + start_time = "23:00" + location = null + log_retention_days = 7 + point_in_time_recovery_enabled = null + retention_count = 7 } } @@ -109,6 +121,12 @@ variable "network" { type = string } +variable "postgres_client_certificates" { + description = "Map of cert keys connect to the application(s) using public IP." + type = list(string) + default = null +} + variable "prefix" { description = "Optional prefix used to generate instance names." type = string diff --git a/modules/cloudsql-instance/versions.tf b/modules/cloudsql-instance/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/cloudsql-instance/versions.tf +++ b/modules/cloudsql-instance/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/compute-mig/README.md b/modules/compute-mig/README.md index 9356e6ecd..8bdb23386 100644 --- a/modules/compute-mig/README.md +++ b/modules/compute-mig/README.md @@ -243,9 +243,9 @@ module "nginx-mig" { target_size = 3 instance_template = module.nginx-template.template.self_link update_policy = { - minimal_action = "REPLACE" - type = "PROACTIVE" - min_ready_sec = 30 + minimal_action = "REPLACE" + type = "PROACTIVE" + min_ready_sec = 30 max_surge = { fixed = 1 } @@ -321,7 +321,7 @@ module "nginx-mig" { } } stateful_disks = { - repd-1 = null + repd-1 = false } } # tftest modules=2 resources=3 @@ -393,8 +393,8 @@ module "nginx-mig" { stateful_config = { # name needs to match a MIG instance name instance-1 = { - minimal_action = "NONE", - most_disruptive_allowed_action = "REPLACE" + minimal_action = "NONE", + most_disruptive_allowed_action = "REPLACE" preserved_state = { disks = { persistent-disk-1 = { @@ -417,10 +417,10 @@ module "nginx-mig" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [instance_template](variables.tf#L174) | Instance template for the default version. | string | ✓ | | -| [location](variables.tf#L179) | Compute zone or region. | string | ✓ | | -| [name](variables.tf#L184) | Managed group name. | string | ✓ | | -| [project_id](variables.tf#L195) | Project id. | string | ✓ | | +| [instance_template](variables.tf#L177) | Instance template for the default version. | string | ✓ | | +| [location](variables.tf#L182) | Compute zone or region. | string | ✓ | | +| [name](variables.tf#L187) | Managed group name. | string | ✓ | | +| [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 | @@ -428,14 +428,14 @@ module "nginx-mig" { | [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 | | [health_check_config](variables.tf#L104) | Optional auto-created health check configuration, use the output self-link to set it in the auto healing policy. Refer to examples for usage. | object({…}) | | null | -| [named_ports](variables.tf#L189) | Named ports. | map(number) | | null | -| [stateful_config](variables.tf#L200) | Stateful configuration for individual instances. | map(object({…})) | | {} | -| [stateful_disks](variables.tf#L219) | Stateful disk configuration applied at the MIG level to all instances, in device name => on permanent instance delete rule as boolean. | map(bool) | | {} | -| [target_pools](variables.tf#L226) | Optional list of URLs for target pools to which new instances in the group are added. | list(string) | | [] | -| [target_size](variables.tf#L232) | Group target size, leave null when using an autoscaler. | number | | null | -| [update_policy](variables.tf#L238) | Update policy. Minimal action and type are required. | object({…}) | | null | -| [versions](variables.tf#L259) | Additional application versions, target_size is optional. | map(object({…})) | | {} | -| [wait_for_instances](variables.tf#L272) | Wait for all instances to be created/updated before returning. | object({…}) | | null | +| [named_ports](variables.tf#L192) | Named ports. | map(number) | | null | +| [stateful_config](variables.tf#L203) | Stateful configuration for individual instances. | map(object({…})) | | {} | +| [stateful_disks](variables.tf#L222) | Stateful disk configuration applied at the MIG level to all instances, in device name => on permanent instance delete rule as boolean. | map(bool) | | {} | +| [target_pools](variables.tf#L229) | Optional list of URLs for target pools to which new instances in the group are added. | list(string) | | [] | +| [target_size](variables.tf#L235) | Group target size, leave null when using an autoscaler. | number | | null | +| [update_policy](variables.tf#L241) | Update policy. Minimal action and type are required. | object({…}) | | null | +| [versions](variables.tf#L262) | Additional application versions, target_size is optional. | map(object({…})) | | {} | +| [wait_for_instances](variables.tf#L275) | Wait for all instances to be created/updated before returning. | object({…}) | | null | ## Outputs diff --git a/modules/compute-mig/main.tf b/modules/compute-mig/main.tf index 35f255a68..65ce55b8e 100644 --- a/modules/compute-mig/main.tf +++ b/modules/compute-mig/main.tf @@ -71,7 +71,7 @@ resource "google_compute_instance_group_manager" "default" { for_each = var.stateful_disks content { device_name = stateful_disk.key - delete_rule = stateful_disk.value + delete_rule = stateful_disk.value ? "ON_PERMANENT_INSTANCE_DELETION" : "NEVER" } } @@ -161,7 +161,7 @@ resource "google_compute_region_instance_group_manager" "default" { for_each = var.stateful_disks content { device_name = stateful_disk.key - delete_rule = stateful_disk.value + delete_rule = stateful_disk.value ? "ON_PERMANENT_INSTANCE_DELETION" : "NEVER" } } diff --git a/modules/compute-mig/variables.tf b/modules/compute-mig/variables.tf index ecddd6687..30f2ce965 100644 --- a/modules/compute-mig/variables.tf +++ b/modules/compute-mig/variables.tf @@ -165,7 +165,10 @@ variable "health_check_config" { condition = ( (try(var.health_check_config.grpc, null) == null ? 0 : 1) + (try(var.health_check_config.http, null) == null ? 0 : 1) + - (try(var.health_check_config.tcp, null) == null ? 0 : 1) <= 1 + (try(var.health_check_config.http2, null) == null ? 0 : 1) + + (try(var.health_check_config.https, null) == null ? 0 : 1) + + (try(var.health_check_config.tcp, null) == null ? 0 : 1) + + (try(var.health_check_config.ssl, null) == null ? 0 : 1) <= 1 ) error_message = "Only one health check type can be configured at a time." } diff --git a/modules/compute-mig/versions.tf b/modules/compute-mig/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/compute-mig/versions.tf +++ b/modules/compute-mig/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/compute-vm/README.md b/modules/compute-vm/README.md index 3cf99403f..7cfaf068f 100644 --- a/modules/compute-vm/README.md +++ b/modules/compute-vm/README.md @@ -8,6 +8,23 @@ This module can operate in two distinct modes: In both modes, an optional service account can be created and assigned to either instances or template. If you need a managed instance group when using the module in template mode, refer to the [`compute-mig`](../compute-mig) module. ## Examples +- [Instance using defaults](#instance-using-defaults) +- [Service account management](#service-account-management) +- [Disk management](#disk-management) + - [Disk sources](#disk-sources) + - [Disk types and options](#disk-types-and-options) +- [Network interfaces](#network-interfaces) + - [Internal and external IPs](#internal-and-external-ips) + - [Using Alias IPs](#using-alias-ips) + - [Using gVNIC](#using-gvnic) +- [Metadata](#metadata) +- [IAM](#iam) +- [Spot VM](#spot-vm) +- [Confidential compute](#confidential-compute) +- [Disk encryption with Cloud KMS](#disk-encryption-with-cloud-kms) +- [Instance template](#instance-template) +- [Instance group](#instance-group) + ### Instance using defaults @@ -25,47 +42,73 @@ module "simple-vm-example" { }] service_account_create = true } -# tftest modules=1 resources=2 - +# tftest modules=1 resources=2 inventory=simple.yaml ``` -### Spot VM +### Service account management -[Spot VMs](https://cloud.google.com/compute/docs/instances/spot) are ephemeral compute instances suitable for batch jobs and fault-tolerant workloads. Spot VMs provide new features that [preemptible instances](https://cloud.google.com/compute/docs/instances/preemptible) do not support, such as the absence of a maximum runtime. +VM service accounts can be managed in three different ways: +- You can let the module create a service account for you by settting `service_account_create = true` +- You can use an existing service account by setting `service_account_create = false` (the default value) and passing the full email address of the service account to the `service_account` variable. This is useful, for example, if you want to reuse the service account from another previously created instance, or if you want to create the service account manually with the `iam-service-account` module. In this case, you probably also want to set `service_account_scopes` to `cloud-platform`. +- Lastly, you can use the default compute service account by setting `service_account_crate = false`. Please note that using the default compute service account is not recommended. ```hcl -module "spot-vm-example" { +module "vm-managed-sa-example" { source = "./fabric/modules/compute-vm" project_id = var.project_id zone = "europe-west1-b" - name = "test" - options = { - spot = true - termination_action = "STOP" - } + name = "test1" network_interfaces = [{ network = var.vpc.self_link subnetwork = var.subnet.self_link }] service_account_create = true } -# tftest modules=1 resources=2 +module "vm-managed-sa-example2" { + source = "./fabric/modules/compute-vm" + project_id = var.project_id + zone = "europe-west1-b" + name = "test2" + network_interfaces = [{ + network = var.vpc.self_link + subnetwork = var.subnet.self_link + }] + service_account = module.vm-managed-sa-example.service_account_email + service_account_scopes = ["cloud-platform"] +} + +# not recommended +module "vm-default-sa-example2" { + source = "./fabric/modules/compute-vm" + project_id = var.project_id + zone = "europe-west1-b" + name = "test3" + network_interfaces = [{ + network = var.vpc.self_link + subnetwork = var.subnet.self_link + }] + service_account_create = false +} + +# tftest modules=3 resources=4 inventory=sas.yaml ``` -### Disk sources +### Disk management + +#### Disk sources Attached disks can be created and optionally initialized from a pre-existing source, or attached to VMs when pre-existing. The `source` and `source_type` attributes of the `attached_disks` variable allows several modes of operation: -- `source_type = "image"` can be used with zonal disks in instances and templates, set `source` to the image name or link -- `source_type = "snapshot"` can be used with instances only, set `source` to the snapshot name or link -- `source_type = "attach"` can be used for both instances and templates to attach an existing disk, set source to the name (for zonal disks) or link (for regional disks) of the existing disk to attach; no disk will be created +- `source_type = "image"` can be used with zonal disks in instances and templates, set `source` to the image name or self link +- `source_type = "snapshot"` can be used with instances only, set `source` to the snapshot name or self link +- `source_type = "attach"` can be used for both instances and templates to attach an existing disk, set source to the name (for zonal disks) or self link (for regional disks) of the existing disk to attach; no disk will be created - `source_type = null` can be used where an empty disk is needed, `source` becomes irrelevant and can be left null This is an example of attaching a pre-existing regional PD to a new instance: ```hcl -module "simple-vm-example" { +module "vm-disks-example" { source = "./fabric/modules/compute-vm" project_id = var.project_id zone = "${var.region}-b" @@ -91,7 +134,7 @@ module "simple-vm-example" { And the same example for an instance template (where not using the full self link of the disk triggers recreation of the template) ```hcl -module "simple-vm-example" { +module "vm-disks-example" { source = "./fabric/modules/compute-vm" project_id = var.project_id zone = "${var.region}-b" @@ -110,44 +153,87 @@ module "simple-vm-example" { } }] service_account_create = true - create_template = true + create_template = true } # tftest modules=1 resources=2 ``` -### Disk encryption with Cloud KMS +#### Disk types and options -This example shows how to control disk encryption via the the `encryption` variable, in this case the self link to a KMS CryptoKey that will be used to encrypt boot and attached disk. Managing the key with the `../kms` module is of course possible, but is not shown here. +The `attached_disks` variable exposes an `option` attribute that can be used to fine tune the configuration of each disk. The following example shows a VM with multiple disks ```hcl -module "kms-vm-example" { +module "vm-disk-options-example" { source = "./fabric/modules/compute-vm" project_id = var.project_id zone = "europe-west1-b" - name = "kms-test" + name = "test" network_interfaces = [{ network = var.vpc.self_link subnetwork = var.subnet.self_link }] attached_disks = [ { - name = "attached-disk" - size = 10 + name = "data1" + size = "10" + source_type = "image" + source = "image-1" + options = { + auto_delete = false + replica_zone = "europe-west1-c" + } + }, + { + name = "data2" + size = "20" + source_type = "snapshot" + source = "snapshot-2" + options = { + type = "pd-ssd" + mode = "READ_ONLY" + } } ] service_account_create = true - boot_disk = { - image = "projects/debian-cloud/global/images/family/debian-10" - } - encryption = { - encrypt_boot = true - kms_key_self_link = var.kms_key.self_link - } } -# tftest modules=1 resources=3 +# tftest modules=1 resources=4 inventory=disk-options.yaml ``` -### Using Alias IPs +### Network interfaces + +#### Internal and external IPs + +By default VNs are create with an automatically assigned IP addresses, but you can change it through the `addreses` and `nat` attributes of the `network_interfaces` variable: + +```hcl +module "vm-internal-ip" { + source = "./fabric/modules/compute-vm" + project_id = "my-project" + zone = "europe-west1-b" + name = "vm-internal-ip" + network_interfaces = [{ + network = var.vpc.self_link + subnetwork = var.subnet.self_link + addresses = { external = null, internal = "10.0.0.2" } + }] +} + +module "vm-external-ip" { + source = "./fabric/modules/compute-vm" + project_id = "my-project" + zone = "europe-west1-b" + name = "vm-external-ip" + network_interfaces = [{ + network = var.vpc.self_link + subnetwork = var.subnet.self_link + nat = true + addresses = { external = "8.8.8.8", internal = null } + }] +} +# tftest modules=2 resources=2 inventory=ips.yaml +``` + +#### Using Alias IPs This example shows how to add additional [Alias IPs](https://cloud.google.com/vpc/docs/alias-ip) to your VM. @@ -164,21 +250,20 @@ module "vm-with-alias-ips" { alias1 = "10.16.0.10/32" } }] - service_account_create = true } -# tftest modules=1 resources=2 +# tftest modules=1 resources=1 inventory=alias-ips.yaml ``` -### Using gVNIC +#### Using gVNIC This example shows how to enable [gVNIC](https://cloud.google.com/compute/docs/networking/using-gvnic) on your VM by customizing a `cos` image. Given that gVNIC needs to be enabled as an instance configuration and as a guest os configuration, you'll need to supply a bootable disk with `guest_os_features=GVNIC`. `SEV_CAPABLE`, `UEFI_COMPATIBLE` and `VIRTIO_SCSI_MULTIQUEUE` are enabled implicitly in the `cos`, `rhel`, `centos` and other images. ```hcl resource "google_compute_image" "cos-gvnic" { - project = "my-project" - name = "my-image" - source_image = "https://www.googleapis.com/compute/v1/projects/cos-cloud/global/images/cos-89-16108-534-18" + project = "my-project" + name = "my-image" + source_image = "https://www.googleapis.com/compute/v1/projects/cos-cloud/global/images/cos-89-16108-534-18" guest_os_features { type = "GVNIC" @@ -200,8 +285,8 @@ module "vm-with-gvnic" { zone = "europe-west1-b" name = "test" boot_disk = { - image = google_compute_image.cos-gvnic.self_link - type = "pd-ssd" + image = google_compute_image.cos-gvnic.self_link + type = "pd-ssd" } network_interfaces = [{ network = var.vpc.self_link @@ -210,9 +295,151 @@ module "vm-with-gvnic" { }] service_account_create = true } -# tftest modules=1 resources=3 +# tftest modules=1 resources=3 inventory=gvnic.yaml ``` +### Metadata + +You can define labels and custom metadata values. Metadata can be leveraged, for example, to define a custom startup script. + +```hcl +module "vm-metadata-example" { + source = "./fabric/modules/compute-vm" + project_id = var.project_id + zone = "europe-west1-b" + name = "nginx-server" + network_interfaces = [{ + network = var.vpc.self_link + subnetwork = var.subnet.self_link + }] + labels = { + env = "dev" + system = "crm" + } + metadata = { + startup-script = <<-EOF + #! /bin/bash + apt-get update + apt-get install -y nginx + EOF + } + service_account_create = true +} +# tftest modules=1 resources=2 inventory=metadata.yaml +``` + +### IAM + +Like most modules, you can assign IAM roles to the instance using the `iam` variable. + +```hcl +module "vm-iam-example" { + source = "./fabric/modules/compute-vm" + project_id = var.project_id + zone = "europe-west1-b" + name = "webserver" + network_interfaces = [{ + network = var.vpc.self_link + subnetwork = var.subnet.self_link + }] + iam = { + "roles/compute.instanceAdmin" = [ + "group:webserver@example.com", + "group:admin@example.com" + ] + } +} +# tftest modules=1 resources=2 inventory=iam.yaml + +``` + +### Spot VM + +[Spot VMs](https://cloud.google.com/compute/docs/instances/spot) are ephemeral compute instances suitable for batch jobs and fault-tolerant workloads. Spot VMs provide new features that [preemptible instances](https://cloud.google.com/compute/docs/instances/preemptible) do not support, such as the absence of a maximum runtime. + +```hcl +module "spot-vm-example" { + source = "./fabric/modules/compute-vm" + project_id = var.project_id + zone = "europe-west1-b" + name = "test" + options = { + spot = true + termination_action = "STOP" + } + network_interfaces = [{ + network = var.vpc.self_link + subnetwork = var.subnet.self_link + }] +} +# tftest modules=1 resources=1 inventory=spot.yaml +``` + +### Confidential compute + +You can enable confidential compute with the `confidential_compute` variable, which can be used for standalone instances or for instance templates. + +```hcl +module "vm-confidential-example" { + source = "./fabric/modules/compute-vm" + project_id = var.project_id + zone = "europe-west1-b" + name = "confidential-vm" + confidential_compute = true + network_interfaces = [{ + network = var.vpc.self_link + subnetwork = var.subnet.self_link + }] + +} + +module "template-confidential-example" { + source = "./fabric/modules/compute-vm" + project_id = var.project_id + zone = "europe-west1-b" + name = "confidential-template" + confidential_compute = true + create_template = true + network_interfaces = [{ + network = var.vpc.self_link + subnetwork = var.subnet.self_link + }] +} + +# tftest modules=2 resources=2 inventory=confidential.yaml +``` + +### Disk encryption with Cloud KMS + +This example shows how to control disk encryption via the the `encryption` variable, in this case the self link to a KMS CryptoKey that will be used to encrypt boot and attached disk. Managing the key with the `../kms` module is of course possible, but is not shown here. + +```hcl +module "kms-vm-example" { + source = "./fabric/modules/compute-vm" + project_id = var.project_id + zone = "europe-west1-b" + name = "kms-test" + network_interfaces = [{ + network = var.vpc.self_link + subnetwork = var.subnet.self_link + }] + attached_disks = [{ + name = "attached-disk" + size = 10 + }] + service_account_create = true + boot_disk = { + image = "projects/debian-cloud/global/images/family/debian-10" + } + encryption = { + encrypt_boot = true + kms_key_self_link = var.kms_key.self_link + } +} +# tftest modules=1 resources=3 inventory=cmek.yaml +``` + + ### Instance template This example shows how to use the module to manage an instance template that defines an additional attached disk for each instance, and overrides defaults for the boot disk image and service account. @@ -239,7 +466,7 @@ module "cos-test" { service_account = "vm-default@my-project.iam.gserviceaccount.com" create_template = true } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 inventory=template.yaml ``` ### Instance group @@ -270,7 +497,7 @@ module "instance-group" { } group = { named_ports = {} } } -# tftest modules=1 resources=2 +# tftest modules=1 resources=2 inventory=group.yaml ``` @@ -278,34 +505,34 @@ module "instance-group" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L180) | Instance name. | string | ✓ | | -| [network_interfaces](variables.tf#L185) | Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed. | list(object({…})) | ✓ | | -| [project_id](variables.tf#L222) | Project id. | string | ✓ | | -| [zone](variables.tf#L281) | Compute zone. | string | ✓ | | +| [name](variables.tf#L181) | Instance name. | string | ✓ | | +| [network_interfaces](variables.tf#L186) | Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed. | list(object({…})) | ✓ | | +| [project_id](variables.tf#L223) | Project id. | string | ✓ | | +| [zone](variables.tf#L282) | Compute zone. | string | ✓ | | | [attached_disk_defaults](variables.tf#L17) | Defaults for attached disks options. | object({…}) | | {…} | -| [attached_disks](variables.tf#L38) | Additional disks, if options is null defaults will be used in its place. Source type is one of 'image' (zonal disks in vms and template), 'snapshot' (vm), 'existing', and null. | list(object({…})) | | [] | -| [boot_disk](variables.tf#L81) | Boot disk properties. | object({…}) | | {…} | -| [can_ip_forward](variables.tf#L97) | Enable IP forwarding. | bool | | false | -| [confidential_compute](variables.tf#L103) | Enable Confidential Compute for these instances. | bool | | false | -| [create_template](variables.tf#L109) | Create instance template instead of instances. | bool | | false | -| [description](variables.tf#L114) | Description of a Compute Instance. | string | | "Managed by the compute-vm Terraform module." | -| [enable_display](variables.tf#L120) | Enable virtual display on the instances. | bool | | false | -| [encryption](variables.tf#L126) | Encryption options. Only one of kms_key_self_link and disk_encryption_key_raw may be set. If needed, you can specify to encrypt or not the boot disk. | object({…}) | | null | -| [group](variables.tf#L136) | Define this variable to create an instance group for instances. Disabled for template use. | object({…}) | | null | -| [hostname](variables.tf#L144) | Instance FQDN name. | string | | null | -| [iam](variables.tf#L150) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [instance_type](variables.tf#L156) | Instance type. | string | | "f1-micro" | -| [labels](variables.tf#L162) | Instance labels. | map(string) | | {} | -| [metadata](variables.tf#L168) | Instance metadata. | map(string) | | {} | -| [min_cpu_platform](variables.tf#L174) | Minimum CPU platform. | string | | null | -| [options](variables.tf#L200) | Instance options. | object({…}) | | {…} | -| [scratch_disks](variables.tf#L227) | Scratch disks configuration. | object({…}) | | {…} | -| [service_account](variables.tf#L239) | Service account email. Unused if service account is auto-created. | string | | null | -| [service_account_create](variables.tf#L245) | Auto-create service account. | bool | | false | -| [service_account_scopes](variables.tf#L253) | Scopes applied to service account. | list(string) | | [] | -| [shielded_config](variables.tf#L259) | Shielded VM configuration of the instances. | object({…}) | | null | -| [tag_bindings](variables.tf#L269) | Tag bindings for this instance, in key => tag value id format. | map(string) | | null | -| [tags](variables.tf#L275) | Instance network tags for firewall rule targets. | list(string) | | [] | +| [attached_disks](variables.tf#L38) | Additional disks, if options is null defaults will be used in its place. Source type is one of 'image' (zonal disks in vms and template), 'snapshot' (vm), 'existing', and null. | list(object({…})) | | [] | +| [boot_disk](variables.tf#L82) | Boot disk properties. | object({…}) | | {…} | +| [can_ip_forward](variables.tf#L98) | Enable IP forwarding. | bool | | false | +| [confidential_compute](variables.tf#L104) | Enable Confidential Compute for these instances. | bool | | false | +| [create_template](variables.tf#L110) | Create instance template instead of instances. | bool | | false | +| [description](variables.tf#L115) | Description of a Compute Instance. | string | | "Managed by the compute-vm Terraform module." | +| [enable_display](variables.tf#L121) | Enable virtual display on the instances. | bool | | false | +| [encryption](variables.tf#L127) | Encryption options. Only one of kms_key_self_link and disk_encryption_key_raw may be set. If needed, you can specify to encrypt or not the boot disk. | object({…}) | | null | +| [group](variables.tf#L137) | Define this variable to create an instance group for instances. Disabled for template use. | object({…}) | | null | +| [hostname](variables.tf#L145) | Instance FQDN name. | string | | null | +| [iam](variables.tf#L151) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [instance_type](variables.tf#L157) | Instance type. | string | | "f1-micro" | +| [labels](variables.tf#L163) | Instance labels. | map(string) | | {} | +| [metadata](variables.tf#L169) | Instance metadata. | map(string) | | {} | +| [min_cpu_platform](variables.tf#L175) | Minimum CPU platform. | string | | null | +| [options](variables.tf#L201) | Instance options. | object({…}) | | {…} | +| [scratch_disks](variables.tf#L228) | Scratch disks configuration. | object({…}) | | {…} | +| [service_account](variables.tf#L240) | Service account email. Unused if service account is auto-created. | string | | null | +| [service_account_create](variables.tf#L246) | Auto-create service account. | bool | | false | +| [service_account_scopes](variables.tf#L254) | Scopes applied to service account. | list(string) | | [] | +| [shielded_config](variables.tf#L260) | Shielded VM configuration of the instances. | object({…}) | | null | +| [tag_bindings](variables.tf#L270) | Tag bindings for this instance, in key => tag value id format. | map(string) | | null | +| [tags](variables.tf#L276) | Instance network tags for firewall rule targets. | list(string) | | [] | ## Outputs diff --git a/modules/compute-vm/main.tf b/modules/compute-vm/main.tf index 32051555c..31e370672 100644 --- a/modules/compute-vm/main.tf +++ b/modules/compute-vm/main.tf @@ -17,7 +17,7 @@ locals { attached_disks = { for disk in var.attached_disks : - disk.name => merge(disk, { + (disk.name != null ? disk.name : disk.device_name) => merge(disk, { options = disk.options == null ? var.attached_disk_defaults : disk.options }) } @@ -138,7 +138,7 @@ resource "google_compute_instance" "default" { for_each = local.attached_disks_zonal iterator = config content { - device_name = config.value.name + device_name = config.value.device_name != null ? config.value.device_name : config.value.name mode = config.value.options.mode source = ( config.value.source_type == "attach" @@ -152,7 +152,7 @@ resource "google_compute_instance" "default" { for_each = local.attached_disks_regional iterator = config content { - device_name = config.value.name + device_name = config.value.device_name != null ? config.value.device_name : config.value.name mode = config.value.options.mode source = ( config.value.source_type == "attach" @@ -285,7 +285,7 @@ resource "google_compute_instance_template" "default" { iterator = config content { auto_delete = config.value.options.auto_delete - device_name = config.value.name + device_name = config.value.device_name != null ? config.value.device_name : config.value.name # Cannot use `source` with any of the fields in # [disk_size_gb disk_name disk_type source_image labels] disk_type = ( diff --git a/modules/compute-vm/variables.tf b/modules/compute-vm/variables.tf index 4287999fe..dc5651775 100644 --- a/modules/compute-vm/variables.tf +++ b/modules/compute-vm/variables.tf @@ -39,6 +39,7 @@ variable "attached_disks" { description = "Additional disks, if options is null defaults will be used in its place. Source type is one of 'image' (zonal disks in vms and template), 'snapshot' (vm), 'existing', and null." type = list(object({ name = string + device_name = optional(string) size = string source = optional(string) source_type = optional(string) diff --git a/modules/compute-vm/versions.tf b/modules/compute-vm/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/compute-vm/versions.tf +++ b/modules/compute-vm/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/container-registry/versions.tf b/modules/container-registry/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/container-registry/versions.tf +++ b/modules/container-registry/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/data-catalog-policy-tag/README.md b/modules/data-catalog-policy-tag/README.md index b38ab77d5..3cf4aaafc 100644 --- a/modules/data-catalog-policy-tag/README.md +++ b/modules/data-catalog-policy-tag/README.md @@ -12,7 +12,7 @@ module "cmn-dc" { source = "./fabric/modules/data-catalog-policy-tag" name = "my-datacatalog-policy-tags" project_id = "my-project" - tags = { + tags = { low = null, medium = null, high = null } } @@ -26,10 +26,10 @@ module "cmn-dc" { source = "./fabric/modules/data-catalog-policy-tag" name = "my-datacatalog-policy-tags" project_id = "my-project" - tags = { - low = null + tags = { + low = null medium = null - high = {"roles/datacatalog.categoryFineGrainedReader" = ["group:GROUP_NAME@example.com"]} + high = { "roles/datacatalog.categoryFineGrainedReader" = ["group:GROUP_NAME@example.com"] } } iam = { "roles/datacatalog.categoryAdmin" = ["group:GROUP_NAME@example.com"] diff --git a/modules/data-catalog-policy-tag/versions.tf b/modules/data-catalog-policy-tag/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/data-catalog-policy-tag/versions.tf +++ b/modules/data-catalog-policy-tag/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/datafusion/README.md b/modules/datafusion/README.md index 79115f050..377e81452 100644 --- a/modules/datafusion/README.md +++ b/modules/datafusion/README.md @@ -8,11 +8,11 @@ This module allows simple management of ['Google Data Fusion'](https://cloud.goo ```hcl module "datafusion" { - source = "./fabric/modules/datafusion" - name = "my-datafusion" - region = "europe-west1" - project_id = "my-project" - network = "my-network-name" + source = "./fabric/modules/datafusion" + name = "my-datafusion" + region = "europe-west1" + project_id = "my-project" + network = "my-network-name" # TODO: remove the following line firewall_create = false } diff --git a/modules/datafusion/versions.tf b/modules/datafusion/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/datafusion/versions.tf +++ b/modules/datafusion/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/dns/README.md b/modules/dns/README.md index 9e461f0e5..a405ff753 100644 --- a/modules/dns/README.md +++ b/modules/dns/README.md @@ -21,7 +21,7 @@ module "private-dns" { "A myhost" = { ttl = 600, records = ["10.0.0.120"] } } } -# tftest modules=1 resources=3 +# tftest modules=1 resources=3 inventory=private-zone.yaml ``` ### Forwarding Zone @@ -36,7 +36,7 @@ module "private-dns" { client_networks = [var.vpc.self_link] forwarders = { "10.0.1.1" = null, "1.2.3.4" = "private" } } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 inventory=forwarding-zone.yaml ``` ### Peering Zone @@ -47,11 +47,12 @@ module "private-dns" { project_id = "myproject" type = "peering" name = "test-example" - domain = "test.example." + domain = "." + description = "Forwarding zone for ." client_networks = [var.vpc.self_link] peer_network = var.vpc2.self_link } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 inventory=peering-zone.yaml ``` ### Routing Policies @@ -84,7 +85,7 @@ module "private-dns" { } } } -# tftest modules=1 resources=4 +# tftest modules=1 resources=4 inventory=routing-policies.yaml ``` ### Reverse Lookup Zone @@ -98,7 +99,23 @@ module "private-dns" { domain = "0.0.10.in-addr.arpa." client_networks = [var.vpc.self_link] } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 inventory=reverse-zone.yaml +``` + +### Public Zone + +```hcl +module "public-dns" { + source = "./fabric/modules/dns" + project_id = "myproject" + type = "public" + name = "example" + domain = "example.com." + recordsets = { + "A myhost" = { ttl = 300, records = ["127.0.0.1"] } + } +} +# tftest modules=1 resources=3 inventory=public-zone.yaml ``` diff --git a/modules/dns/main.tf b/modules/dns/main.tf index ca30c7d0c..edf342ef6 100644 --- a/modules/dns/main.tf +++ b/modules/dns/main.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/modules/dns/versions.tf b/modules/dns/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/dns/versions.tf +++ b/modules/dns/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/endpoints/README.md b/modules/endpoints/README.md index 4e6fd2eb9..3b9e317db 100644 --- a/modules/endpoints/README.md +++ b/modules/endpoints/README.md @@ -22,7 +22,7 @@ module "endpoint" { ``` ```yaml -# tftest file openapi configs/endpoints/openapi.yaml +# tftest-file id=openapi path=configs/endpoints/openapi.yaml swagger: "2.0" info: description: "A simple Google Cloud Endpoints API example." diff --git a/modules/endpoints/versions.tf b/modules/endpoints/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/endpoints/versions.tf +++ b/modules/endpoints/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/folder/README.md b/modules/folder/README.md index 5120d2467..e1ad6809e 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -2,29 +2,36 @@ This module allows the creation and management of folders, including support for IAM bindings, organization policies, and hierarchical firewall rules. -## Examples - -### IAM bindings +## Basic example with IAM bindings ```hcl module "folder" { source = "./fabric/modules/folder" parent = "organizations/1234567890" - name = "Folder name" - group_iam = { + name = "Folder name" + group_iam = { "cloud-owners@example.org" = [ - "roles/owner", - "roles/resourcemanager.projectCreator" + "roles/owner", + "roles/resourcemanager.folderAdmin", + "roles/resourcemanager.projectCreator" ] } iam = { - "roles/owner" = ["user:one@example.com"] + "roles/owner" = ["user:one@example.org"] + } + iam_additive = { + "roles/compute.admin" = ["user:a1@example.org", "user:a2@example.org"] + "roles/compute.viewer" = ["user:a2@example.org"] + } + iam_additive_members = { + "user:am1@example.org" = ["roles/storage.admin"] + "user:am2@example.org" = ["roles/storage.objectViewer"] } } -# tftest modules=1 resources=3 +# tftest modules=1 resources=9 inventory=iam.yaml ``` -### Organization policies +## Organization policies To manage organization policies, the `orgpolicy.googleapis.com` service should be enabled in the quota project. @@ -32,7 +39,7 @@ To manage organization policies, the `orgpolicy.googleapis.com` service should b module "folder" { source = "./fabric/modules/folder" parent = "organizations/1234567890" - name = "Folder name" + name = "Folder name" org_policies = { "compute.disableGuestAttributesAccess" = { enforce = true @@ -72,70 +79,14 @@ module "folder" { } } } -# tftest modules=1 resources=8 +# tftest modules=1 resources=8 inventory=org-policies.yaml ``` ### Organization policy factory See the [organization policy factory in the project module](../project#organization-policy-factory). -### Firewall policy factory - -In the same way as for the [organization](../organization) module, the in-built factory allows you to define a single policy, using one file for rules, and an optional file for CIDR range substitution variables. Remember that non-absolute paths are relative to the root module (the folder where you run `terraform`). - -```hcl -module "folder" { - source = "./fabric/modules/folder" - parent = "organizations/1234567890" - name = "Folder name" - firewall_policy_factory = { - cidr_file = "configs/firewall-policies/cidrs.yaml" - policy_name = null - rules_file = "configs/firewall-policies/rules.yaml" - } - firewall_policy_association = { - factory-policy = module.folder.firewall_policy_id["factory"] - } -} -# tftest modules=1 resources=5 files=cidrs,rules -``` - -```yaml -# tftest file cidrs configs/firewall-policies/cidrs.yaml -rfc1918: - - 10.0.0.0/8 - - 172.16.0.0/12 - - 192.168.0.0/16 -``` - -```yaml -# tftest file rules configs/firewall-policies/rules.yaml -allow-admins: - description: Access from the admin subnet to all subnets - direction: INGRESS - action: allow - priority: 1000 - ranges: - - $rfc1918 - ports: - all: [] - target_resources: null - enable_logging: false - -allow-ssh-from-iap: - description: Enable SSH from IAP - direction: INGRESS - action: allow - priority: 1002 - ranges: - - 35.235.240.0/20 - ports: - tcp: ["22"] - target_resources: null - enable_logging: false -``` - -### Logging Sinks +## Logging Sinks ```hcl module "gcs" { @@ -164,7 +115,6 @@ module "bucket" { id = "bucket" } - module "folder-sink" { source = "./fabric/modules/folder" parent = "folders/657104291943" @@ -198,10 +148,19 @@ module "folder-sink" { no-gce-instances = "resource.type=gce_instance" } } -# tftest modules=5 resources=14 +# tftest modules=5 resources=14 inventory=logging.yaml ``` -### Hierarchical firewall policies +## Hierarchical firewall policies + +Hierarchical firewall policies can be managed in two ways: + +- via the `firewall_policies` variable, to directly define policies and rules in Terraform +- via the `firewall_policy_factory` variable, to leverage external YaML files via a simple "factory" embedded in the module ([see here](../../blueprints/factories) for more context on factories) + +Once you have policies (either created via the module or externally), you can associate them using the `firewall_policy_association` variable. + +### Directly defined firewall policies ```hcl module "folder1" { @@ -211,6 +170,17 @@ module "folder1" { firewall_policies = { iap-policy = { + allow-admins = { + description = "Access from the admin subnet to all subnets" + direction = "INGRESS" + action = "allow" + priority = 1000 + ranges = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] + ports = { all = [] } + target_service_accounts = null + target_resources = null + logging = false + } allow-iap-ssh = { description = "Always allow ssh from IAP" direction = "INGRESS" @@ -237,7 +207,71 @@ module "folder2" { iap-policy = module.folder1.firewall_policy_id["iap-policy"] } } -# tftest modules=2 resources=6 +# tftest modules=2 resources=7 inventory=hfw.yaml +``` +### Firewall policy factory + +The in-built factory allows you to define a single policy, using one file for rules, and an optional file for CIDR range substitution variables. Remember that non-absolute paths are relative to the root module (the folder where you run `terraform`). + +```hcl +module "folder1" { + source = "./fabric/modules/folder" + parent = var.organization_id + name = "policy-container" + firewall_policy_factory = { + cidr_file = "configs/firewall-policies/cidrs.yaml" + policy_name = "iap-policy" + rules_file = "configs/firewall-policies/rules.yaml" + } + firewall_policy_association = { + iap-policy = "iap-policy" + } +} + +module "folder2" { + source = "./fabric/modules/folder" + parent = var.organization_id + name = "hf2" + firewall_policy_association = { + iap-policy = module.folder1.firewall_policy_id["iap-policy"] + } +} +# tftest modules=2 resources=7 files=cidrs,rules inventory=hfw.yaml +``` + +```yaml +# tftest-file id=cidrs path=configs/firewall-policies/cidrs.yaml +rfc1918: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 +``` + +```yaml +# tftest-file id=rules path=configs/firewall-policies/rules.yaml +allow-admins: + description: Access from the admin subnet to all subnets + direction: INGRESS + action: allow + priority: 1000 + ranges: + - $rfc1918 + ports: + all: [] + target_resources: null + logging: false + +allow-iap-ssh: + description: "Always allow ssh from IAP" + direction: INGRESS + action: allow + priority: 100 + ranges: + - 35.235.240.0/20 + ports: + tcp: ["22"] + target_resources: null + logging: false ``` ## Tags @@ -250,8 +284,8 @@ module "org" { organization_id = var.organization_id tags = { environment = { - description = "Environment specification." - iam = null + description = "Environment specification." + iam = null values = { dev = null prod = null @@ -269,7 +303,7 @@ module "folder" { foo = "tagValues/12345678" } } -# tftest modules=2 resources=6 +# tftest modules=2 resources=6 inventory=tags.yaml ``` diff --git a/modules/folder/organization-policies.tf b/modules/folder/organization-policies.tf index 999d1c586..47532f21b 100644 --- a/modules/folder/organization-policies.tf +++ b/modules/folder/organization-policies.tf @@ -95,23 +95,6 @@ resource "google_org_policy_policy" "default" { inherit_from_parent = each.value.inherit_from_parent reset = each.value.reset - rules { - allow_all = try(each.value.allow.all, null) == true ? "TRUE" : null - deny_all = try(each.value.deny.all, null) == true ? "TRUE" : null - enforce = ( - each.value.is_boolean_policy && each.value.enforce != null - ? upper(tostring(each.value.enforce)) - : null - ) - dynamic "values" { - for_each = each.value.has_values ? [1] : [] - content { - allowed_values = try(each.value.allow.values, null) - denied_values = try(each.value.deny.values, null) - } - } - } - dynamic "rules" { for_each = each.value.rules iterator = rule @@ -138,5 +121,22 @@ resource "google_org_policy_policy" "default" { } } } + + rules { + allow_all = try(each.value.allow.all, null) == true ? "TRUE" : null + deny_all = try(each.value.deny.all, null) == true ? "TRUE" : null + enforce = ( + each.value.is_boolean_policy && each.value.enforce != null + ? upper(tostring(each.value.enforce)) + : null + ) + dynamic "values" { + for_each = each.value.has_values ? [1] : [] + content { + allowed_values = try(each.value.allow.values, null) + denied_values = try(each.value.deny.values, null) + } + } + } } } diff --git a/modules/folder/versions.tf b/modules/folder/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/folder/versions.tf +++ b/modules/folder/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/gcs/README.md b/modules/gcs/README.md index 7e6cc22f4..07c5a6d7b 100644 --- a/modules/gcs/README.md +++ b/modules/gcs/README.md @@ -1,4 +1,5 @@ # Google Cloud Storage Module + ## Example ```hcl @@ -7,52 +8,46 @@ module "bucket" { project_id = "myproject" prefix = "test" name = "my-bucket" + versioning = true iam = { "roles/storage.admin" = ["group:storage@example.com"] } + labels = { + cost-center = "devops" + } } -# tftest modules=1 resources=2 +# tftest modules=1 resources=2 inventory=simple.yaml ``` ### Example with Cloud KMS ```hcl module "bucket" { - source = "./fabric/modules/gcs" - project_id = "myproject" - prefix = "test" - name = "my-bucket" - iam = { - "roles/storage.admin" = ["group:storage@example.com"] - } + source = "./fabric/modules/gcs" + project_id = "myproject" + name = "my-bucket" encryption_key = "my-encryption-key" } -# tftest modules=1 resources=2 +# tftest modules=1 resources=1 inventory=cmek.yaml ``` -### Example with retention policy +### Example with retention policy and logging ```hcl module "bucket" { source = "./fabric/modules/gcs" project_id = "myproject" - prefix = "test" name = "my-bucket" - iam = { - "roles/storage.admin" = ["group:storage@example.com"] - } - retention_policy = { retention_period = 100 is_locked = true } - logging_config = { - log_bucket = var.bucket + log_bucket = "log-bucket" log_object_prefix = null } } -# tftest modules=1 resources=2 +# tftest modules=1 resources=1 inventory=retention-logging.yaml ``` ### Example with lifecycle rule @@ -61,39 +56,28 @@ module "bucket" { module "bucket" { source = "./fabric/modules/gcs" project_id = "myproject" - prefix = "test" - name = "my-bucket" - - iam = { - "roles/storage.admin" = ["group:storage@example.com"] - } - - lifecycle_rule = { - action = { - type = "SetStorageClass" - storage_class = "STANDARD" - } - condition = { - age = 30 - created_before = null - with_state = null - matches_storage_class = null - num_newer_versions = null - custom_time_before = null - days_since_custom_time = null - days_since_noncurrent_time = null - noncurrent_time_before = null + name = "my-bucket" + lifecycle_rules = { + lr-0 = { + action = { + type = "SetStorageClass" + storage_class = "STANDARD" + } + condition = { + age = 30 + } } } } -# tftest modules=1 resources=2 +# tftest modules=1 resources=1 inventory=lifecycle.yaml ``` + ### Minimal example with GCS notifications + ```hcl module "bucket-gcs-notification" { source = "./fabric/modules/gcs" project_id = "myproject" - prefix = "test" name = "my-bucket" notification_config = { enabled = true @@ -104,7 +88,7 @@ module "bucket-gcs-notification" { custom_attributes = {} } } -# tftest modules=1 resources=4 +# tftest modules=1 resources=4 inventory=notification.yaml ``` @@ -112,23 +96,23 @@ module "bucket-gcs-notification" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L89) | Bucket name suffix. | string | ✓ | | -| [project_id](variables.tf#L117) | Bucket project id. | string | ✓ | | -| [cors](variables.tf#L17) | CORS configuration for the bucket. Defaults to null. | object({…}) | | null | +| [name](variables.tf#L116) | Bucket name suffix. | string | ✓ | | +| [project_id](variables.tf#L145) | Bucket project id. | string | ✓ | | +| [cors](variables.tf#L17) | CORS configuration for the bucket. Defaults to null. | object({…}) | | null | | [encryption_key](variables.tf#L28) | KMS key that will be used for encryption. | string | | null | | [force_destroy](variables.tf#L34) | Optional map to set force destroy keyed by name, defaults to false. | bool | | false | | [iam](variables.tf#L40) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [labels](variables.tf#L46) | Labels to be attached to all buckets. | map(string) | | {} | -| [lifecycle_rule](variables.tf#L52) | Bucket lifecycle rule. | object({…}) | | null | -| [location](variables.tf#L74) | Bucket location. | string | | "EU" | -| [logging_config](variables.tf#L80) | Bucket logging configuration. | object({…}) | | null | -| [notification_config](variables.tf#L94) | GCS Notification configuration. | object({…}) | | null | -| [prefix](variables.tf#L107) | Optional prefix used to generate the bucket name. | string | | null | -| [retention_policy](variables.tf#L122) | Bucket retention policy. | object({…}) | | null | -| [storage_class](variables.tf#L131) | Bucket storage class. | string | | "MULTI_REGIONAL" | -| [uniform_bucket_level_access](variables.tf#L141) | Allow using object ACLs (false) or not (true, this is the recommended behavior) , defaults to true (which is the recommended practice, but not the behavior of storage API). | bool | | true | -| [versioning](variables.tf#L147) | Enable versioning, defaults to false. | bool | | false | -| [website](variables.tf#L153) | Bucket website. | object({…}) | | null | +| [lifecycle_rules](variables.tf#L52) | Bucket lifecycle rule. | map(object({…})) | | {} | +| [location](variables.tf#L101) | Bucket location. | string | | "EU" | +| [logging_config](variables.tf#L107) | Bucket logging configuration. | object({…}) | | null | +| [notification_config](variables.tf#L121) | GCS Notification configuration. | object({…}) | | null | +| [prefix](variables.tf#L135) | Optional prefix used to generate the bucket name. | string | | null | +| [retention_policy](variables.tf#L150) | Bucket retention policy. | object({…}) | | null | +| [storage_class](variables.tf#L159) | Bucket storage class. | string | | "MULTI_REGIONAL" | +| [uniform_bucket_level_access](variables.tf#L169) | Allow using object ACLs (false) or not (true, this is the recommended behavior) , defaults to true (which is the recommended practice, but not the behavior of storage API). | bool | | true | +| [versioning](variables.tf#L175) | Enable versioning, defaults to false. | bool | | false | +| [website](variables.tf#L181) | Bucket website. | object({…}) | | null | ## Outputs diff --git a/modules/gcs/main.tf b/modules/gcs/main.tf index 960b23d2e..68b77077d 100644 --- a/modules/gcs/main.tf +++ b/modules/gcs/main.tf @@ -75,22 +75,25 @@ resource "google_storage_bucket" "bucket" { } dynamic "lifecycle_rule" { - for_each = var.lifecycle_rule == null ? [] : [""] + for_each = var.lifecycle_rules + iterator = rule content { action { - type = var.lifecycle_rule.action["type"] - storage_class = var.lifecycle_rule.action["storage_class"] + type = rule.value.action.type + storage_class = rule.value.action.storage_class } condition { - age = var.lifecycle_rule.condition["age"] - created_before = var.lifecycle_rule.condition["created_before"] - with_state = var.lifecycle_rule.condition["with_state"] - matches_storage_class = var.lifecycle_rule.condition["matches_storage_class"] - num_newer_versions = var.lifecycle_rule.condition["num_newer_versions"] - custom_time_before = var.lifecycle_rule.condition["custom_time_before"] - days_since_custom_time = var.lifecycle_rule.condition["days_since_custom_time"] - days_since_noncurrent_time = var.lifecycle_rule.condition["days_since_noncurrent_time"] - noncurrent_time_before = var.lifecycle_rule.condition["noncurrent_time_before"] + age = rule.value.condition.age + created_before = rule.value.condition.created_before + custom_time_before = rule.value.condition.custom_time_before + days_since_custom_time = rule.value.condition.days_since_custom_time + days_since_noncurrent_time = rule.value.condition.days_since_noncurrent_time + matches_prefix = rule.value.condition.matches_prefix + matches_storage_class = rule.value.condition.matches_storage_class + matches_suffix = rule.value.condition.matches_suffix + noncurrent_time_before = rule.value.condition.noncurrent_time_before + num_newer_versions = rule.value.condition.num_newer_versions + with_state = rule.value.condition.with_state } } } @@ -104,15 +107,14 @@ resource "google_storage_bucket_iam_binding" "bindings" { } resource "google_storage_notification" "notification" { - count = local.notification ? 1 : 0 - bucket = google_storage_bucket.bucket.name - payload_format = var.notification_config.payload_format - topic = google_pubsub_topic.topic[0].id - event_types = var.notification_config.event_types - custom_attributes = var.notification_config.custom_attributes - - depends_on = [google_pubsub_topic_iam_binding.binding] - + count = local.notification ? 1 : 0 + bucket = google_storage_bucket.bucket.name + payload_format = var.notification_config.payload_format + topic = google_pubsub_topic.topic[0].id + custom_attributes = var.notification_config.custom_attributes + event_types = var.notification_config.event_types + object_name_prefix = var.notification_config.object_name_prefix + depends_on = [google_pubsub_topic_iam_binding.binding] } resource "google_pubsub_topic_iam_binding" "binding" { count = local.notification ? 1 : 0 diff --git a/modules/gcs/variables.tf b/modules/gcs/variables.tf index 2e1517234..58aec4dfb 100644 --- a/modules/gcs/variables.tf +++ b/modules/gcs/variables.tf @@ -17,10 +17,10 @@ variable "cors" { description = "CORS configuration for the bucket. Defaults to null." type = object({ - origin = list(string) - method = list(string) - response_header = list(string) - max_age_seconds = number + origin = optional(list(string)) + method = optional(list(string)) + response_header = optional(list(string)) + max_age_seconds = optional(number) }) default = null } @@ -49,26 +49,53 @@ variable "labels" { default = {} } -variable "lifecycle_rule" { +variable "lifecycle_rules" { description = "Bucket lifecycle rule." - type = object({ + type = map(object({ action = object({ type = string - storage_class = string + storage_class = optional(string) }) condition = object({ - age = number - created_before = string - with_state = string - matches_storage_class = list(string) - num_newer_versions = string - custom_time_before = string - days_since_custom_time = string - days_since_noncurrent_time = string - noncurrent_time_before = string + age = optional(number) + created_before = optional(string) + custom_time_before = optional(string) + days_since_custom_time = optional(number) + days_since_noncurrent_time = optional(number) + matches_prefix = optional(list(string)) + matches_storage_class = optional(list(string)) # STANDARD, MULTI_REGIONAL, REGIONAL, NEARLINE, COLDLINE, ARCHIVE, DURABLE_REDUCED_AVAILABILITY + matches_suffix = optional(list(string)) + noncurrent_time_before = optional(string) + num_newer_versions = optional(number) + with_state = optional(string) # "LIVE", "ARCHIVED", "ANY" }) - }) - default = null + })) + default = {} + nullable = false + validation { + condition = alltrue([ + for k, v in var.lifecycle_rules : v.action != null && v.condition != null + ]) + error_message = "Lifecycle rules action and condition cannot be null." + } + validation { + condition = alltrue([ + for k, v in var.lifecycle_rules : contains( + ["Delete", "SetStorageClass", "AbortIncompleteMultipartUpload"], + v.action.type + ) + ]) + error_message = "Lifecycle rules action type has unsupported value." + } + validation { + condition = alltrue([ + for k, v in var.lifecycle_rules : + v.action.type != "SetStorageClass" + || + v.action.storage_class != null + ]) + error_message = "Lifecycle rules with action type SetStorageClass require a storage class." + } } variable "location" { @@ -81,7 +108,7 @@ variable "logging_config" { description = "Bucket logging configuration." type = object({ log_bucket = string - log_object_prefix = string + log_object_prefix = optional(string) }) default = null } @@ -94,12 +121,13 @@ variable "name" { variable "notification_config" { description = "GCS Notification configuration." type = object({ - enabled = bool - payload_format = string - topic_name = string - sa_email = string - event_types = list(string) - custom_attributes = map(string) + enabled = bool + payload_format = string + topic_name = string + sa_email = string + event_types = optional(list(string)) + custom_attributes = optional(map(string)) + object_name_prefix = optional(string) }) default = null } @@ -123,7 +151,7 @@ variable "retention_policy" { description = "Bucket retention policy." type = object({ retention_period = number - is_locked = bool + is_locked = optional(bool) }) default = null } @@ -153,8 +181,8 @@ variable "versioning" { variable "website" { description = "Bucket website." type = object({ - main_page_suffix = string - not_found_page = string + main_page_suffix = optional(string) + not_found_page = optional(string) }) default = null } diff --git a/modules/gcs/versions.tf b/modules/gcs/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/gcs/versions.tf +++ b/modules/gcs/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/gke-cluster/README.md b/modules/gke-cluster/README.md index 55b594c69..2e09aeb11 100644 --- a/modules/gke-cluster/README.md +++ b/modules/gke-cluster/README.md @@ -22,7 +22,7 @@ module "cluster-1" { master_authorized_ranges = { internal-vms = "10.0.0.0/8" } - master_ipv4_cidr_block = "192.168.0.0/28" + master_ipv4_cidr_block = "192.168.0.0/28" } max_pods_per_node = 32 private_cluster_config = { @@ -33,7 +33,7 @@ module "cluster-1" { environment = "dev" } } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 inventory=basic.yaml ``` ### GKE Cluster with Dataplane V2 enabled @@ -42,7 +42,7 @@ module "cluster-1" { module "cluster-1" { source = "./fabric/modules/gke-cluster" project_id = "myproject" - name = "cluster-1" + name = "cluster-dataplane-v2" location = "europe-west1-b" vpc_config = { network = var.vpc.self_link @@ -54,7 +54,7 @@ module "cluster-1" { master_authorized_ranges = { internal-vms = "10.0.0.0/8" } - master_ipv4_cidr_block = "192.168.0.0/28" + master_ipv4_cidr_block = "192.168.0.0/28" } private_cluster_config = { enable_private_endpoint = true @@ -68,7 +68,60 @@ module "cluster-1" { environment = "dev" } } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 inventory=dataplane-v2.yaml +``` +### Autopilot Cluster + +```hcl +module "cluster-autopilot" { + source = "./fabric/modules/gke-cluster" + project_id = "myproject" + name = "cluster-autopilot" + location = "europe-west1-b" + vpc_config = { + network = var.vpc.self_link + subnetwork = var.subnet.self_link + secondary_range_names = { + pods = "pods" + services = "services" + } + master_authorized_ranges = { + internal-vms = "10.0.0.0/8" + } + master_ipv4_cidr_block = "192.168.0.0/28" + } + enable_features = { + autopilot = true + workload_identity = false + } +} +# tftest modules=1 resources=1 inventory=autopilot.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. + +```hcl +module "cluster-1" { + source = "./fabric/modules/gke-cluster" + 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_names = { pods = "pods", services = "services" } + } + enable_features = { + dns = { + provider = "CLOUD_DNS" + scope = "CLUSTER_SCOPE" + domain = "gke.local" + } + } +} +# tftest modules=1 resources=1 inventory=dns.yaml ``` @@ -76,24 +129,25 @@ module "cluster-1" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [location](variables.tf#L117) | Cluster zone or region. | string | ✓ | | -| [name](variables.tf#L174) | Cluster name. | string | ✓ | | -| [project_id](variables.tf#L200) | Cluster project id. | string | ✓ | | -| [vpc_config](variables.tf#L211) | VPC-level configuration. | object({…}) | ✓ | | +| [location](variables.tf#L119) | Cluster zone or region. | string | ✓ | | +| [name](variables.tf#L176) | Cluster name. | string | ✓ | | +| [project_id](variables.tf#L202) | Cluster project id. | string | ✓ | | +| [vpc_config](variables.tf#L219) | VPC-level configuration. | object({…}) | ✓ | | | [cluster_autoscaling](variables.tf#L17) | Enable and configure limits for Node Auto-Provisioning with Cluster Autoscaler. | object({…}) | | null | | [description](variables.tf#L38) | Cluster description. | string | | null | | [enable_addons](variables.tf#L44) | Addons enabled in the cluster (true means enabled). | object({…}) | | {…} | -| [enable_features](variables.tf#L68) | Enable cluster-level features. Certain features allow configuration. | object({…}) | | {…} | -| [issue_client_certificate](variables.tf#L105) | Enable issuing client certificate. | bool | | false | -| [labels](variables.tf#L111) | Cluster resource labels. | map(string) | | null | -| [logging_config](variables.tf#L122) | Logging configuration. | list(string) | | ["SYSTEM_COMPONENTS"] | -| [maintenance_config](variables.tf#L128) | Maintenance window configuration. | object({…}) | | {…} | -| [max_pods_per_node](variables.tf#L151) | Maximum number of pods per node in this cluster. | number | | 110 | -| [min_master_version](variables.tf#L157) | Minimum version of the master, defaults to the version of the most recent official release. | string | | null | -| [monitoring_config](variables.tf#L163) | Monitoring components. | object({…}) | | {…} | -| [node_locations](variables.tf#L179) | Zones in which the cluster's nodes are located. | list(string) | | [] | -| [private_cluster_config](variables.tf#L186) | Private cluster configuration. | object({…}) | | null | -| [release_channel](variables.tf#L205) | Release channel for GKE upgrades. | string | | null | +| [enable_features](variables.tf#L68) | Enable cluster-level features. Certain features allow configuration. | object({…}) | | {…} | +| [issue_client_certificate](variables.tf#L107) | Enable issuing client certificate. | bool | | false | +| [labels](variables.tf#L113) | Cluster resource labels. | map(string) | | null | +| [logging_config](variables.tf#L124) | Logging configuration. | list(string) | | ["SYSTEM_COMPONENTS"] | +| [maintenance_config](variables.tf#L130) | Maintenance window configuration. | object({…}) | | {…} | +| [max_pods_per_node](variables.tf#L153) | Maximum number of pods per node in this cluster. | number | | 110 | +| [min_master_version](variables.tf#L159) | Minimum version of the master, defaults to the version of the most recent official release. | string | | null | +| [monitoring_config](variables.tf#L165) | Monitoring components. | object({…}) | | {…} | +| [node_locations](variables.tf#L181) | Zones in which the cluster's nodes are located. | list(string) | | [] | +| [private_cluster_config](variables.tf#L188) | Private cluster configuration. | object({…}) | | null | +| [release_channel](variables.tf#L207) | Release channel for GKE upgrades. | string | | null | +| [tags](variables.tf#L213) | Network tags applied to nodes. | list(string) | | null | ## Outputs diff --git a/modules/gke-cluster/main.tf b/modules/gke-cluster/main.tf index f4b86bf66..0079dd8d8 100644 --- a/modules/gke-cluster/main.tf +++ b/modules/gke-cluster/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,6 +15,12 @@ */ resource "google_container_cluster" "cluster" { + lifecycle { + ignore_changes = [ + node_config[0].boot_disk_kms_key, + node_config[0].spot + ] + } provider = google-beta project = var.project_id name = var.name @@ -49,13 +55,17 @@ resource "google_container_cluster" "cluster" { # the default nodepool is deleted here, use the gke-nodepool module instead # default nodepool configuration based on a shielded_nodes variable - node_config { - dynamic "shielded_instance_config" { - for_each = var.enable_features.shielded_nodes ? [""] : [] - content { - enable_secure_boot = true - enable_integrity_monitoring = true + dynamic "node_config" { + for_each = var.enable_features.autopilot ? [] : [""] + content { + dynamic "shielded_instance_config" { + for_each = var.enable_features.shielded_nodes ? [""] : [] + content { + enable_secure_boot = true + enable_integrity_monitoring = true + } } + tags = var.tags } } @@ -130,7 +140,17 @@ resource "google_container_cluster" "cluster" { dynamic "cluster_autoscaling" { for_each = var.cluster_autoscaling == null ? [] : [""] content { - enabled = true + enabled = var.enable_features.autopilot ? null : true + + dynamic "auto_provisioning_defaults" { + for_each = var.cluster_autoscaling.auto_provisioning_defaults != null ? [""] : [] + content { + boot_disk_kms_key = var.cluster_autoscaling.auto_provisioning_defaults.boot_disk_kms_key + image_type = var.cluster_autoscaling.auto_provisioning_defaults.image_type + oauth_scopes = var.cluster_autoscaling.auto_provisioning_defaults.oauth_scopes + service_account = var.cluster_autoscaling.auto_provisioning_defaults.service_account + } + } dynamic "resource_limits" { for_each = var.cluster_autoscaling.cpu_limits != null ? [""] : [] content { @@ -160,11 +180,11 @@ resource "google_container_cluster" "cluster" { } dynamic "dns_config" { - for_each = var.enable_features.cloud_dns != null ? [""] : [] + for_each = var.enable_features.dns != null ? [""] : [] content { - cluster_dns = enable_features.cloud_dns.cluster_dns - cluster_dns_scope = enable_features.cloud_dns.cluster_dns_scope - cluster_dns_domain = enable_features.cloud_dns.cluster_dns_domain + cluster_dns = var.enable_features.dns.provider + cluster_dns_scope = var.enable_features.dns.scope + cluster_dns_domain = var.enable_features.dns.domain } } @@ -190,6 +210,13 @@ 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 = ( @@ -248,6 +275,13 @@ resource "google_container_cluster" "cluster" { } } + dynamic "mesh_certificates" { + for_each = var.enable_features.mesh_certificates != null ? [""] : [] + content { + enable_certificates = var.enable_features.mesh_certificates + } + } + dynamic "monitoring_config" { for_each = var.monitoring_config != null && !var.enable_features.autopilot ? [""] : [] content { diff --git a/modules/gke-cluster/variables.tf b/modules/gke-cluster/variables.tf index f9a3b69e3..a51ff2087 100644 --- a/modules/gke-cluster/variables.tf +++ b/modules/gke-cluster/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. @@ -70,7 +70,7 @@ variable "enable_features" { type = object({ autopilot = optional(bool, false) binary_authorization = optional(bool, false) - cloud_dns = optional(object({ + dns = optional(object({ provider = optional(string) scope = optional(string) domain = optional(string) @@ -80,9 +80,11 @@ variable "enable_features" { key_name = string })) dataplane_v2 = optional(bool, false) + gateway_api = optional(bool, false) groups_for_rbac = optional(string) intranode_visibility = optional(bool, false) l4_ilb_subsetting = optional(bool, false) + mesh_certificates = optional(bool) pod_security_policy = optional(bool, false) resource_usage_export = optional(object({ dataset = string @@ -95,7 +97,7 @@ variable "enable_features" { topic_id = optional(string) })) vertical_pod_autoscaling = optional(bool, false) - workload_identity = optional(bool, false) + workload_identity = optional(bool, true) }) default = { workload_identity = true @@ -208,6 +210,12 @@ variable "release_channel" { default = null } +variable "tags" { + description = "Network tags applied to nodes." + type = list(string) + default = null +} + variable "vpc_config" { description = "VPC-level configuration." type = object({ diff --git a/modules/gke-cluster/versions.tf b/modules/gke-cluster/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/gke-cluster/versions.tf +++ b/modules/gke-cluster/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/gke-hub/README.md b/modules/gke-hub/README.md index 9fe47344d..793a81cac 100644 --- a/modules/gke-hub/README.md +++ b/modules/gke-hub/README.md @@ -56,7 +56,7 @@ module "cluster_1" { master_authorized_ranges = { fc1918_10_8 = "10.0.0.0/8" } - master_ipv4_cidr_block = "192.168.0.0/28" + master_ipv4_cidr_block = "192.168.0.0/28" } enable_features = { dataplane_v2 = true @@ -115,11 +115,11 @@ module "hub" { } } configmanagement_clusters = { - "default" = [ "cluster-1" ] + "default" = ["cluster-1"] } } -# tftest modules=4 resources=15 +# tftest modules=4 resources=16 ``` ## Multi-cluster mesh on GKE @@ -141,6 +141,13 @@ module "project" { ] } +resource "google_project_iam_member" "gkehub_fix" { + member = "serviceAccount:${module.project.service_accounts.robots.fleet}" + project = module.project.project_id + role = "roles/gkehub.serviceAgent" +} + + module "vpc" { source = "./fabric/modules/net-vpc" project_id = module.project.project_id @@ -151,7 +158,7 @@ module "vpc" { ip_cidr_range = "10.0.1.0/24" name = "subnet-cluster-1" region = "europe-west1" - secondary_ip_range = { + secondary_ip_ranges = { pods = "10.1.0.0/16" services = "10.2.0.0/24" } @@ -160,16 +167,16 @@ module "vpc" { ip_cidr_range = "10.0.2.0/24" name = "subnet-cluster-2" region = "europe-west4" - secondary_ip_range = { + secondary_ip_ranges = { pods = "10.3.0.0/16" services = "10.4.0.0/24" } }, { - ip_cidr_range = "10.0.0.0/28" - name = "subnet-mgmt" - region = "europe-west1" - secondary_ip_range = null + ip_cidr_range = "10.0.0.0/28" + name = "subnet-mgmt" + region = "europe-west1" + secondary_ip_ranges = null } ] } @@ -216,34 +223,40 @@ module "cluster_1" { mgmt = "10.0.0.0/28" pods-cluster-1 = "10.3.0.0/16" } - master_ipv4_cidr_block = "192.168.1.0/28" + master_ipv4_cidr_block = "192.168.1.0/28" } private_cluster_config = { enable_private_endpoint = false master_global_access = true } + release_channel = "REGULAR" labels = { mesh_id = "proj-${module.project.number}" } + enable_features = { + workload_identity = true + dataplane_v2 = true + } } module "cluster_1_nodepool" { source = "./fabric/modules/gke-nodepool" project_id = module.project.project_id cluster_name = module.cluster_1.name + cluster_id = module.cluster_1.id location = "europe-west1" - name = "nodepool" + name = "cluster-1-nodepool" node_count = { initial = 1 } service_account = { create = true } tags = ["cluster-1-node"] } module "cluster_2" { - source = "./fabric/modules/gke-cluster" - project_id = module.project.project_id - name = "cluster-2" - location = "europe-west4" + source = "./fabric/modules/gke-cluster" + project_id = module.project.project_id + name = "cluster-2" + location = "europe-west4" vpc_config = { network = module.vpc.self_link subnetwork = module.vpc.subnet_self_links["europe-west4/subnet-cluster-2"] @@ -251,7 +264,7 @@ module "cluster_2" { mgmt = "10.0.0.0/28" pods-cluster-1 = "10.3.0.0/16" } - master_ipv4_cidr_block = "192.168.2.0/28" + master_ipv4_cidr_block = "192.168.2.0/28" } private_cluster_config = { enable_private_endpoint = false @@ -261,14 +274,19 @@ module "cluster_2" { labels = { mesh_id = "proj-${module.project.number}" } + enable_features = { + workload_identity = true + dataplane_v2 = true + } } module "cluster_2_nodepool" { - source = "./fabric/modules/gke-nodepool" - project_id = module.project.project_id - cluster_name = module.cluster_2.name - location = "europe-west4" - name = "nodepool" + source = "./fabric/modules/gke-nodepool" + project_id = module.project.project_id + cluster_name = module.cluster_2.name + cluster_id = module.cluster_2.id + location = "europe-west4" + name = "cluster-2-nodepool" node_count = { initial = 1 } service_account = { create = true } tags = ["cluster-2-node"] @@ -277,7 +295,8 @@ module "cluster_2_nodepool" { module "hub" { source = "./fabric/modules/gke-hub" project_id = module.project.project_id - clusters = { + depends_on = [google_project_iam_member.gkehub_fix] + clusters = { cluster-1 = module.cluster_1.id cluster-2 = module.cluster_2.id } @@ -295,7 +314,7 @@ module "hub" { ] } -# tftest modules=8 resources=28 +# tftest modules=8 resources=31 ``` @@ -307,7 +326,7 @@ module "hub" { | [clusters](variables.tf#L17) | Clusters members of this GKE Hub in name => id format. | map(string) | | {} | | [configmanagement_clusters](variables.tf#L24) | Config management features enabled on specific sets of member clusters, in config name => [cluster name] format. | map(list(string)) | | {} | | [configmanagement_templates](variables.tf#L31) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(object({…})) | | {} | -| [features](variables.tf#L66) | Enable and configue fleet features. | object({…}) | | {…} | +| [features](variables.tf#L66) | Enable and configue fleet features. | object({…}) | | {…} | | [workload_identity_clusters](variables.tf#L92) | Clusters that will use Fleet Workload Identity. | list(string) | | [] | ## Outputs diff --git a/modules/gke-hub/main.tf b/modules/gke-hub/main.tf index f433d3227..ddd35a462 100644 --- a/modules/gke-hub/main.tf +++ b/modules/gke-hub/main.tf @@ -70,6 +70,20 @@ resource "google_gke_hub_feature" "default" { } } +resource "google_gke_hub_feature_membership" "servicemesh" { + provider = google-beta + for_each = var.features.servicemesh ? var.clusters : {} + project = var.project_id + location = "global" + feature = google_gke_hub_feature.default["servicemesh"].name + membership = google_gke_hub_membership.default[each.key].membership_id + + mesh { + management = "MANAGEMENT_AUTOMATIC" + control_plane = "AUTOMATIC" + } +} + resource "google_gke_hub_feature_membership" "default" { provider = google-beta for_each = local.cluster_cm_config diff --git a/modules/gke-hub/variables.tf b/modules/gke-hub/variables.tf index c7133c07f..25e3d21d5 100644 --- a/modules/gke-hub/variables.tf +++ b/modules/gke-hub/variables.tf @@ -66,12 +66,12 @@ variable "configmanagement_templates" { variable "features" { description = "Enable and configue fleet features." type = object({ - appdevexperience = bool - configmanagement = bool - identityservice = bool - multiclusteringress = string - multiclusterservicediscovery = bool - servicemesh = bool + appdevexperience = optional(bool, false) + configmanagement = optional(bool, false) + identityservice = optional(bool, false) + multiclusteringress = optional(string, null) + multiclusterservicediscovery = optional(bool, false) + servicemesh = optional(bool, false) }) default = { appdevexperience = false diff --git a/modules/gke-hub/versions.tf b/modules/gke-hub/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/gke-hub/versions.tf +++ b/modules/gke-hub/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/gke-nodepool/README.md b/modules/gke-nodepool/README.md index 4c471c606..2f632c9c7 100644 --- a/modules/gke-nodepool/README.md +++ b/modules/gke-nodepool/README.md @@ -10,13 +10,13 @@ If no specific node configuration is set via variables, the module uses the prov ```hcl module "cluster-1-nodepool-1" { - source = "./fabric/modules/gke-nodepool" - project_id = "myproject" - cluster_name = "cluster-1" - location = "europe-west1-b" - name = "nodepool-1" + source = "./fabric/modules/gke-nodepool" + project_id = "myproject" + cluster_name = "cluster-1" + location = "europe-west1-b" + name = "nodepool-1" } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 inventory=basic.yaml ``` ### Internally managed service account @@ -27,35 +27,25 @@ If you create a new service account, its resource and email (in both plain and I #### GCE default service account -To use the GCE default service account, you can ignore the variable which is equivalent to `{ create = null, email = null }`. - -```hcl -module "cluster-1-nodepool-1" { - source = "./fabric/modules/gke-nodepool" - project_id = "myproject" - cluster_name = "cluster-1" - location = "europe-west1-b" - name = "nodepool-1" -} -# tftest modules=1 resources=1 -``` +To use the GCE default service account, you can ignore the variable which is equivalent to `{ create = null, email = null }`. This is what the first example of this document does. #### Externally defined service account -To use an existing service account, pass in just the `email` attribute. +To use an existing service account, pass in just the `email` attribute. If you do this, will most likely want to use the `cloud-platform` scope. ```hcl module "cluster-1-nodepool-1" { - source = "./fabric/modules/gke-nodepool" - project_id = "myproject" - cluster_name = "cluster-1" - location = "europe-west1-b" - name = "nodepool-1" + source = "./fabric/modules/gke-nodepool" + project_id = "myproject" + cluster_name = "cluster-1" + location = "europe-west1-b" + name = "nodepool-1" service_account = { - email = "foo-bar@myproject.iam.gserviceaccount.com" + email = "foo-bar@myproject.iam.gserviceaccount.com" + oauth_scopes = ["https://www.googleapis.com/auth/cloud-platform"] } } -# tftest modules=1 resources=1 +# tftest modules=1 resources=1 inventory=external-sa.yaml ``` #### Auto-created service account @@ -64,18 +54,54 @@ To have the module create a service account, set the `create` attribute to `true ```hcl module "cluster-1-nodepool-1" { - source = "./fabric/modules/gke-nodepool" - project_id = "myproject" - cluster_name = "cluster-1" - location = "europe-west1-b" - name = "nodepool-1" + source = "./fabric/modules/gke-nodepool" + project_id = "myproject" + cluster_name = "cluster-1" + location = "europe-west1-b" + name = "nodepool-1" service_account = { - create = true - # optional - email = "spam-eggs" + create = true + email = "spam-eggs" # optional + oauth_scopes = ["https://www.googleapis.com/auth/cloud-platform"] } } -# tftest modules=1 resources=2 +# tftest modules=1 resources=2 inventory=create-sa.yaml +``` +### Node & node pool configuration + +```hcl +module "cluster-1-nodepool-1" { + source = "./fabric/modules/gke-nodepool" + project_id = "myproject" + cluster_name = "cluster-1" + location = "europe-west1-b" + name = "nodepool-1" + labels = { environment = "dev" } + service_account = { + create = true + email = "nodepool-1" # optional + oauth_scopes = ["https://www.googleapis.com/auth/cloud-platform"] + } + node_config = { + machine_type = "n2-standard-2" + disk_size_gb = 50 + disk_type = "pd-ssd" + ephemeral_ssd_count = 1 + gvnic = true + spot = true + } + nodepool_config = { + autoscaling = { + max_node_count = 10 + min_node_count = 1 + } + management = { + auto_repair = true + auto_upgrade = false + } + } +} +# tftest modules=1 resources=2 inventory=config.yaml ``` @@ -83,23 +109,24 @@ module "cluster-1-nodepool-1" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [cluster_name](variables.tf#L17) | Cluster name. | string | ✓ | | -| [location](variables.tf#L35) | Cluster location. | string | ✓ | | -| [project_id](variables.tf#L143) | Cluster project id. | string | ✓ | | -| [gke_version](variables.tf#L22) | Kubernetes nodes version. Ignored if auto_upgrade is set in management_config. | string | | null | -| [labels](variables.tf#L28) | Kubernetes labels applied to each node. | map(string) | | {} | -| [max_pods_per_node](variables.tf#L40) | Maximum number of pods per node. | number | | null | -| [name](variables.tf#L46) | Optional nodepool name. | string | | null | -| [node_config](variables.tf#L52) | Node-level configuration. | object({…}) | | {…} | -| [node_count](variables.tf#L91) | Number of nodes per instance group. Initial value can only be changed by recreation, current is ignored when autoscaling is used. | object({…}) | | {…} | -| [node_locations](variables.tf#L103) | Node locations. | list(string) | | null | -| [nodepool_config](variables.tf#L109) | Nodepool-level configuration. | object({…}) | | null | -| [pod_range](variables.tf#L131) | Pod secondary range configuration. | object({…}) | | null | -| [reservation_affinity](variables.tf#L148) | Configuration of the desired reservation which instances could take capacity from. | object({…}) | | null | -| [service_account](variables.tf#L158) | Nodepool service account. If this variable is set to null, the default GCE service account will be used. If set and email is null, a service account will be created. If scopes are null a default will be used. | object({…}) | | {} | -| [sole_tenant_nodegroup](variables.tf#L169) | Sole tenant node group. | string | | null | -| [tags](variables.tf#L175) | Network tags applied to nodes. | list(string) | | null | -| [taints](variables.tf#L181) | Kubernetes taints applied to all nodes. | list(object({…})) | | null | +| [cluster_name](variables.tf#L23) | Cluster name. | string | ✓ | | +| [location](variables.tf#L41) | Cluster location. | string | ✓ | | +| [project_id](variables.tf#L149) | Cluster project id. | string | ✓ | | +| [cluster_id](variables.tf#L17) | Cluster id. Optional, but providing cluster_id is recommended to prevent cluster misconfiguration in some of the edge cases. | string | | null | +| [gke_version](variables.tf#L28) | Kubernetes nodes version. Ignored if auto_upgrade is set in management_config. | string | | null | +| [labels](variables.tf#L34) | Kubernetes labels applied to each node. | map(string) | | {} | +| [max_pods_per_node](variables.tf#L46) | Maximum number of pods per node. | number | | null | +| [name](variables.tf#L52) | Optional nodepool name. | string | | null | +| [node_config](variables.tf#L58) | Node-level configuration. | object({…}) | | {…} | +| [node_count](variables.tf#L97) | Number of nodes per instance group. Initial value can only be changed by recreation, current is ignored when autoscaling is used. | object({…}) | | {…} | +| [node_locations](variables.tf#L109) | Node locations. | list(string) | | null | +| [nodepool_config](variables.tf#L115) | Nodepool-level configuration. | object({…}) | | null | +| [pod_range](variables.tf#L137) | Pod secondary range configuration. | object({…}) | | null | +| [reservation_affinity](variables.tf#L154) | Configuration of the desired reservation which instances could take capacity from. | object({…}) | | null | +| [service_account](variables.tf#L164) | Nodepool service account. If this variable is set to null, the default GCE service account will be used. If set and email is null, a service account will be created. If scopes are null a default will be used. | object({…}) | | {} | +| [sole_tenant_nodegroup](variables.tf#L175) | Sole tenant node group. | string | | null | +| [tags](variables.tf#L181) | Network tags applied to nodes. | list(string) | | null | +| [taints](variables.tf#L187) | Kubernetes taints applied to all nodes. | list(object({…})) | | null | ## Outputs diff --git a/modules/gke-nodepool/main.tf b/modules/gke-nodepool/main.tf index 0c35c8d0f..9ae4cf284 100644 --- a/modules/gke-nodepool/main.tf +++ b/modules/gke-nodepool/main.tf @@ -70,7 +70,7 @@ resource "google_service_account" "service_account" { resource "google_container_node_pool" "nodepool" { provider = google-beta project = var.project_id - cluster = var.cluster_name + cluster = coalesce(var.cluster_id, var.cluster_name) location = var.location name = var.name version = var.gke_version @@ -115,9 +115,9 @@ resource "google_container_node_pool" "nodepool" { dynamic "network_config" { for_each = var.pod_range != null ? [""] : [] content { - create_pod_range = var.pod_range.create - pod_ipv4_cidr_block = var.pod_range.cidr - pod_range = var.pod_range.name + create_pod_range = var.pod_range.secondary_pod_range.create + pod_ipv4_cidr_block = var.pod_range.secondary_pod_range.cidr + pod_range = var.pod_range.secondary_pod_range.name } } diff --git a/modules/gke-nodepool/variables.tf b/modules/gke-nodepool/variables.tf index 15c8a1515..1166c34f4 100644 --- a/modules/gke-nodepool/variables.tf +++ b/modules/gke-nodepool/variables.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,12 @@ * limitations under the License. */ +variable "cluster_id" { + description = "Cluster id. Optional, but providing cluster_id is recommended to prevent cluster misconfiguration in some of the edge cases." + type = string + default = null +} + variable "cluster_name" { description = "Cluster name." type = string @@ -159,8 +165,8 @@ variable "service_account" { description = "Nodepool service account. If this variable is set to null, the default GCE service account will be used. If set and email is null, a service account will be created. If scopes are null a default will be used." type = object({ create = optional(bool, false) - email = optional(string, null) - oauth_scopes = optional(list(string), null) + email = optional(string) + oauth_scopes = optional(list(string)) }) default = {} nullable = false diff --git a/modules/gke-nodepool/versions.tf b/modules/gke-nodepool/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/gke-nodepool/versions.tf +++ b/modules/gke-nodepool/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/iam-service-account/README.md b/modules/iam-service-account/README.md index 2c6faee52..a62ce8f53 100644 --- a/modules/iam-service-account/README.md +++ b/modules/iam-service-account/README.md @@ -8,12 +8,11 @@ Note that this module does not fully comply with our design principles, as outpu ```hcl module "myproject-default-service-accounts" { - source = "./fabric/modules/iam-service-account" - project_id = "myproject" - name = "vm-default" - generate_key = true + source = "./fabric/modules/iam-service-account" + project_id = "myproject" + name = "vm-default" # authoritative roles granted *on* the service accounts to other identities - iam = { + iam = { "roles/iam.serviceAccountUser" = ["user:foo@example.com"] } # non-authoritative roles granted *to* the service accounts on other resources @@ -24,7 +23,7 @@ module "myproject-default-service-accounts" { ] } } -# tftest modules=1 resources=5 +# tftest modules=1 resources=4 inventory=basic.yaml ``` diff --git a/modules/iam-service-account/versions.tf b/modules/iam-service-account/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/iam-service-account/versions.tf +++ b/modules/iam-service-account/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/kms/README.md b/modules/kms/README.md index 4fe748578..3398a595d 100644 --- a/modules/kms/README.md +++ b/modules/kms/README.md @@ -14,9 +14,9 @@ In this module **no lifecycle blocks are set on resources to prevent destroy**, ```hcl module "kms" { - source = "./fabric/modules/kms" - project_id = "my-project" - iam = { + source = "./fabric/modules/kms" + project_id = "my-project" + iam = { "roles/cloudkms.admin" = ["user:user1@example.com"] } keyring = { location = "europe-west1", name = "test" } @@ -63,8 +63,8 @@ module "kms" { ```hcl module "kms" { - source = "./fabric/modules/kms" - project_id = "my-project" + source = "./fabric/modules/kms" + project_id = "my-project" key_purpose = { key-c = { purpose = "ASYMMETRIC_SIGN" @@ -74,8 +74,8 @@ module "kms" { } } } - keyring = { location = "europe-west1", name = "test" } - keys = { key-a = null, key-b = null, key-c = null } + keyring = { location = "europe-west1", name = "test" } + keys = { key-a = null, key-b = null, key-c = null } } # tftest modules=1 resources=4 ``` diff --git a/modules/kms/versions.tf b/modules/kms/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/kms/versions.tf +++ b/modules/kms/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/logging-bucket/versions.tf b/modules/logging-bucket/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/logging-bucket/versions.tf +++ b/modules/logging-bucket/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/net-address/README.md b/modules/net-address/README.md index e51383489..23e947cd4 100644 --- a/modules/net-address/README.md +++ b/modules/net-address/README.md @@ -27,12 +27,12 @@ module "addresses" { project_id = var.project_id internal_addresses = { ilb-1 = { - purpose = "SHARED_LOADBALANCER_VIP" + purpose = "SHARED_LOADBALANCER_VIP" region = var.region subnetwork = var.subnet.self_link } ilb-2 = { - address = "10.0.0.2" + address = "10.0.0.2" region = var.region subnetwork = var.subnet.self_link } @@ -66,11 +66,11 @@ module "addresses" { project_id = var.project_id psc_addresses = { one = { - address = null + address = null network = var.vpc.self_link } two = { - address = "10.0.0.32" + address = "10.0.0.32" network = var.vpc.self_link } } diff --git a/modules/net-address/versions.tf b/modules/net-address/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/net-address/versions.tf +++ b/modules/net-address/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/net-cloudnat/versions.tf b/modules/net-cloudnat/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/net-cloudnat/versions.tf +++ b/modules/net-cloudnat/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/net-glb/README.md b/modules/net-glb/README.md index 4fc97b076..8cd3f353f 100644 --- a/modules/net-glb/README.md +++ b/modules/net-glb/README.md @@ -117,7 +117,7 @@ The module uses a classic Global Load Balancer by default. To use the non-classi ```hcl module "glb-0" { - source = "./fabric/modules/net-glb" + source = "./fabric/modules/net-glb" project_id = "myprj" name = "glb-test-0" use_classic_version = false @@ -214,6 +214,66 @@ module "glb-0" { } # tftest modules=1 resources=6 ``` +#### Managed Instance Groups + +This example shows how to use the module with a manage instance group as backend: + +```hcl +module "win-template" { + source = "./fabric/modules/compute-vm" + project_id = "myprj" + zone = "europe-west8-a" + name = "win-template" + instance_type = "n2d-standard-2" + create_template = true + boot_disk = { + image = "projects/windows-cloud/global/images/windows-server-2019-dc-v20221214" + type = "pd-balanced" + size = 70 + } + network_interfaces = [{ + network = var.vpc.self_link + subnetwork = var.subnet.self_link + nat = false + addresses = null + }] +} + +module "win-mig" { + source = "./fabric/modules/compute-mig" + project_id = "myprj" + location = "europe-west8-a" + name = "win-mig" + instance_template = module.win-template.template.self_link + autoscaler_config = { + max_replicas = 3 + min_replicas = 1 + cooldown_period = 30 + scaling_signals = { + cpu_utilization = { + target = 0.80 + } + } + } + named_ports = { + http = 80 + } +} + +module "glb-0" { + source = "./fabric/modules/net-glb" + project_id = "myprj" + name = "glb-test-0" + backend_service_configs = { + default = { + backends = [ + { backend = module.win-mig.group_manager.instance_group } + ] + } + } +} +# tftest modules=3 resources=8 +``` #### Storage Buckets @@ -285,11 +345,13 @@ module "glb-0" { network = "projects/myprj-host/global/networks/svpc" subnetwork = "projects/myprj-host/regions/europe-west8/subnetworks/gce" zone = "europe-west8-b" - endpoints = [{ - instance = "myinstance-b-0" - ip_address = "10.24.32.25" - port = 80 - }] + endpoints = { + e-0 = { + instance = "myinstance-b-0" + ip_address = "10.24.32.25" + port = 80 + } + } } } } @@ -320,12 +382,14 @@ module "glb-0" { neg_configs = { neg-0 = { hybrid = { - network = "projects/myprj-host/global/networks/svpc" - zone = "europe-west8-b" - endpoints = [{ - ip_address = "10.0.0.10" - port = 80 - }] + network = "projects/myprj-host/global/networks/svpc" + zone = "europe-west8-b" + endpoints = { + e-0 = { + ip_address = "10.0.0.10" + port = 80 + } + } } } } @@ -355,11 +419,13 @@ module "glb-0" { neg_configs = { neg-0 = { internet = { - use_fqdn = true - endpoints = [{ - destination = "www.example.org" - port = 80 - }] + use_fqdn = true + endpoints = { + e-0 = { + destination = "www.example.org" + port = 80 + } + } } } } @@ -373,7 +439,7 @@ The module supports managing PSC NEGs if the non-classic version of the load bal ```hcl module "glb-0" { - source = "./fabric/modules/net-glb" + source = "./fabric/modules/net-glb" project_id = "myprj" name = "glb-test-0" use_classic_version = false @@ -390,7 +456,7 @@ module "glb-0" { neg_configs = { neg-0 = { psc = { - region = "europe-west8" + region = "europe-west8" target_service = "europe-west8-cloudkms.googleapis.com" } } @@ -432,6 +498,46 @@ module "glb-0" { # tftest modules=1 resources=5 ``` +Serverless NEGs don't use the port name but it should be set to `http`. An HTTPS frontend requires the protocol to be set to `HTTPS`, and the port name field will infer this value if omitted so you need to set it explicitly: + +```hcl +module "glb-0" { + source = "./fabric/modules/net-glb" + project_id = "myprj" + name = "glb-test-0" + backend_service_configs = { + default = { + backends = [ + { backend = "neg-0" } + ] + health_checks = [] + port_name = "http" + } + } + # with a single serverless NEG the implied default health check is not needed + health_check_configs = {} + neg_configs = { + neg-0 = { + cloudrun = { + region = "europe-west8" + target_service = { + name = "hello" + } + } + } + } + protocol = "HTTPS" + ssl_certificates = { + managed_configs = { + default = { + domains = ["glb-test-0.example.org"] + } + } + } +} +# tftest modules=1 resources=6 inventory=https-sneg.yaml +``` + ### URL Map The module exposes the full URL map resource configuration, with some minor changes to the interface to decrease verbosity, and support for aliasing backend services via keys. @@ -465,7 +571,7 @@ module "glb-0" { pathmap = { default_service = "default" path_rules = [{ - paths = ["/other", "/other/*"] + paths = ["/other", "/other/*"] service = "other" }] } @@ -483,7 +589,6 @@ The module also allows managing managed and self-managed SSL certificates via th THe [HTTPS example above](#minimal-https-examples) shows how to configure manage certificated, the following example shows how to use an unmanaged (or self managed) certificate. The example uses Terraform resource for the key and certificate so that the we don't depend on external files when running tests, in real use the key and certificate are generally provided via external files read by the Terraform `file()` function. ```hcl - resource "tls_private_key" "default" { algorithm = "RSA" rsa_bits = 4096 @@ -554,16 +659,16 @@ module "glb-0" { neg-gce-0 = { backends = [{ balancing_mode = "RATE" - backend = "neg-ew8-c" + backend = "neg-ew8-c" max_rate = { per_endpoint = 10 } }] } neg-hybrid-0 = { backends = [{ - backend = "neg-hello" + backend = "neg-hello" }] - health_checks = ["neg"] - protocol = "HTTPS" + health_checks = ["neg"] + protocol = "HTTPS" } } group_configs = { @@ -600,22 +705,26 @@ module "glb-0" { gce = { network = "projects/myprj-host/global/networks/svpc" subnetwork = "projects/myprj-host/regions/europe-west8/subnetworks/gce" - zone = "europe-west8-c" - endpoints = [{ - instance = "nginx-ew8-c" - ip_address = "10.24.32.26" - port = 80 - }] + zone = "europe-west8-c" + endpoints = { + e-0 = { + instance = "nginx-ew8-c" + ip_address = "10.24.32.26" + port = 80 + } + } } } neg-hello = { hybrid = { - network = "projects/myprj-host/global/networks/svpc" - zone = "europe-west8-b" - endpoints = [{ - ip_address = "192.168.0.3" - port = 443 - }] + network = "projects/myprj-host/global/networks/svpc" + zone = "europe-west8-b" + endpoints = { + e-0 = { + ip_address = "192.168.0.3" + port = 443 + } + } } } } @@ -691,7 +800,7 @@ module "glb-0" { | [health_check_configs](variables-health-check.tf#L19) | Optional auto-created health check configurations, use the output self-link to set it in the auto healing policy. Refer to examples for usage. | map(object({…})) | | {…} | | [https_proxy_config](variables.tf#L74) | HTTPS proxy connfiguration. | object({…}) | | {} | | [labels](variables.tf#L85) | Labels set on resources. | map(string) | | {} | -| [neg_configs](variables.tf#L96) | Optional network endpoint groups to create. Can be referenced in backends via key or outputs. | map(object({…})) | | {} | +| [neg_configs](variables.tf#L96) | Optional network endpoint groups to create. Can be referenced in backends via key or outputs. | map(object({…})) | | {} | | [ports](variables.tf#L187) | Optional ports for HTTP load balancer, valid ports are 80 and 8080. | list(string) | | null | | [protocol](variables.tf#L198) | Protocol supported by this load balancer. | string | | "HTTP" | | [ssl_certificates](variables.tf#L211) | SSL target proxy certificates (only if protocol is HTTPS) for existing, custom, and managed certificates. | object({…}) | | {} | diff --git a/modules/net-glb/backend-service.tf b/modules/net-glb/backend-service.tf index f8956e138..acadda3bd 100644 --- a/modules/net-glb/backend-service.tf +++ b/modules/net-glb/backend-service.tf @@ -60,7 +60,7 @@ resource "google_compute_backend_service" "default" { health_checks = length(each.value.health_checks) == 0 ? null : [ for k in each.value.health_checks : lookup(local.hc_ids, k, k) ] - load_balancing_scheme = "EXTERNAL" + load_balancing_scheme = var.use_classic_version ? "EXTERNAL" : "EXTERNAL_MANAGED" port_name = ( each.value.port_name == null ? lower(each.value.protocol == null ? var.protocol : each.value.protocol) diff --git a/modules/net-glb/negs.tf b/modules/net-glb/negs.tf index 9edae1cd8..0011968d5 100644 --- a/modules/net-glb/negs.tf +++ b/modules/net-glb/negs.tf @@ -19,23 +19,23 @@ locals { _neg_endpoints_global = flatten([ for k, v in local.neg_global : [ - for vv in v.internet.endpoints : - merge(vv, { neg = k, use_fqdn = v.internet.use_fqdn }) + for kk, vv in v.internet.endpoints : merge(vv, { + key = "${k}-${kk}", neg = k, use_fqdn = v.internet.use_fqdn + }) ] ]) _neg_endpoints_zonal = flatten([ for k, v in local.neg_zonal : [ - for vv in v.endpoints : - merge(vv, { neg = k, zone = v.zone }) + for kk, vv in v.endpoints : merge(vv, { + key = "${k}-${kk}", neg = k, zone = v.zone + }) ] ]) neg_endpoints_global = { - for v in local._neg_endpoints_global : - "${v.neg}-${v.destination}-${coalesce(v.port, "none")}" => v + for v in local._neg_endpoints_global : (v.key) => v } neg_endpoints_zonal = { - for v in local._neg_endpoints_zonal : - "${v.neg}-${v.ip_address}-${coalesce(v.port, "none")}" => v + for v in local._neg_endpoints_zonal : (v.key) => v } neg_global = { for k, v in var.neg_configs : diff --git a/modules/net-glb/variables.tf b/modules/net-glb/variables.tf index 523b8f5f4..72e6c0c40 100644 --- a/modules/net-glb/variables.tf +++ b/modules/net-glb/variables.tf @@ -115,7 +115,7 @@ variable "neg_configs" { subnetwork = string zone = string # default_port = optional(number) - endpoints = optional(list(object({ + endpoints = optional(map(object({ instance = string ip_address = string port = number @@ -126,7 +126,7 @@ variable "neg_configs" { zone = string # re-enable once provider properly support this # default_port = optional(number) - endpoints = optional(list(object({ + endpoints = optional(map(object({ ip_address = string port = number }))) @@ -135,7 +135,7 @@ variable "neg_configs" { use_fqdn = optional(bool, true) # re-enable once provider properly support this # default_port = optional(number) - endpoints = optional(list(object({ + endpoints = optional(map(object({ destination = string port = number }))) diff --git a/modules/net-glb/versions.tf b/modules/net-glb/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/net-glb/versions.tf +++ b/modules/net-glb/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/net-ilb-l7/README.md b/modules/net-ilb-l7/README.md index 969a4da4f..b5862f31e 100644 --- a/modules/net-ilb-l7/README.md +++ b/modules/net-ilb-l7/README.md @@ -176,7 +176,7 @@ module "ilb-l7" { backend_service_configs = { default = { port_name = "http" - backends = [ + backends = [ { group = "default" } ] } @@ -228,6 +228,14 @@ module "ilb-l7" { Similarly to instance groups, NEGs can also be managed by this module which supports GCE, hybrid, and serverless NEGs: ```hcl +resource "google_compute_address" "test" { + name = "neg-test" + subnetwork = var.subnet.self_link + address_type = "INTERNAL" + address = "10.0.0.10" + region = "europe-west1" +} + module "ilb-l7" { source = "./fabric/modules/net-ilb-l7" name = "ilb-test" @@ -237,7 +245,7 @@ module "ilb-l7" { default = { backends = [{ balancing_mode = "RATE" - group = "my-neg" + group = "my-neg" max_rate = { per_endpoint = 1 } }] } @@ -245,12 +253,15 @@ module "ilb-l7" { neg_configs = { my-neg = { gce = { - zone = "europe-west1-b" - endpoints = [{ - instance = "test-1" - ip_address = "10.0.0.10" - port = 80 - }] + zone = "europe-west1-b" + endpoints = { + e-0 = { + instance = "test-1" + ip_address = google_compute_address.test.address + # ip_address = "10.0.0.10" + port = 80 + } + } } } } @@ -259,7 +270,7 @@ module "ilb-l7" { subnetwork = var.subnet.self_link } } -# tftest modules=1 resources=7 +# tftest modules=1 resources=8 ``` Hybrid NEGs are also supported: @@ -274,7 +285,7 @@ module "ilb-l7" { default = { backends = [{ balancing_mode = "RATE" - group = "my-neg" + group = "my-neg" max_rate = { per_endpoint = 1 } }] } @@ -282,11 +293,13 @@ module "ilb-l7" { neg_configs = { my-neg = { hybrid = { - zone = "europe-west1-b" - endpoints = [{ - ip_address = "10.0.0.10" - port = 80 - }] + zone = "europe-west1-b" + endpoints = { + e-0 = { + ip_address = "10.0.0.10" + port = 80 + } + } } } } @@ -310,7 +323,7 @@ module "ilb-l7" { default = { backends = [{ balancing_mode = "RATE" - group = "my-neg" + group = "my-neg" max_rate = { per_endpoint = 1 } }] } @@ -367,7 +380,7 @@ module "ilb-l7" { pathmap = { default_service = "default" path_rules = [{ - paths = ["/video", "/video/*"] + paths = ["/video", "/video/*"] service = "video" }] } @@ -512,20 +525,24 @@ module "ilb-l7" { neg-nginx-ew8-c = { gce = { zone = "europe-west8-c" - endpoints = [{ - instance = "nginx-ew8-c" - ip_address = "10.24.32.26" - port = 80 - }] + endpoints = { + e-0 = { + instance = "nginx-ew8-c" + ip_address = "10.24.32.26" + port = 80 + } + } } } neg-home-hello = { hybrid = { - zone = "europe-west8-b" - endpoints = [{ - ip_address = "192.168.0.3" - port = 443 - }] + zone = "europe-west8-b" + endpoints = { + e-0 = { + ip_address = "192.168.0.3" + port = 443 + } + } } } } @@ -597,7 +614,7 @@ module "ilb-l7" { | [group_configs](variables.tf#L36) | Optional unmanaged groups to create. Can be referenced in backends via key or outputs. | map(object({…})) | | {} | | [health_check_configs](variables-health-check.tf#L19) | Optional auto-created health check configurations, use the output self-link to set it in the auto healing policy. Refer to examples for usage. | map(object({…})) | | {…} | | [labels](variables.tf#L48) | Labels set on resources. | map(string) | | {} | -| [neg_configs](variables.tf#L59) | Optional network endpoint groups to create. Can be referenced in backends via key or outputs. | map(object({…})) | | {} | +| [neg_configs](variables.tf#L59) | Optional network endpoint groups to create. Can be referenced in backends via key or outputs. | map(object({…})) | | {} | | [network_tier_premium](variables.tf#L119) | Use premium network tier. Defaults to true. | bool | | true | | [ports](variables.tf#L126) | Optional ports for HTTP load balancer, valid ports are 80 and 8080. | list(string) | | null | | [protocol](variables.tf#L137) | Protocol supported by this load balancer. | string | | "HTTP" | diff --git a/modules/net-ilb-l7/backend-service.tf b/modules/net-ilb-l7/backend-service.tf index e2d92299f..a517bd08c 100644 --- a/modules/net-ilb-l7/backend-service.tf +++ b/modules/net-ilb-l7/backend-service.tf @@ -23,6 +23,9 @@ locals { }, { for k, v in google_compute_network_endpoint_group.default : k => v.id + }, + { + for k, v in google_compute_region_network_endpoint_group.default : k => v.id } ) hc_ids = { diff --git a/modules/net-ilb-l7/main.tf b/modules/net-ilb-l7/main.tf index 5b6211a39..803b3ff5c 100644 --- a/modules/net-ilb-l7/main.tf +++ b/modules/net-ilb-l7/main.tf @@ -15,9 +15,12 @@ */ locals { + # we need keys in the endpoint type to address issue #1055 _neg_endpoints = flatten([ for k, v in local.neg_zonal : [ - for vv in v.endpoints : merge(vv, { neg = k, zone = v.zone }) + for kk, vv in v.endpoints : merge(vv, { + key = "${k}-${kk}", neg = k, zone = v.zone + }) ] ]) fwd_rule_ports = ( @@ -29,8 +32,7 @@ locals { : google_compute_region_target_http_proxy.default.0.id ) neg_endpoints = { - for v in local._neg_endpoints : - "${v.neg}-${v.ip_address}-${coalesce(v.port, "none")}" => v + for v in local._neg_endpoints : (v.key) => v } neg_regional = { for k, v in var.neg_configs : diff --git a/modules/net-ilb-l7/variables.tf b/modules/net-ilb-l7/variables.tf index 0577ddf6e..09b3f7ac7 100644 --- a/modules/net-ilb-l7/variables.tf +++ b/modules/net-ilb-l7/variables.tf @@ -73,7 +73,7 @@ variable "neg_configs" { # default_port = optional(number) network = optional(string) subnetwork = optional(string) - endpoints = optional(list(object({ + endpoints = optional(map(object({ instance = string ip_address = string port = number @@ -85,7 +85,7 @@ variable "neg_configs" { network = optional(string) # re-enable once provider properly support this # default_port = optional(number) - endpoints = optional(list(object({ + endpoints = optional(map(object({ ip_address = string port = number }))) diff --git a/modules/net-ilb-l7/versions.tf b/modules/net-ilb-l7/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/net-ilb-l7/versions.tf +++ b/modules/net-ilb-l7/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/net-ilb/README.md b/modules/net-ilb/README.md index bf2b507bd..48c1d9081 100644 --- a/modules/net-ilb/README.md +++ b/modules/net-ilb/README.md @@ -12,6 +12,62 @@ One other issue is a `Provider produced inconsistent final plan` error which is ## Examples +### Reference existing MIGs + +This example shows how to reference existing Managed Infrastructure Groups (MIGs). + +```hcl +module "instance_template" { + source = "./fabric/modules/compute-vm" + project_id = var.project_id + create_template = true + name = "vm-test" + service_account_create = true + zone = "europe-west1-b" + + network_interfaces = [ + { + network = var.vpc.self_link + subnetwork = var.subnet.self_link + } + ] + + tags = [ + "http-server" + ] +} + +module "mig" { + source = "./fabric/modules/compute-mig" + project_id = var.project_id + location = "europe-west1" + name = "mig-test" + target_size = 1 + instance_template = module.instance_template.template.self_link +} + +module "ilb" { + source = "./fabric/modules/net-ilb" + project_id = var.project_id + region = "europe-west1" + name = "ilb-test" + service_label = "ilb-test" + vpc_config = { + network = var.vpc.self_link + subnetwork = var.subnet.self_link + } + backends = [{ + group = module.mig.group_manager.instance_group + }] + health_check_config = { + http = { + port = 80 + } + } +} +# tftest modules=3 resources=6 +``` + ### Externally managed instances This examples shows how to create an ILB by combining externally managed instances (in a custom module or even outside of the current root module) in an unmanaged group. When using internally managed groups, remember to run `terraform apply` each time group instances change. @@ -37,7 +93,7 @@ module "ilb" { } } backends = [{ - group = module.ilb.groups.my-group.self_link + group = module.ilb.groups.my-group.self_link }] health_check_config = { http = { @@ -96,7 +152,7 @@ module "ilb" { vpc_config = { network = var.vpc.self_link subnetwork = var.subnet.self_link - } + } ports = [80] backends = [ for z, mod in module.instance-group : { diff --git a/modules/net-ilb/versions.tf b/modules/net-ilb/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/net-ilb/versions.tf +++ b/modules/net-ilb/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/net-interconnect-attachment-direct/versions.tf b/modules/net-interconnect-attachment-direct/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/net-interconnect-attachment-direct/versions.tf +++ b/modules/net-interconnect-attachment-direct/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/net-vpc-firewall/README.md b/modules/net-vpc-firewall/README.md index 58c116561..f886035ba 100644 --- a/modules/net-vpc-firewall/README.md +++ b/modules/net-vpc-firewall/README.md @@ -33,7 +33,7 @@ Some implicit defaults are used in the rules variable types and can be controlle - action is controlled via the `deny` attribute which defaults to `true` for egress and `false` for ingress - priority defaults to `1000` -- destination ranges (for egress) and source ranges (for ingress) default to `["0.0.0.0/0"]` if not explicitly set +- destination ranges (for egress) and source ranges (for ingress) default to `["0.0.0.0/0"]` if not explicitly set or set to `null`, to disable the behaviour set ranges to the empty list (`[]`) - rules default to all protocols if not set ```hcl @@ -44,32 +44,40 @@ module "firewall" { default_rules_config = { admin_ranges = ["10.0.0.0/8"] } - egress_rules = { - # implicit `deny` action + egress_rules = { + # implicit deny action allow-egress-rfc1918 = { + deny = false description = "Allow egress to RFC 1918 ranges." - destination_ranges = [ + destination_ranges = [ "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" ] - # implicit { protocol = "all" } rule + } + allow-egress-tag = { + deny = false + description = "Allow egress from a specific tag to 0/0." + targets = ["target-tag"] } deny-egress-all = { description = "Block egress." - # implicit ["0.0.0.0/0"] destination ranges - # implicit { protocol = "all" } rule } } ingress_rules = { - # implicit `allow` action + # implicit allow action allow-ingress-ntp = { - description = "Allow NTP service based on tag." - source_ranges = ["0.0.0.0/0"] - targets = ["ntp-svc"] - rules = [{ protocol = "udp", ports = [123] }] + description = "Allow NTP service based on tag." + targets = ["ntp-svc"] + rules = [{ protocol = "udp", ports = [123] }] + } + allow-ingress-tag = { + description = "Allow ingress from a specific tag." + source_ranges = [] + sources = ["client-tag"] + targets = ["target-tag"] } } } -# tftest modules=1 resources=7 +# tftest modules=1 resources=9 ``` ### Controlling or turning off default rules @@ -108,7 +116,7 @@ module "firewall" { project_id = "my-project" network = "my-network" default_rules_config = { - ssh_ranges = [] + ssh_ranges = [] } } # tftest modules=1 resources=2 @@ -134,34 +142,54 @@ The module includes a rules factory (see [Resource Factories](../../blueprints/f ```hcl module "firewall" { - source = "./fabric/modules/net-vpc-firewall" - project_id = "my-project" - network = "my-network" + source = "./fabric/modules/net-vpc-firewall" + project_id = "my-project" + network = "my-network" factories_config = { - rules_folder = "configs/firewal/rules" - cidr_tpl_file = "configs/firewal/cidr_template.yaml" + rules_folder = "configs/firewall/rules" + cidr_tpl_file = "configs/firewall/cidrs.yaml" } - + default_rules_config = { disabled = true } } -# tftest modules=1 resources=3 +# tftest modules=1 resources=3 files=lbs,cidrs ``` ```yaml -# tftest file configs/firewall/rules/load_balancers.yaml -allow-healthchecks: - description: Allow ingress from healthchecks. - ranges: - - healthchecks - targets: ["lb-backends"] - rules: - - protocol: tcp - ports: - - 80 - - 443 +# tftest-file id=lbs path=configs/firewall/rules/load_balancers.yaml +ingress: + allow-healthchecks: + description: Allow ingress from healthchecks. + source_ranges: + - healthchecks + targets: ["lb-backends"] + rules: + - protocol: tcp + ports: + - 80 + - 443 + allow-service-1-to-service-2: + description: Allow ingress from service-1 SA + targets: ["service-2"] + use_service_accounts: true + sources: + - service-1@my-project.iam.gserviceaccount.com + rules: + - protocol: tcp + ports: + - 80 + - 443 +egress: + block-telnet: + description: block outbound telnet + deny: true + rules: + - protocol: tcp + ports: + - 23 ``` ```yaml -# tftest file configs/firewall/cidr_template.yaml +# tftest-file id=cidrs path=configs/firewall/cidrs.yaml healthchecks: - 35.191.0.0/16 - 130.211.0.0/22 @@ -174,13 +202,13 @@ healthchecks: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [network](variables.tf#L109) | Name of the network this set of firewall rules applies to. | string | ✓ | | -| [project_id](variables.tf#L114) | Project id of the project that holds the network. | string | ✓ | | +| [network](variables.tf#L108) | Name of the network this set of firewall rules applies to. | string | ✓ | | +| [project_id](variables.tf#L113) | Project id of the project that holds the network. | string | ✓ | | | [default_rules_config](variables.tf#L17) | Optionally created convenience rules. Set the 'disabled' attribute to true, or individual rule attributes to empty lists to disable. | object({…}) | | {} | -| [egress_rules](variables.tf#L37) | List of egress rule definitions, default to deny action. | map(object({…})) | | {} | -| [factories_config](variables.tf#L60) | Paths to data files and folders that enable factory functionality. | object({…}) | | null | -| [ingress_rules](variables.tf#L69) | List of ingress rule definitions, default to allow action. | map(object({…})) | | {} | -| [named_ranges](variables.tf#L92) | Define mapping of names to ranges that can be used in custom rules. | map(list(string)) | | {…} | +| [egress_rules](variables.tf#L37) | List of egress rule definitions, default to deny action. Null destination ranges will be replaced with 0/0. | map(object({…})) | | {} | +| [factories_config](variables.tf#L59) | Paths to data files and folders that enable factory functionality. | object({…}) | | null | +| [ingress_rules](variables.tf#L68) | List of ingress rule definitions, default to allow action. Null source ranges will be replaced with 0/0. | map(object({…})) | | {} | +| [named_ranges](variables.tf#L91) | Define mapping of names to ranges that can be used in custom rules. | map(list(string)) | | {…} | ## Outputs diff --git a/modules/net-vpc-firewall/main.tf b/modules/net-vpc-firewall/main.tf index 708b8844b..e525ceb4b 100644 --- a/modules/net-vpc-firewall/main.tf +++ b/modules/net-vpc-firewall/main.tf @@ -66,15 +66,23 @@ locals { for name, rule in local._rules : name => merge(rule, { action = rule.deny == true ? "DENY" : "ALLOW" - destination_ranges = flatten([ - for range in coalesce(try(rule.destination_ranges, null), []) : - try(local._named_ranges[range], range) - ]) + destination_ranges = ( + try(rule.destination_ranges, null) == null + ? null + : flatten([ + for range in rule.destination_ranges : + try(local._named_ranges[range], range) + ]) + ) rules = { for k, v in rule.rules : k => v } - source_ranges = flatten([ - for range in coalesce(try(rule.source_ranges, null), []) : - try(local._named_ranges[range], range) - ]) + source_ranges = ( + try(rule.source_ranges, null) == null + ? null + : flatten([ + for range in rule.source_ranges : + try(local._named_ranges[range], range) + ]) + ) }) } } @@ -89,18 +97,20 @@ resource "google_compute_firewall" "custom-rules" { source_ranges = ( each.value.direction == "INGRESS" ? ( - coalesce(each.value.source_ranges, []) == [] + each.value.source_ranges == null ? ["0.0.0.0/0"] : each.value.source_ranges - ) : null + ) + : null ) destination_ranges = ( each.value.direction == "EGRESS" ? ( - coalesce(each.value.destination_ranges, []) == [] + each.value.destination_ranges == null ? ["0.0.0.0/0"] : each.value.destination_ranges - ) : null + ) + : null ) source_tags = ( each.value.use_service_accounts || each.value.direction == "EGRESS" diff --git a/modules/net-vpc-firewall/variables.tf b/modules/net-vpc-firewall/variables.tf index 3e458acd8..9f750cc83 100644 --- a/modules/net-vpc-firewall/variables.tf +++ b/modules/net-vpc-firewall/variables.tf @@ -35,7 +35,7 @@ variable "default_rules_config" { } variable "egress_rules" { - description = "List of egress rule definitions, default to deny action." + description = "List of egress rule definitions, default to deny action. Null destination ranges will be replaced with 0/0." type = map(object({ deny = optional(bool, true) description = optional(string) @@ -45,7 +45,6 @@ variable "egress_rules" { include_metadata = optional(bool) })) priority = optional(number, 1000) - sources = optional(list(string)) targets = optional(list(string)) use_service_accounts = optional(bool, false) rules = optional(list(object({ @@ -67,7 +66,7 @@ variable "factories_config" { } variable "ingress_rules" { - description = "List of ingress rule definitions, default to allow action." + description = "List of ingress rule definitions, default to allow action. Null source ranges will be replaced with 0/0." type = map(object({ deny = optional(bool, false) description = optional(string) diff --git a/modules/net-vpc-firewall/versions.tf b/modules/net-vpc-firewall/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/net-vpc-firewall/versions.tf +++ b/modules/net-vpc-firewall/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/net-vpc-peering/versions.tf b/modules/net-vpc-peering/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/net-vpc-peering/versions.tf +++ b/modules/net-vpc-peering/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/net-vpc/README.md b/modules/net-vpc/README.md index b59694062..dbd855022 100644 --- a/modules/net-vpc/README.md +++ b/modules/net-vpc/README.md @@ -30,7 +30,88 @@ module "vpc" { } ] } -# tftest modules=1 resources=3 +# tftest modules=1 resources=3 inventory=simple.yaml +``` + +### Subnet Options +```hcl +module "vpc" { + source = "./fabric/modules/net-vpc" + project_id = "my-project" + name = "my-network" + subnets = [ + # simple subnet + { + name = "simple" + region = "europe-west1" + ip_cidr_range = "10.0.0.0/24" + }, + # custom description and PGA disabled + { + name = "no-pga" + region = "europe-west1" + ip_cidr_range = "10.0.1.0/24", + description = "Subnet b" + enable_private_access = false + }, + # secondary ranges + { + name = "with-secondary-ranges" + region = "europe-west1" + ip_cidr_range = "10.0.2.0/24" + secondary_ip_ranges = { + a = "192.168.0.0/24" + b = "192.168.1.0/24" + } + }, + # enable flow logs + { + name = "with-flow-logs" + region = "europe-west1" + ip_cidr_range = "10.0.3.0/24" + flow_logs_config = { + flow_sampling = 0.5 + aggregation_interval = "INTERVAL_10_MIN" + } + } + ] +} +# tftest modules=1 resources=5 inventory=subnet-options.yaml +``` + +### Subnet IAM + +```hcl +module "vpc" { + source = "./fabric/modules/net-vpc" + project_id = "my-project" + name = "my-network" + subnets = [ + { + name = "subnet-1" + region = "europe-west1" + ip_cidr_range = "10.0.1.0/24" + }, + { + name = "subnet-2" + region = "europe-west1" + ip_cidr_range = "10.0.1.0/24" + } + ] + subnet_iam = { + "europe-west1/subnet-1" = { + "roles/compute.networkUser" = [ + "user:user1@example.com", "group:group1@example.com" + ] + } + "europe-west1/subnet-2" = { + "roles/compute.networkUser" = [ + "user:user2@example.com", "group:group2@example.com" + ] + } + } +} +# tftest modules=1 resources=5 inventory=subnet-iam.yaml ``` ### Peering @@ -45,9 +126,9 @@ module "vpc-hub" { project_id = "hub" name = "vpc-hub" subnets = [{ - ip_cidr_range = "10.0.0.0/24" - name = "subnet-1" - region = "europe-west1" + ip_cidr_range = "10.0.0.0/24" + name = "subnet-1" + region = "europe-west1" }] } @@ -56,16 +137,16 @@ module "vpc-spoke-1" { project_id = "spoke1" name = "vpc-spoke1" subnets = [{ - ip_cidr_range = "10.0.1.0/24" - name = "subnet-2" - region = "europe-west1" + ip_cidr_range = "10.0.1.0/24" + name = "subnet-2" + region = "europe-west1" }] peering_config = { peer_vpc_self_link = module.vpc-hub.self_link import_routes = true } } -# tftest modules=2 resources=6 +# tftest modules=2 resources=6 inventory=peering.yaml ``` ### Shared VPC @@ -75,9 +156,9 @@ module "vpc-spoke-1" { ```hcl locals { service_project_1 = { - project_id = "project1" - gke_service_account = "gke" - cloud_services_service_account = "cloudsvc" + project_id = "project1" + gke_service_account = "serviceAccount:gke" + cloud_services_service_account = "serviceAccount:cloudsvc" } service_project_2 = { project_id = "project2" @@ -116,7 +197,7 @@ module "vpc-host" { } } } -# tftest modules=1 resources=7 +# tftest modules=1 resources=7 inventory=shared-vpc.yaml ``` ### Private Service Networking @@ -128,16 +209,16 @@ module "vpc" { name = "my-network" subnets = [ { - ip_cidr_range = "10.0.0.0/24" - name = "production" - region = "europe-west1" + ip_cidr_range = "10.0.0.0/24" + name = "production" + region = "europe-west1" } ] psa_config = { ranges = { myrange = "10.0.1.0/24" } } } -# tftest modules=1 resources=5 +# tftest modules=1 resources=5 inventory=psc.yaml ``` ### Private Service Networking with peering routes @@ -151,18 +232,18 @@ module "vpc" { name = "my-network" subnets = [ { - ip_cidr_range = "10.0.0.0/24" - name = "production" - region = "europe-west1" + ip_cidr_range = "10.0.0.0/24" + name = "production" + region = "europe-west1" } ] psa_config = { - ranges = { myrange = "10.0.1.0/24" } + ranges = { myrange = "10.0.1.0/24" } export_routes = true import_routes = true } } -# tftest modules=1 resources=5 +# tftest modules=1 resources=5 inventory=psc-routes.yaml ``` ### Subnets for Private Service Connect, Proxy-only subnets @@ -194,7 +275,7 @@ module "vpc" { } ] } -# tftest modules=1 resources=3 +# tftest modules=1 resources=3 inventory=proxy-only-subnets.yaml ``` ### DNS Policies @@ -205,7 +286,7 @@ module "vpc" { project_id = "my-project" name = "my-network" dns_policy = { - inbound = true + inbound = true outbound = { private_ns = ["10.0.0.1"] public_ns = ["8.8.8.8"] @@ -213,13 +294,13 @@ module "vpc" { } subnets = [ { - ip_cidr_range = "10.0.0.0/24" - name = "production" - region = "europe-west1" + ip_cidr_range = "10.0.0.0/24" + name = "production" + region = "europe-west1" } ] } -# tftest modules=1 resources=3 +# tftest modules=1 resources=3 inventory=dns-policies.yaml ``` ### Subnet Factory @@ -233,11 +314,17 @@ module "vpc" { name = "my-network" data_folder = "config/subnets" } -# tftest modules=1 resources=1 file=subnets +# tftest modules=1 resources=3 files=subnet-simple,subnet-detailed inventory=factory.yaml ``` ```yaml -# tftest file subnets ./config/subnets/subnet-name.yaml +# tftest-file id=subnet-simple path=config/subnets/subnet-simple.yaml +region: europe-west4 +ip_cidr_range: 10.0.1.0/24 +``` + +```yaml +# tftest-file id=subnet-detailed path=config/subnets/subnet-detailed.yaml region: europe-west1 description: Sample description ip_cidr_range: 10.0.0.0/24 @@ -249,11 +336,50 @@ iam_service_accounts: ["fbz@prj.iam.gserviceaccount.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 - - aggregation_interval: "INTERVAL_5_SEC" - - flow_sampling: 0.5 - - metadata: "INCLUDE_ALL_METADATA" + aggregation_interval: "INTERVAL_5_SEC" + flow_sampling: 0.5 + metadata: "INCLUDE_ALL_METADATA" + filter_expression: null ``` - + +### Custom Routes + +VPC routes can be configured through the `routes` variable. + +```hcl +locals { + route_types = { + gateway = "global/gateways/default-internet-gateway" + instance = "zones/europe-west1-b/test" + ip = "192.168.0.128" + ilb = "regions/europe-west1/forwardingRules/test" + vpn_tunnel = "regions/europe-west1/vpnTunnels/foo" + } +} +module "vpc" { + source = "./fabric/modules/net-vpc" + for_each = local.route_types + project_id = "my-project" + name = "my-network-with-route-${replace(each.key, "_", "-")}" + routes = { + next-hop = { + dest_range = "192.168.128.0/24" + tags = null + next_hop_type = each.key + next_hop = each.value + } + gateway = { + dest_range = "0.0.0.0/0", + priority = 100 + tags = ["tag-a"] + next_hop_type = "gateway", + next_hop = "global/gateways/default-internet-gateway" + } + } +} +# tftest modules=5 resources=15 inventory=routes.yaml +``` + ## Variables diff --git a/modules/net-vpc/main.tf b/modules/net-vpc/main.tf index 7eedc95ac..d15058017 100644 --- a/modules/net-vpc/main.tf +++ b/modules/net-vpc/main.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -109,7 +109,7 @@ resource "google_dns_policy" "default" { ) iterator = ns content { - ipv4_address = ns.key + ipv4_address = ns.value forwarding_path = "private" } } @@ -121,7 +121,7 @@ resource "google_dns_policy" "default" { ) iterator = ns content { - ipv4_address = ns.key + ipv4_address = ns.value } } } diff --git a/modules/net-vpc/versions.tf b/modules/net-vpc/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/net-vpc/versions.tf +++ b/modules/net-vpc/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/net-vpn-dynamic/README.md b/modules/net-vpn-dynamic/README.md index 378ba6b7b..447e5652c 100644 --- a/modules/net-vpn-dynamic/README.md +++ b/modules/net-vpn-dynamic/README.md @@ -23,11 +23,11 @@ module "vm" { module "vpn-dynamic" { - source = "./fabric/modules/net-vpn-dynamic" - project_id = "my-project" - region = "europe-west1" - network = var.vpc.name - name = "gateway-1" + source = "./fabric/modules/net-vpn-dynamic" + project_id = "my-project" + region = "europe-west1" + network = var.vpc.name + name = "gateway-1" router_config = { asn = 64514 } diff --git a/modules/net-vpn-dynamic/versions.tf b/modules/net-vpn-dynamic/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/net-vpn-dynamic/versions.tf +++ b/modules/net-vpn-dynamic/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/net-vpn-ha/README.md b/modules/net-vpn-ha/README.md index e6cd752c2..0b7b52903 100644 --- a/modules/net-vpn-ha/README.md +++ b/modules/net-vpn-ha/README.md @@ -13,7 +13,7 @@ module "vpn-1" { name = "net1-to-net-2" peer_gateway = { gcp = module.vpn-2.self_link } router_config = { - asn = 64514 + asn = 64514 custom_advertise = { all_subnets = true ip_ranges = { @@ -48,7 +48,7 @@ module "vpn-2" { network = var.vpc2.self_link name = "net2-to-net1" router_config = { asn = 64513 } - peer_gateway = { gcp = module.vpn-1.self_link} + peer_gateway = { gcp = module.vpn-1.self_link } tunnels = { remote-0 = { bgp_peer = { diff --git a/modules/net-vpn-ha/versions.tf b/modules/net-vpn-ha/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/net-vpn-ha/versions.tf +++ b/modules/net-vpn-ha/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/net-vpn-static/versions.tf b/modules/net-vpn-static/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/net-vpn-static/versions.tf +++ b/modules/net-vpn-static/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/organization/README.md b/modules/organization/README.md index 2e24c91bd..b6caa3cd0 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -16,22 +16,14 @@ To manage organization policies, the `orgpolicy.googleapis.com` service should b module "org" { source = "./fabric/modules/organization" organization_id = "organizations/1234567890" - group_iam = { + group_iam = { "cloud-owners@example.org" = ["roles/owner", "roles/projectCreator"] } - iam = { + iam = { "roles/resourcemanager.projectCreator" = ["group:cloud-admins@example.org"] } - - org_policy_custom_constraints = { - "custom.gkeEnableAutoUpgrade" = { - resource_types = ["container.googleapis.com/NodePool"] - method_types = ["CREATE"] - condition = "resource.management.autoUpgrade == true" - action_type = "ALLOW" - display_name = "Enable node auto-upgrade" - description = "All node pools must have node auto-upgrade enabled." - } + iam_additive_members = { + "user:compute@example.org" = ["roles/compute.admin", "roles/container.viewer"] } org_policies = { @@ -76,7 +68,7 @@ module "org" { } } } -# tftest modules=1 resources=12 +# tftest modules=1 resources=13 inventory=basic.yaml ``` ## IAM @@ -104,7 +96,7 @@ To manage organization policy custom constraints, the `orgpolicy.googleapis.com` module "org" { source = "./fabric/modules/organization" organization_id = var.organization_id - + org_policy_custom_constraints = { "custom.gkeEnableAutoUpgrade" = { resource_types = ["container.googleapis.com/NodePool"] @@ -123,7 +115,7 @@ module "org" { } } } -# tftest modules=1 resources=2 +# tftest modules=1 resources=2 inventory=custom-constraints.yaml ``` ### Org policy custom constraints factory @@ -134,16 +126,20 @@ The example below deploys a few org policy custom constraints split between two ```hcl module "org" { - source = "./fabric/modules/organization" - organization_id = var.organization_id - + source = "./fabric/modules/organization" + organization_id = var.organization_id org_policy_custom_constraints_data_path = "configs/custom-constraints" + org_policies = { + "custom.gkeEnableAutoUpgrade" = { + enforce = true + } + } } -# tftest modules=1 resources=3 files=gke,dataproc +# tftest modules=1 resources=3 files=gke inventory=custom-constraints.yaml ``` ```yaml -# tftest file gke configs/custom-constraints/gke.yaml +# tftest-file id=gke path=configs/custom-constraints/gke.yaml custom.gkeEnableLogging: resource_types: - container.googleapis.com/Cluster @@ -164,8 +160,9 @@ custom.gkeEnableAutoUpgrade: description: All node pools must have node auto-upgrade enabled. ``` + ```yaml -# tftest file dataproc configs/custom-constraints/dataproc.yaml +# tftest-file id=dataproc path=configs/custom-constraints/dataproc.yaml custom.dataprocNoMoreThan10Workers: resource_types: - dataproc.googleapis.com/Cluster @@ -195,6 +192,17 @@ module "org" { organization_id = var.organization_id firewall_policies = { iap-policy = { + allow-admins = { + description = "Access from the admin subnet to all subnets" + direction = "INGRESS" + action = "allow" + priority = 1000 + ranges = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] + ports = { all = [] } + target_service_accounts = null + target_resources = null + logging = false + } allow-iap-ssh = { description = "Always allow ssh from IAP." direction = "INGRESS" @@ -214,7 +222,7 @@ module "org" { iap_policy = "iap-policy" } } -# tftest modules=1 resources=3 +# tftest modules=1 resources=4 inventory=hfw.yaml ``` ### Firewall policy factory @@ -227,18 +235,18 @@ module "org" { organization_id = var.organization_id firewall_policy_factory = { cidr_file = "configs/firewall-policies/cidrs.yaml" - policy_name = null + policy_name = "iap-policy" rules_file = "configs/firewall-policies/rules.yaml" } firewall_policy_association = { - factory-policy = module.org.firewall_policy_id["factory"] + iap_policy = module.org.firewall_policy_id["iap-policy"] } } -# tftest modules=1 resources=4 files=cidrs,rules +# tftest modules=1 resources=4 files=cidrs,rules inventory=hfw.yaml ``` ```yaml -# tftest file cidrs configs/firewall-policies/cidrs.yaml +# tftest-file id=cidrs path=configs/firewall-policies/cidrs.yaml rfc1918: - 10.0.0.0/8 - 172.16.0.0/12 @@ -246,7 +254,7 @@ rfc1918: ``` ```yaml -# tftest file rules configs/firewall-policies/rules.yaml +# tftest-file id=rules path=configs/firewall-policies/rules.yaml allow-admins: description: Access from the admin subnet to all subnets direction: INGRESS @@ -257,19 +265,19 @@ allow-admins: ports: all: [] target_resources: null - enable_logging: false + logging: false -allow-ssh-from-iap: - description: Enable SSH from IAP +allow-iap-ssh: + description: "Always allow ssh from IAP." direction: INGRESS action: allow - priority: 1002 + priority: 100 ranges: - 35.235.240.0/20 ports: tcp: ["22"] target_resources: null - enable_logging: false + logging: false ``` ## Logging Sinks @@ -325,7 +333,7 @@ module "org" { debug = { destination = module.bucket.id filter = "severity=DEBUG" - exclusions = { + exclusions = { no-compute = "logName:compute" } type = "logging" @@ -335,7 +343,7 @@ module "org" { no-gce-instances = "resource.type=gce_instance" } } -# tftest modules=5 resources=13 +# tftest modules=5 resources=13 inventory=logging.yaml ``` ## Custom Roles @@ -353,7 +361,7 @@ module "org" { (module.org.custom_role_id.myRole) = ["user:me@example.com"] } } -# tftest modules=1 resources=2 +# tftest modules=1 resources=2 inventory=roles.yaml ``` ## Tags @@ -366,12 +374,12 @@ module "org" { organization_id = var.organization_id tags = { environment = { - description = "Environment specification." - iam = { + description = "Environment specification." + iam = { "roles/resourcemanager.tagAdmin" = ["group:admins@example.com"] } values = { - dev = {} + dev = {} prod = { description = "Environment: production." iam = { @@ -386,7 +394,7 @@ module "org" { foo = "tagValues/12345678" } } -# tftest modules=1 resources=7 +# tftest modules=1 resources=7 inventory=tags.yaml ``` You can also define network tags, through a dedicated variable *network_tags*: @@ -397,13 +405,13 @@ module "org" { organization_id = var.organization_id network_tags = { net-environment = { - description = "This is a network tag." - network = "my_project/my_vpc" - iam = { + description = "This is a network tag." + network = "my_project/my_vpc" + iam = { "roles/resourcemanager.tagAdmin" = ["group:admins@example.com"] } values = { - dev = null + dev = null prod = { description = "Environment: production." iam = { @@ -414,7 +422,7 @@ module "org" { } } } -# tftest modules=1 resources=5 +# tftest modules=1 resources=5 inventory=network-tags.yaml ``` @@ -454,13 +462,13 @@ module "org" { | [iam_bindings_authoritative](variables.tf#L116) | IAM authoritative bindings, in {ROLE => [MEMBERS]} format. Roles and members not explicitly listed will be cleared. Bindings should also be authoritative when using authoritative audit config. Use with caution. | map(list(string)) | | null | | [logging_exclusions](variables.tf#L122) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_sinks](variables.tf#L129) | Logging sinks to create for the organization. | map(object({…})) | | {} | -| [network_tags](variables.tf#L159) | Network tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | -| [org_policies](variables.tf#L180) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | +| [network_tags](variables.tf#L159) | 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#L181) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | | [org_policies_data_path](variables.tf#L220) | Path containing org policies in YAML format. | string | | null | | [org_policy_custom_constraints](variables.tf#L226) | Organization policiy custom constraints keyed by constraint name. | map(object({…})) | | {} | | [org_policy_custom_constraints_data_path](variables.tf#L240) | Path containing org policy custom constraints in YAML format. | string | | null | | [tag_bindings](variables.tf#L255) | Tag bindings for this organization, in key => tag value id format. | map(string) | | null | -| [tags](variables.tf#L261) | Tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | +| [tags](variables.tf#L261) | 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 @@ -472,9 +480,9 @@ module "org" { | [firewall_policy_id](outputs.tf#L40) | Map of firewall policy ids created in the organization. | | | [network_tag_keys](outputs.tf#L45) | Tag key resources. | | | [network_tag_values](outputs.tf#L54) | Tag value resources. | | -| [organization_id](outputs.tf#L65) | Organization id dependent on module resources. | | -| [sink_writer_identities](outputs.tf#L82) | Writer identities created for each sink. | | -| [tag_keys](outputs.tf#L90) | Tag key resources. | | -| [tag_values](outputs.tf#L99) | Tag value resources. | | +| [organization_id](outputs.tf#L62) | Organization id dependent on module resources. | | +| [sink_writer_identities](outputs.tf#L79) | Writer identities created for each sink. | | +| [tag_keys](outputs.tf#L87) | Tag key resources. | | +| [tag_values](outputs.tf#L96) | Tag value resources. | | diff --git a/modules/organization/organization-policies.tf b/modules/organization/organization-policies.tf index 62d464557..1a99ef9a1 100644 --- a/modules/organization/organization-policies.tf +++ b/modules/organization/organization-policies.tf @@ -95,23 +95,6 @@ resource "google_org_policy_policy" "default" { inherit_from_parent = each.value.inherit_from_parent reset = each.value.reset - rules { - allow_all = try(each.value.allow.all, null) == true ? "TRUE" : null - deny_all = try(each.value.deny.all, null) == true ? "TRUE" : null - enforce = ( - each.value.is_boolean_policy && each.value.enforce != null - ? upper(tostring(each.value.enforce)) - : null - ) - dynamic "values" { - for_each = each.value.has_values ? [1] : [] - content { - allowed_values = try(each.value.allow.values, null) - denied_values = try(each.value.deny.values, null) - } - } - } - dynamic "rules" { for_each = each.value.rules iterator = rule @@ -138,6 +121,23 @@ resource "google_org_policy_policy" "default" { } } } + + rules { + allow_all = try(each.value.allow.all, null) == true ? "TRUE" : null + deny_all = try(each.value.deny.all, null) == true ? "TRUE" : null + enforce = ( + each.value.is_boolean_policy && each.value.enforce != null + ? upper(tostring(each.value.enforce)) + : null + ) + dynamic "values" { + for_each = each.value.has_values ? [1] : [] + content { + allowed_values = try(each.value.allow.values, null) + denied_values = try(each.value.deny.values, null) + } + } + } } depends_on = [ diff --git a/modules/organization/outputs.tf b/modules/organization/outputs.tf index 40d84b473..2e594ee66 100644 --- a/modules/organization/outputs.tf +++ b/modules/organization/outputs.tf @@ -54,11 +54,8 @@ output "network_tag_keys" { output "network_tag_values" { description = "Tag value resources." value = { - for k, v in google_tags_tag_value.default - : k => v if( - google_tags_tag_key.default[split("/", k)[0]].purpose != null && - google_tags_tag_key.default[split("/", k)[0]].purpose != "" - ) + for k, v in google_tags_tag_value.default : + k => v if local.tag_values[k].tag_network } } @@ -99,10 +96,7 @@ output "tag_keys" { output "tag_values" { description = "Tag value resources." value = { - for k, v in google_tags_tag_value.default - : k => v if( - google_tags_tag_key.default[split("/", k)[0]].purpose == null || - google_tags_tag_key.default[split("/", k)[0]].purpose == "" - ) + for k, v in google_tags_tag_value.default : + k => v if !local.tag_values[k].tag_network } } diff --git a/modules/organization/tags.tf b/modules/organization/tags.tf index 544b8989f..b579479ed 100644 --- a/modules/organization/tags.tf +++ b/modules/organization/tags.tf @@ -23,17 +23,21 @@ locals { "Managed by the Terraform organization module." ) key = "${tag}/${value}" + id = try(value_attrs.id, null) name = value roles = keys(coalesce( value_attrs == null ? null : value_attrs.iam, {} )) - tag = tag + tag = tag + tag_id = attrs.id + tag_network = try(attrs.network, null) != null } ] ]) _tag_values_iam = flatten([ for key, value_attrs in local.tag_values : [ for role in value_attrs.roles : { + id = value_attrs.id key = value_attrs.key name = value_attrs.name role = role @@ -44,8 +48,9 @@ locals { _tags_iam = flatten([ for tag, attrs in local.tags : [ for role in keys(coalesce(attrs.iam, {})) : { - role = role - tag = tag + role = role + tag = tag + tag_id = attrs.id } ] ]) @@ -64,7 +69,7 @@ locals { # keys resource "google_tags_tag_key" "default" { - for_each = local.tags + for_each = { for k, v in local.tags : k => v if v.id == null } parent = var.organization_id purpose = ( lookup(each.value, "network", null) == null ? null : "GCE_FIREWALL" @@ -83,8 +88,12 @@ resource "google_tags_tag_key" "default" { resource "google_tags_tag_key_iam_binding" "default" { for_each = local.tags_iam - tag_key = google_tags_tag_key.default[each.value.tag].id - role = each.value.role + tag_key = ( + each.value.tag_id == null + ? google_tags_tag_key.default[each.value.tag].id + : each.value.tag_id + ) + role = each.value.role members = coalesce( local.tags[each.value.tag]["iam"][each.value.role], [] ) @@ -93,16 +102,24 @@ resource "google_tags_tag_key_iam_binding" "default" { # values resource "google_tags_tag_value" "default" { - for_each = local.tag_values - parent = google_tags_tag_key.default[each.value.tag].id + for_each = { for k, v in local.tag_values : k => v if v.id == null } + parent = ( + each.value.tag_id == null + ? google_tags_tag_key.default[each.value.tag].id + : each.value.tag_id + ) short_name = each.value.name description = each.value.description } resource "google_tags_tag_value_iam_binding" "default" { - for_each = local.tag_values_iam - tag_value = google_tags_tag_value.default[each.value.key].id - role = each.value.role + for_each = local.tag_values_iam + tag_value = ( + each.value.id == null + ? google_tags_tag_value.default[each.value.key].id + : each.value.id + ) + role = each.value.role members = coalesce( local.tags[each.value.tag]["values"][each.value.name]["iam"][each.value.role], [] diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index 84c81ff5b..ced5cad3d 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -157,10 +157,11 @@ variable "logging_sinks" { } variable "network_tags" { - description = "Network tags by key name. The `iam` attribute behaves like the similarly named one at module level." + description = "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." type = map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) + id = optional(string) network = string # project_id/vpc_name values = optional(map(object({ description = optional(string, "Managed by the Terraform organization module.") @@ -193,7 +194,6 @@ variable "org_policies" { values = optional(list(string)) })) enforce = optional(bool, true) # for boolean policies only. - # conditional values rules = optional(list(object({ allow = optional(object({ @@ -259,13 +259,15 @@ variable "tag_bindings" { } variable "tags" { - description = "Tags by key name. The `iam` attribute behaves like the similarly named one at module level." + description = "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." type = map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) + id = optional(string) values = optional(map(object({ description = optional(string, "Managed by the Terraform organization module.") iam = optional(map(list(string)), {}) + id = optional(string) })), {}) })) nullable = false diff --git a/modules/organization/versions.tf b/modules/organization/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/organization/versions.tf +++ b/modules/organization/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/project/README.md b/modules/project/README.md index b6fb3881c..e7a645fe5 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -2,6 +2,23 @@ This module implements the creation and management of one GCP project including IAM, organization policies, Shared VPC host or service attachment, service API activation, and tag attachment. It also offers a convenient way to refer to managed service identities (aka robot service accounts) for APIs. +# Basic Project Creation + +```hcl +module "project" { + source = "./fabric/modules/project" + billing_account = "123456-123456-123456" + name = "myproject" + parent = "folders/1234567890" + prefix = "foo" + services = [ + "container.googleapis.com", + "stackdriver.googleapis.com" + ] +} +# tftest modules=1 resources=3 inventory=basic.yaml +``` + ## IAM Examples IAM is managed via several variables that implement different levels of control: @@ -26,7 +43,7 @@ module "project" { name = "project-example" parent = "folders/1234567890" prefix = "foo" - services = [ + services = [ "container.googleapis.com", "stackdriver.googleapis.com" ] @@ -36,7 +53,7 @@ module "project" { ] } } -# tftest modules=1 resources=4 +# tftest modules=1 resources=4 inventory=iam-authoritative.yaml ``` The `group_iam` variable uses group email addresses as keys and is a convenient way to assign roles to humans following Google's best practices. The end result is readable code that also serves as documentation. @@ -48,10 +65,6 @@ module "project" { name = "project-example" parent = "folders/1234567890" prefix = "foo" - services = [ - "container.googleapis.com", - "stackdriver.googleapis.com" - ] group_iam = { "gcp-security-admins@example.com" = [ "roles/cloudasset.owner", @@ -61,7 +74,7 @@ module "project" { ] } } -# tftest modules=1 resources=7 +# tftest modules=1 resources=5 inventory=iam-group.yaml ``` ### Additive IAM @@ -70,22 +83,37 @@ Additive IAM is typically used where bindings for specific roles are controlled ```hcl module "project" { - source = "./fabric/modules/project" - name = "project-example" + source = "./fabric/modules/project" + name = "project-example" iam_additive = { - "roles/viewer" = [ + "roles/viewer" = [ "group:one@example.org", "group:two@xample.org" ], - "roles/storage.objectAdmin" = [ + "roles/storage.objectAdmin" = [ "group:two@example.org" ], - "roles/owner" = [ + "roles/owner" = [ "group:three@example.org" ], } } -# tftest modules=1 resources=5 +# tftest modules=1 resources=5 inventory=iam-additive.yaml +``` + +### Additive IAM by members + +```hcl +module "project" { + source = "./fabric/modules/project" + name = "project-example" + iam_additive_members = { + "user:one@example.org" = ["roles/owner"] + "user:two@example.org" = ["roles/owner", "roles/editor"] + } + +} +# tftest modules=1 resources=4 inventory=iam-additive-members.yaml ``` ### Service Identities and authoritative IAM @@ -94,15 +122,15 @@ As mentioned above, there are cases where authoritative management of specific I ```hcl module "project" { - source = "./fabric/modules/project" - name = "project-example" + source = "./fabric/modules/project" + name = "project-example" group_iam = { "foo@example.com" = [ "roles/editor" ] } iam = { - "roles/editor" = [ + "roles/editor" = [ "serviceAccount:${module.project.service_accounts.cloud_services}" ] } @@ -110,39 +138,88 @@ module "project" { # tftest modules=1 resources=2 ``` -## Shared VPC service - -The module allows managing Shared VPC status for both hosts and service projects, and includes a simple way of assigning Shared VPC roles to service identities. - -### Host project - -You can enable Shared VPC Host at the project level and manage project service association independently. +### Using shortcodes for Service Identities in additive IAM +Most Service Identities contains project number in their e-mail address and this prevents additive IAM to work, as these values are not known at moment of execution of `terraform plan` (its not an issue for authoritative IAM). To refer current project Service Identities you may use shortcodes for Service Identities similarly as for `service_identity_iam` when configuring Shared VPC. ```hcl module "project" { - source = "./fabric/modules/project" - name = "project-example" - shared_vpc_host_config = { - enabled = true + source = "./fabric/modules/project" + name = "project-example" + + services = [ + "run.googleapis.com", + "container.googleapis.com", + ] + + iam_additive = { + "roles/editor" = ["cloudservices"] + "roles/vpcaccess.user" = ["cloudrun"] + "roles/container.hostServiceAgentUser" = ["container-engine"] + } +} +# tftest modules=1 resources=6 +``` + + +### Service identities requiring manual IAM grants + +The module will create service identities at project creation instead of creating of them at the time of first use. This allows granting these service identities roles in other projects, something which is usually necessary in a Shared VPC context. + +You can grant roles to service identities using the following construct: + +```hcl +module "project" { + source = "./fabric/modules/project" + name = "project-example" + iam = { + "roles/apigee.serviceAgent" = [ + "serviceAccount:${module.project.service_accounts.robots.apigee}" + ] } } # tftest modules=1 resources=2 ``` -### Service project +This table lists all affected services and roles that you need to grant to service identities + +| service | service identity | role | +|---|---|---| +| apigee.googleapis.com | apigee | roles/apigee.serviceAgent | +| artifactregistry.googleapis.com | artifactregistry | roles/artifactregistry.serviceAgent | +| cloudasset.googleapis.com | cloudasset | roles/cloudasset.serviceAgent | +| cloudbuild.googleapis.com | cloudbuild | roles/cloudbuild.builds.builder | +| gkehub.googleapis.com | fleet | roles/gkehub.serviceAgent | +| multiclusteringress.googleapis.com | multicluster-ingress | roles/multiclusteringress.serviceAgent | +| pubsub.googleapis.com | pubsub | roles/pubsub.serviceAgent | +| sqladmin.googleapis.com | sqladmin | roles/cloudsql.serviceAgent | + + +## Shared VPC + +The module allows managing Shared VPC status for both hosts and service projects, and includes a simple way of assigning Shared VPC roles to service identities. + +You can enable Shared VPC Host at the project level and manage project service association independently. ```hcl -module "project" { - source = "./fabric/modules/project" - name = "project-example" +module "host-project" { + source = "./fabric/modules/project" + name = "my-host-project" + shared_vpc_host_config = { + enabled = true + } +} + +module "service-project" { + source = "./fabric/modules/project" + name = "my-service-project" shared_vpc_service_config = { - attach = true - host_project = "my-host-project" + attach = true + host_project = module.host-project.project_id service_identity_iam = { - "roles/compute.networkUser" = [ + "roles/compute.networkUser" = [ "cloudservices", "container-engine" ] - "roles/vpcaccess.user" = [ + "roles/vpcaccess.user" = [ "cloudrun" ] "roles/container.hostServiceAgentUser" = [ @@ -151,7 +228,7 @@ module "project" { } } } -# tftest modules=1 resources=6 +# tftest modules=2 resources=8 inventory=shared-vpc.yaml ``` ## Organization policies @@ -165,10 +242,6 @@ module "project" { name = "project-example" parent = "folders/1234567890" prefix = "foo" - services = [ - "container.googleapis.com", - "stackdriver.googleapis.com" - ] org_policies = { "compute.disableGuestAttributesAccess" = { enforce = true @@ -208,7 +281,7 @@ module "project" { } } } -# tftest modules=1 resources=10 +# tftest modules=1 resources=8 inventory=org-policies.yaml ``` ### Organization policy factory @@ -220,63 +293,54 @@ Note that contraints defined via `org_policies` take precedence over those in `o The example below deploys a few organization policies split between two YAML files. ```hcl -module "folder" { - source = "./fabric/modules/folder" - parent = "organizations/1234567890" - name = "Folder name" +module "project" { + source = "./fabric/modules/project" + billing_account = "123456-123456-123456" + name = "project-example" + parent = "folders/1234567890" + prefix = "foo" org_policies_data_path = "configs/org-policies/" } -# tftest modules=1 resources=6 files=boolean,list +# tftest modules=1 resources=8 files=boolean,list inventory=org-policies.yaml ``` ```yaml -# tftest file boolean configs/org-policies/boolean.yaml +# tftest-file id=boolean path=configs/org-policies/boolean.yaml +compute.disableGuestAttributesAccess: + enforce: true +constraints/compute.skipDefaultNetworkCreation: + enforce: true iam.disableServiceAccountKeyCreation: enforce: true - iam.disableServiceAccountKeyUpload: enforce: false rules: - - condition: - expression: resource.matchTagId("tagKeys/1234", "tagValues/1234") - title: condition - description: test condition - location: xxx - enforce: true + - condition: + description: test condition + expression: resource.matchTagId("tagKeys/1234", "tagValues/1234") + location: somewhere + title: condition + enforce: true ``` ```yaml -# tftest file list configs/org-policies/list.yaml -compute.vmExternalIpAccess: - deny: - all: true - -iam.allowedPolicyMemberDomains: +# tftest-file id=list path=configs/org-policies/list.yaml +constraints/compute.trustedImageProjects: allow: values: - - C0xxxxxxx - - C0yyyyyyy - -compute.restrictLoadBalancerCreationForTypes: + - projects/my-project +constraints/compute.vmExternalIpAccess: deny: - values: ["in:EXTERNAL"] - rules: - - condition: - expression: resource.matchTagId("tagKeys/1234", "tagValues/1234") - title: condition - description: test condition - allow: - values: ["in:EXTERNAL"] - - condition: - expression: resource.matchTagId("tagKeys/12345", "tagValues/12345") - title: condition2 - description: test condition2 - allow: - all: true + all: true +constraints/iam.allowedPolicyMemberDomains: + allow: + values: + - C0xxxxxxx + - C0yyyyyyy ``` -## Logging Sinks (in same project) +## Logging Sinks ```hcl module "gcs" { @@ -339,49 +403,18 @@ module "project-host" { no-gce-instances = "resource.type=gce_instance" } } -# tftest modules=5 resources=14 +# tftest modules=5 resources=14 inventory=logging.yaml ``` -## Logging Sinks (in different project) - -When writing to destinations in a different project, set `unique_writer` to `true`. - -```hcl -module "gcs" { - source = "./fabric/modules/gcs" - project_id = "project-1" - name = "gcs_sink" - force_destroy = true -} - -module "project-host" { - source = "./fabric/modules/project" - name = "project-2" - billing_account = "123456-123456-123456" - parent = "folders/1234567890" - logging_sinks = { - warnings = { - destination = module.gcs.id - filter = "severity=WARNING" - unique_writer = true - type = "storage" - } - } -} -# tftest modules=2 resources=4 -``` - - ## Cloud KMS encryption keys The module offers a simple, centralized way to assign `roles/cloudkms.cryptoKeyEncrypterDecrypter` to service identities. ```hcl module "project" { - source = "./fabric/modules/project" - name = "my-project" - billing_account = "123456-123456-123456" - prefix = "foo" + source = "./fabric/modules/project" + name = "my-project" + prefix = "foo" services = [ "compute.googleapis.com", "storage.googleapis.com" @@ -409,8 +442,8 @@ module "org" { organization_id = var.organization_id tags = { environment = { - description = "Environment specification." - iam = null + description = "Environment specification." + iam = null values = { dev = null prod = null @@ -438,8 +471,8 @@ One non-obvious output is `service_accounts`, which offers a simple way to disco ```hcl module "project" { - source = "./fabric/modules/project" - name = "project-example" + source = "./fabric/modules/project" + name = "project-example" services = [ "compute.googleapis.com" ] @@ -448,7 +481,7 @@ module "project" { output "compute_robot" { value = module.project.service_accounts.robots.compute } -# tftest modules=1 resources=2 +# tftest modules=1 resources=2 inventory:outputs.yaml ``` diff --git a/modules/project/iam.tf b/modules/project/iam.tf index 69925cc76..3ed2d2a6f 100644 --- a/modules/project/iam.tf +++ b/modules/project/iam.tf @@ -47,7 +47,18 @@ locals { } iam_additive = { for pair in concat(local._iam_additive_pairs, local._iam_additive_member_pairs) : - "${pair.role}-${pair.member}" => pair + "${pair.role}-${pair.member}" => { + role = pair.role + member = ( + pair.member == "cloudservices" + ? "serviceAccount:${local.service_account_cloud_services}" + : pair.member == "default-compute" + ? "serviceAccount:${local.service_accounts_default.compute}" + : pair.member == "default-gae" + ? "serviceAccount:${local.service_accounts_default.gae}" + : try("serviceAccount:${local.service_accounts_robots[pair.member]}", pair.member) + ) + } } } diff --git a/modules/project/organization-policies.tf b/modules/project/organization-policies.tf index 7763aff40..4ff5bb992 100644 --- a/modules/project/organization-policies.tf +++ b/modules/project/organization-policies.tf @@ -95,23 +95,6 @@ resource "google_org_policy_policy" "default" { inherit_from_parent = each.value.inherit_from_parent reset = each.value.reset - rules { - allow_all = try(each.value.allow.all, null) == true ? "TRUE" : null - deny_all = try(each.value.deny.all, null) == true ? "TRUE" : null - enforce = ( - each.value.is_boolean_policy && each.value.enforce != null - ? upper(tostring(each.value.enforce)) - : null - ) - dynamic "values" { - for_each = each.value.has_values ? [1] : [] - content { - allowed_values = try(each.value.allow.values, null) - denied_values = try(each.value.deny.values, null) - } - } - } - dynamic "rules" { for_each = each.value.rules iterator = rule @@ -138,5 +121,22 @@ resource "google_org_policy_policy" "default" { } } } + + rules { + allow_all = try(each.value.allow.all, null) == true ? "TRUE" : null + deny_all = try(each.value.deny.all, null) == true ? "TRUE" : null + enforce = ( + each.value.is_boolean_policy && each.value.enforce != null + ? upper(tostring(each.value.enforce)) + : null + ) + dynamic "values" { + for_each = each.value.has_values ? [1] : [] + content { + allowed_values = try(each.value.allow.values, null) + denied_values = try(each.value.deny.values, null) + } + } + } } } diff --git a/modules/project/service-accounts.tf b/modules/project/service-accounts.tf index e1f6cb71a..e93978a86 100644 --- a/modules/project/service-accounts.tf +++ b/modules/project/service-accounts.tf @@ -45,6 +45,9 @@ locals { # TODO: jit? gke-mcs = "service-%s@gcp-sa-mcsd" monitoring-notifications = "service-%s@gcp-sa-monitoring-notification" + multicluster-ingress = "service-%s@gcp-sa-multiclusteringress" + multicluster-discovery = "service-%s@gcp-sa-mcsd" + notebooks = "service-%s@gcp-sa-notebooks" pubsub = "service-%s@gcp-sa-pubsub" secretmanager = "service-%s@gcp-sa-secretmanager" sql = "service-%s@gcp-sa-cloud-sql" @@ -67,14 +70,19 @@ locals { gke-mcs-importer = "${local.project.project_id}.svc.id.goog[gke-mcs/gke-mcs-importer]" } ) + # JIT-ed service accounts are created without default roles granted, these needs to be assigned manually to them + # Roles can be found here: https://cloud.google.com/iam/docs/service-agents + # Remember to update "Service identities requiring manual IAM grants" in README.md when updating this list service_accounts_jit_services = [ - "apigee.googleapis.com", - "artifactregistry.googleapis.com", - "cloudasset.googleapis.com", - "gkehub.googleapis.com", - "pubsub.googleapis.com", - "secretmanager.googleapis.com", - "sqladmin.googleapis.com" + "apigee.googleapis.com", # grant roles/apigee.serviceAgent to apigee + "artifactregistry.googleapis.com", # grant roles/artifactregistry.serviceAgent to artifactregistry + "cloudasset.googleapis.com", # grant roles/cloudasset.serviceAgent to cloudasset + "cloudbuild.googleapis.com", # grant roles/cloudbuild.builds.builder to cloudbuild + "gkehub.googleapis.com", # grant roles/gkehub.serviceAgent to fleet + "multiclusteringress.googleapis.com", # grant roles/multiclusteringress.serviceAgent to multicluster-ingress + "pubsub.googleapis.com", # grant roles/pubsub.serviceAgent to pubsub + "secretmanager.googleapis.com", # no grants needed + "sqladmin.googleapis.com", # grant roles/cloudsql.serviceAgent to sqladmin (TODO: verify) ] service_accounts_cmek_service_keys = distinct(flatten([ for s in keys(var.service_encryption_key_ids) : [ diff --git a/modules/project/versions.tf b/modules/project/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/project/versions.tf +++ b/modules/project/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/projects-data-source/versions.tf b/modules/projects-data-source/versions.tf index 23f38edbc..d9c1d37c7 100644 --- a/modules/projects-data-source/versions.tf +++ b/modules/projects-data-source/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/pubsub/README.md b/modules/pubsub/README.md index b204ff564..81e433659 100644 --- a/modules/pubsub/README.md +++ b/modules/pubsub/README.md @@ -28,7 +28,7 @@ module "topic_with_schema" { name = "my-topic" schema = { msg_encoding = "JSON" - schema_type = "AVRO" + schema_type = "AVRO" definition = jsonencode({ "type" = "record", "name" = "Avro", diff --git a/modules/pubsub/versions.tf b/modules/pubsub/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/pubsub/versions.tf +++ b/modules/pubsub/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/secret-manager/README.md b/modules/secret-manager/README.md index a0a55e473..6816db4d4 100644 --- a/modules/secret-manager/README.md +++ b/modules/secret-manager/README.md @@ -16,7 +16,7 @@ The secret replication policy is automatically managed if no location is set, or module "secret-manager" { source = "./fabric/modules/secret-manager" project_id = "my-project" - secrets = { + secrets = { test-auto = null test-manual = ["europe-west1", "europe-west4"] } @@ -32,12 +32,12 @@ IAM bindings can be set per secret in the same way as for most other modules sup module "secret-manager" { source = "./fabric/modules/secret-manager" project_id = "my-project" - secrets = { + secrets = { test-auto = null test-manual = ["europe-west1", "europe-west4"] } iam = { - test-auto = { + test-auto = { "roles/secretmanager.secretAccessor" = ["group:auto-readers@example.com"] } test-manual = { @@ -56,7 +56,7 @@ As mentioned above, please be aware that **version data will be stored in state module "secret-manager" { source = "./fabric/modules/secret-manager" project_id = "my-project" - secrets = { + secrets = { test-auto = null test-manual = ["europe-west1", "europe-west4"] } diff --git a/modules/secret-manager/versions.tf b/modules/secret-manager/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/secret-manager/versions.tf +++ b/modules/secret-manager/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/service-directory/README.md b/modules/service-directory/README.md index ded837f23..d6961b418 100644 --- a/modules/service-directory/README.md +++ b/modules/service-directory/README.md @@ -11,10 +11,10 @@ It can be used in conjunction with the [DNS](../dns) module to create [service-d ```hcl module "service-directory" { - source = "./fabric/modules/service-directory" - project_id = "my-project" - location = "europe-west1" - name = "sd-1" + source = "./fabric/modules/service-directory" + project_id = "my-project" + location = "europe-west1" + name = "sd-1" iam = { "roles/servicedirectory.editor" = [ "serviceAccount:namespace-editor@example.com" @@ -28,10 +28,10 @@ module "service-directory" { ```hcl module "service-directory" { - source = "./fabric/modules/service-directory" - project_id = "my-project" - location = "europe-west1" - name = "sd-1" + source = "./fabric/modules/service-directory" + project_id = "my-project" + location = "europe-west1" + name = "sd-1" services = { one = { endpoints = ["first", "second"] @@ -59,9 +59,9 @@ Wiring a service directory namespace to a private DNS zone allows querying the n ```hcl module "service-directory" { - source = "./fabric/modules/service-directory" - project_id = "my-project" - location = "europe-west1" + source = "./fabric/modules/service-directory" + project_id = "my-project" + location = "europe-west1" name = "apps" iam = { "roles/servicedirectory.editor" = [ diff --git a/modules/service-directory/versions.tf b/modules/service-directory/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/service-directory/versions.tf +++ b/modules/service-directory/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/source-repository/README.md b/modules/source-repository/README.md index 9baf0ebd0..389de9e9d 100644 --- a/modules/source-repository/README.md +++ b/modules/source-repository/README.md @@ -27,16 +27,16 @@ module "repo" { name = "my-repo" triggers = { foo = { - filename = "ci/workflow-foo.yaml" - included_files = ["**/*tf"] + filename = "ci/workflow-foo.yaml" + included_files = ["**/*tf"] service_account = null substitutions = { BAR = 1 } template = { branch_name = "main" - project_id = null - tag_name = null + project_id = null + tag_name = null } } } diff --git a/modules/source-repository/versions.tf b/modules/source-repository/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/source-repository/versions.tf +++ b/modules/source-repository/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/modules/vpc-sc/README.md b/modules/vpc-sc/README.md index 8ba38c105..8e412bcfa 100644 --- a/modules/vpc-sc/README.md +++ b/modules/vpc-sc/README.md @@ -34,6 +34,21 @@ module "test" { # tftest modules=1 resources=1 ``` +If you need the module to create a scoped policy for you, specify 'scopes' of the policy in the `access_policy_create` variable: + +```hcl +module "test" { + source = "./fabric/modules/vpc-sc" + access_policy = null + access_policy_create = { + parent = "organizations/123456" + title = "vpcsc-policy" + scopes = ["folders/456789"] + } +} +# tftest modules=1 resources=1 +``` + ### Access levels As highlighted above, the `access_levels` type replicates the underlying resource structure. @@ -120,7 +135,7 @@ module "test" { to = { operations = [{ method_selectors = ["*"] - service_name = "storage.googleapis.com" + service_name = "storage.googleapis.com" }] resources = ["projects/123456789"] } @@ -189,11 +204,11 @@ module "test" { |---|---|:---:|:---:|:---:| | [access_policy](variables.tf#L56) | Access Policy name, set to null if creating one. | string | ✓ | | | [access_levels](variables.tf#L17) | Access level definitions. | map(object({…})) | | {} | -| [access_policy_create](variables.tf#L61) | Access Policy configuration, fill in to create. Parent is in 'organizations/123456' format. | object({…}) | | null | -| [egress_policies](variables.tf#L70) | Egress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | -| [ingress_policies](variables.tf#L99) | Ingress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | -| [service_perimeters_bridge](variables.tf#L130) | Bridge service perimeters. | map(object({…})) | | {} | -| [service_perimeters_regular](variables.tf#L140) | Regular service perimeters. | map(object({…})) | | {} | +| [access_policy_create](variables.tf#L61) | Access Policy configuration, fill in to create. Parent is in 'organizations/123456' format, scopes are in 'folders/456789' or 'projects/project_id' format. | object({…}) | | null | +| [egress_policies](variables.tf#L71) | Egress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | +| [ingress_policies](variables.tf#L100) | Ingress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | +| [service_perimeters_bridge](variables.tf#L131) | Bridge service perimeters. | map(object({…})) | | {} | +| [service_perimeters_regular](variables.tf#L141) | Regular service perimeters. | map(object({…})) | | {} | ## Outputs diff --git a/modules/vpc-sc/main.tf b/modules/vpc-sc/main.tf index 0b06b4814..7dd589044 100644 --- a/modules/vpc-sc/main.tf +++ b/modules/vpc-sc/main.tf @@ -25,4 +25,5 @@ resource "google_access_context_manager_access_policy" "default" { count = var.access_policy_create != null ? 1 : 0 parent = var.access_policy_create.parent title = var.access_policy_create.title + scopes = var.access_policy_create.scopes } diff --git a/modules/vpc-sc/variables.tf b/modules/vpc-sc/variables.tf index a196cc52b..a10b07689 100644 --- a/modules/vpc-sc/variables.tf +++ b/modules/vpc-sc/variables.tf @@ -59,10 +59,11 @@ variable "access_policy" { } variable "access_policy_create" { - description = "Access Policy configuration, fill in to create. Parent is in 'organizations/123456' format." + description = "Access Policy configuration, fill in to create. Parent is in 'organizations/123456' format, scopes are in 'folders/456789' or 'projects/project_id' format." type = object({ parent = string title = string + scopes = optional(list(string), null) }) default = null } diff --git a/modules/vpc-sc/versions.tf b/modules/vpc-sc/versions.tf index 286536a65..08492c6f9 100644 --- a/modules/vpc-sc/versions.tf +++ b/modules/vpc-sc/versions.tf @@ -17,11 +17,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.40.0" # tftest + version = ">= 4.50.0" # tftest } } } diff --git a/tests/blueprints/cloud_operations/apigee/__init__.py b/tests/blueprints/apigee/bigquery-analytics/__init__.py similarity index 100% rename from tests/blueprints/cloud_operations/apigee/__init__.py rename to tests/blueprints/apigee/bigquery-analytics/__init__.py diff --git a/tests/blueprints/apigee/bigquery-analytics/basic.tfvars b/tests/blueprints/apigee/bigquery-analytics/basic.tfvars new file mode 100644 index 000000000..2f9315a43 --- /dev/null +++ b/tests/blueprints/apigee/bigquery-analytics/basic.tfvars @@ -0,0 +1,24 @@ +project_create = { + billing_account_id = "12345-12345-12345" + parent = "folders/123456789" +} +project_id = "my-project" +envgroups = { + test = ["test.cool-demos.space"] +} +environments = { + apis-test = { + envgroups = ["test"] + } +} +instances = { + instance-ew1 = { + region = "europe-west1" + environments = ["apis-test"] + runtime_ip_cidr_range = "10.0.4.0/22" + troubleshooting_ip_cidr_range = "10.1.0.0/28" + } +} +psc_config = { + europe-west1 = "10.0.0.0/28" +} diff --git a/tests/modules/net_glb/__init__.py b/tests/blueprints/apigee/bigquery-analytics/basic.yaml similarity index 93% rename from tests/modules/net_glb/__init__.py rename to tests/blueprints/apigee/bigquery-analytics/basic.yaml index 6d6d1266c..691af456b 100644 --- a/tests/modules/net_glb/__init__.py +++ b/tests/blueprints/apigee/bigquery-analytics/basic.yaml @@ -11,3 +11,7 @@ # 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. + +counts: + modules: 9 + resources: 62 diff --git a/tests/blueprints/factories/bigquery_factory/fixture/views/view_a.yaml b/tests/blueprints/apigee/bigquery-analytics/tftest.yaml similarity index 89% rename from tests/blueprints/factories/bigquery_factory/fixture/views/view_a.yaml rename to tests/blueprints/apigee/bigquery-analytics/tftest.yaml index 23c41b98f..a3441f559 100644 --- a/tests/blueprints/factories/bigquery_factory/fixture/views/view_a.yaml +++ b/tests/blueprints/apigee/bigquery-analytics/tftest.yaml @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -dataset: dataset_b -view: view_a -query: "SELECT CURRENT_DATE() LIMIT 1" +module: blueprints/apigee/bigquery-analytics + +tests: + basic: diff --git a/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/__init__.py b/tests/blueprints/apigee/hybrid-gke/__init__.py similarity index 100% rename from tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/__init__.py rename to tests/blueprints/apigee/hybrid-gke/__init__.py diff --git a/tests/blueprints/apigee/hybrid-gke/basic.tfvars b/tests/blueprints/apigee/hybrid-gke/basic.tfvars new file mode 100644 index 000000000..5b2cb4ccf --- /dev/null +++ b/tests/blueprints/apigee/hybrid-gke/basic.tfvars @@ -0,0 +1,6 @@ +project_create = { + billing_account_id = "12345-12345-12345" + parent = "folders/123456789" +} +project_id = "my-project" +hostname = "test.myorg.org" \ No newline at end of file diff --git a/tests/modules/iam_service_account/__init__.py b/tests/blueprints/apigee/hybrid-gke/basic.yaml similarity index 93% rename from tests/modules/iam_service_account/__init__.py rename to tests/blueprints/apigee/hybrid-gke/basic.yaml index 6d6d1266c..0bab56418 100644 --- a/tests/modules/iam_service_account/__init__.py +++ b/tests/blueprints/apigee/hybrid-gke/basic.yaml @@ -11,3 +11,7 @@ # 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. + +counts: + modules: 17 + resources: 59 diff --git a/tests/modules/net_vpc/data/factory-subnet2.yaml b/tests/blueprints/apigee/hybrid-gke/tftest.yaml similarity index 87% rename from tests/modules/net_vpc/data/factory-subnet2.yaml rename to tests/blueprints/apigee/hybrid-gke/tftest.yaml index e110c1625..ebe16e577 100644 --- a/tests/modules/net_vpc/data/factory-subnet2.yaml +++ b/tests/blueprints/apigee/hybrid-gke/tftest.yaml @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -region: europe-west4 -description: Sample description -ip_cidr_range: 10.129.0.0/24 +module: blueprints/apigee/hybrid-gke + +tests: + basic: diff --git a/tests/blueprints/factories/bigquery_factory/__init__.py b/tests/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/__init__.py similarity index 100% rename from tests/blueprints/factories/bigquery_factory/__init__.py rename to tests/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/__init__.py diff --git a/tests/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/basic.tfvars b/tests/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/basic.tfvars new file mode 100644 index 000000000..ae07c514f --- /dev/null +++ b/tests/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/basic.tfvars @@ -0,0 +1,5 @@ +billing_account_id = "12345-12345-12345" +parent = "folders/123456789" +apigee_project_id = "my-apigee-project" +onprem_project_id = "my-onprem-project" +hostname = "test.myorg.org" diff --git a/tests/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/basic.yaml b/tests/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/basic.yaml new file mode 100644 index 000000000..de461ff2e --- /dev/null +++ b/tests/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/basic.yaml @@ -0,0 +1,17 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +counts: + modules: 13 + resources: 73 diff --git a/tests/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/tftest.yaml b/tests/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/tftest.yaml new file mode 100644 index 000000000..5c92fb82a --- /dev/null +++ b/tests/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/tftest.yaml @@ -0,0 +1,18 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module: blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg + +tests: + basic: diff --git a/tests/blueprints/cloud_operations/apigee/fixture/main.tf b/tests/blueprints/cloud_operations/apigee/fixture/main.tf deleted file mode 100644 index bb319de2d..000000000 --- a/tests/blueprints/cloud_operations/apigee/fixture/main.tf +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module "test" { - source = "../../../../../blueprints/cloud-operations/apigee" - project_create = var.project_create - project_id = var.project_id - organization = var.organization - envgroups = var.envgroups - environments = var.environments - instances = var.instances - path = var.path - datastore_name = var.datastore_name - psc_config = var.psc_config -} diff --git a/tests/blueprints/cloud_operations/apigee/fixture/variables.tf b/tests/blueprints/cloud_operations/apigee/fixture/variables.tf deleted file mode 100644 index d66f3874d..000000000 --- a/tests/blueprints/cloud_operations/apigee/fixture/variables.tf +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -variable "project_create" { - description = "Parameters for the creation of the new project." - type = object({ - billing_account_id = string - parent = string - }) - default = null -} - -variable "vpc_create" { - description = "Boolean flag indicating whether the VPC should be created or not." - type = bool - default = true -} - -variable "project_id" { - description = "Project ID." - type = string - nullable = false -} - -variable "organization" { - description = "Apigee organization." - type = object({ - display_name = optional(string, "Apigee organization created by tf module") - description = optional(string, "Apigee organization created by tf module") - authorized_network = optional(string, "vpc") - runtime_type = optional(string, "CLOUD") - billing_type = optional(string) - database_encryption_key = optional(string) - analytics_region = optional(string, "europe-west1") - }) - nullable = false - default = { - } -} - -variable "envgroups" { - description = "Environment groups (NAME => [HOSTNAMES])." - type = map(list(string)) - nullable = false -} - -variable "environments" { - description = "Environments." - type = map(object({ - display_name = optional(string) - description = optional(string) - node_config = optional(object({ - min_node_count = optional(number) - max_node_count = optional(number) - })) - iam = optional(map(list(string))) - envgroups = list(string) - })) - nullable = false -} - -variable "instances" { - description = "Instance." - type = map(object({ - display_name = optional(string) - description = optional(string) - region = string - environments = list(string) - psa_ip_cidr_range = string - disk_encryption_key = optional(string) - consumer_accept_list = optional(list(string)) - })) - nullable = false -} - -variable "path" { - description = "Bucket path." - type = string - default = "/analytics" - nullable = false -} - -variable "datastore_name" { - description = "Datastore" - type = string - nullable = false - default = "gcs" -} - -variable "psc_config" { - description = "PSC configuration." - type = map(string) - nullable = false -} diff --git a/tests/blueprints/cloud_operations/asset_inventory_feed_remediation/test_plan.py b/tests/blueprints/cloud_operations/asset_inventory_feed_remediation/test_plan.py index df03e144f..497af6be5 100644 --- a/tests/blueprints/cloud_operations/asset_inventory_feed_remediation/test_plan.py +++ b/tests/blueprints/cloud_operations/asset_inventory_feed_remediation/test_plan.py @@ -16,4 +16,4 @@ def test_resources(e2e_plan_runner): "Test that plan works and the numbers of resources is as expected." modules, resources = e2e_plan_runner() assert len(modules) == 6 - assert len(resources) == 18 + assert len(resources) == 19 diff --git a/tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/test_plan.py b/tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/test_plan.py index c5394839d..3bcc63440 100644 --- a/tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/test_plan.py +++ b/tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/test_plan.py @@ -16,4 +16,4 @@ def test_resources(e2e_plan_runner): "Test that plan works and the numbers of resources is as expected." modules, resources = e2e_plan_runner() assert len(modules) == 7 - assert len(resources) == 29 + assert len(resources) == 30 diff --git a/tests/fast/stages/s00_bootstrap/__init__.py b/tests/blueprints/cloud_operations/terraform_enterprise_wif/__init__.py similarity index 100% rename from tests/fast/stages/s00_bootstrap/__init__.py rename to tests/blueprints/cloud_operations/terraform_enterprise_wif/__init__.py diff --git a/tests/fast/stages/s01_resman/__init__.py b/tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/__init__.py similarity index 100% rename from tests/fast/stages/s01_resman/__init__.py rename to tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/__init__.py diff --git a/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/main.tf b/tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/fixture/main.tf similarity index 100% rename from tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/main.tf rename to tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/fixture/main.tf diff --git a/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/variables.tf b/tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/fixture/variables.tf similarity index 100% rename from tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/variables.tf rename to tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/fixture/variables.tf diff --git a/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/test_plan.py b/tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/test_plan.py similarity index 100% rename from tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/test_plan.py rename to tests/blueprints/cloud_operations/terraform_enterprise_wif/gcp_workload_identity_provider/test_plan.py diff --git a/tests/blueprints/cloud_operations/unmanaged_instances_healthcheck/test_plan.py b/tests/blueprints/cloud_operations/unmanaged_instances_healthcheck/test_plan.py index 3f6f34994..b1f0fba3c 100644 --- a/tests/blueprints/cloud_operations/unmanaged_instances_healthcheck/test_plan.py +++ b/tests/blueprints/cloud_operations/unmanaged_instances_healthcheck/test_plan.py @@ -16,4 +16,4 @@ def test_resources(e2e_plan_runner): "Test that plan works and the numbers of resources is as expected." modules, resources = e2e_plan_runner() assert len(modules) == 10 - assert len(resources) == 34 + assert len(resources) == 32 diff --git a/tests/blueprints/data_solutions/cmek_via_centralized_kms/fixture/main.tf b/tests/blueprints/data_solutions/cmek_via_centralized_kms/fixture/main.tf index 65cc20aeb..3fee8af5f 100644 --- a/tests/blueprints/data_solutions/cmek_via_centralized_kms/fixture/main.tf +++ b/tests/blueprints/data_solutions/cmek_via_centralized_kms/fixture/main.tf @@ -15,7 +15,10 @@ */ module "test" { - source = "../../../../../blueprints/data-solutions/cmek-via-centralized-kms/" - billing_account = var.billing_account - root_node = var.root_node + source = "../../../../../blueprints/data-solutions/cmek-via-centralized-kms/" + project_config = { + billing_account_id = "123456-123456-123456" + parent = "folders/12345678" + } + prefix = "prefix" } diff --git a/tests/blueprints/data_solutions/data_platform_foundations/test_plan.py b/tests/blueprints/data_solutions/data_platform_foundations/test_plan.py index 1b51472cd..785f47053 100644 --- a/tests/blueprints/data_solutions/data_platform_foundations/test_plan.py +++ b/tests/blueprints/data_solutions/data_platform_foundations/test_plan.py @@ -21,5 +21,6 @@ FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture') def test_resources(e2e_plan_runner): "Test that plan works and the numbers of resources is as expected." modules, resources = e2e_plan_runner(FIXTURES_DIR) - assert len(modules) == 41 - assert len(resources) == 315 + + assert len(modules) == 42 + assert len(resources) == 296 diff --git a/tests/blueprints/data_solutions/data_playground/test_plan.py b/tests/blueprints/data_solutions/data_playground/test_plan.py index a0c3b5e6f..daaa57fc9 100644 --- a/tests/blueprints/data_solutions/data_playground/test_plan.py +++ b/tests/blueprints/data_solutions/data_playground/test_plan.py @@ -22,4 +22,4 @@ def test_resources(e2e_plan_runner): "Test that plan works and the numbers of resources is as expected." modules, resources = e2e_plan_runner(FIXTURES_DIR) assert len(modules) == 7 - assert len(resources) == 37 + assert len(resources) == 38 diff --git a/tests/blueprints/data_solutions/shielded_folder/simple.tfvars b/tests/blueprints/data_solutions/shielded_folder/simple.tfvars new file mode 100644 index 000000000..83e8b1399 --- /dev/null +++ b/tests/blueprints/data_solutions/shielded_folder/simple.tfvars @@ -0,0 +1,20 @@ +access_policy_config = { + access_policy_create = { + parent = "organizations/1234567890123" + title = "ShieldedMVP" + } +} +folder_config = { + folder_create = { + display_name = "ShieldedMVP" + parent = "organizations/1234567890123" + } +} +organization = { + domain = "example.com" + id = "1122334455" +} +prefix = "prefix" +project_config = { + billing_account_id = "123456-123456-123456" +} diff --git a/tests/blueprints/data_solutions/shielded_folder/simple.yaml b/tests/blueprints/data_solutions/shielded_folder/simple.yaml new file mode 100644 index 000000000..244dcb976 --- /dev/null +++ b/tests/blueprints/data_solutions/shielded_folder/simple.yaml @@ -0,0 +1,51 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.folder.google_compute_firewall_policy.policy["prefix-fw-policy"]: + short_name: prefix-fw-policy + module.folder.google_folder.folder[0]: + display_name: ShieldedMVP + parent: organizations/1234567890123 + module.log-export-project[0].google_project.project[0]: + billing_account: 123456-123456-123456 + project_id: prefix-audit-logs + module.vpc-sc[0].google_access_context_manager_access_policy.default[0]: + parent: organizations/1122334455 + title: shielded-folder + module.vpc-sc[0].google_access_context_manager_service_perimeter.regular["shielded"]: + description: null + perimeter_type: PERIMETER_TYPE_REGULAR + title: shielded + +counts: + google_access_context_manager_access_policy: 1 + google_access_context_manager_service_perimeter: 1 + google_bigquery_dataset: 1 + google_bigquery_dataset_iam_member: 2 + google_bigquery_default_service_account: 1 + google_compute_firewall_policy: 1 + google_compute_firewall_policy_rule: 4 + google_folder: 2 + google_folder_iam_binding: 2 + google_logging_folder_sink: 2 + google_org_policy_policy: 12 + google_project: 1 + google_project_iam_binding: 1 + google_project_service: 4 + google_project_service_identity: 1 + google_projects: 1 + google_storage_project_service_account: 1 + modules: 5 + resources: 38 diff --git a/tests/blueprints/data_solutions/shielded_folder/tftest.yaml b/tests/blueprints/data_solutions/shielded_folder/tftest.yaml new file mode 100644 index 000000000..3c0bcb8c4 --- /dev/null +++ b/tests/blueprints/data_solutions/shielded_folder/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: blueprints/data-solutions/shielded-folder + +tests: + simple: diff --git a/tests/fast/stages/s02_networking_nva/__init__.py b/tests/blueprints/data_solutions/vertex_mlops/__init__.py similarity index 100% rename from tests/fast/stages/s02_networking_nva/__init__.py rename to tests/blueprints/data_solutions/vertex_mlops/__init__.py diff --git a/tests/fast/stages/s03_project_factory/fixture/main.tf b/tests/blueprints/data_solutions/vertex_mlops/fixture/main.tf similarity index 52% rename from tests/fast/stages/s03_project_factory/fixture/main.tf rename to tests/blueprints/data_solutions/vertex_mlops/fixture/main.tf index 420cc2567..0b671f335 100644 --- a/tests/fast/stages/s03_project_factory/fixture/main.tf +++ b/tests/blueprints/data_solutions/vertex_mlops/fixture/main.tf @@ -15,18 +15,25 @@ */ module "projects" { - source = "../../../../../fast/stages/03-project-factory/dev" - data_dir = "./data/projects/" - defaults_file = "./data/defaults.yaml" - prefix = "test" - environment_dns_zone = "dev" - billing_account = { - id = "000000-111111-222222" - organization_id = 123456789012 + source = "../../../../../blueprints/data-solutions/vertex-mlops/" + labels = { + "env" : "dev", + "team" : "ml" } - vpc_self_links = { - dev-spoke-0 = "link" + bucket_name = "test-dev" + dataset_name = "test" + identity_pool_claims = "attribute.repository/ORGANIZATION/REPO" + notebooks = { + "myworkbench" : { + "owner" : "user@example.com", + "region" : "europe-west4", + "subnet" : "default", + } + } + prefix = "pref" + project_id = "test-dev" + project_create = { + billing_account_id = "000000-123456-123456" + parent = "folders/111111111111" } } - - diff --git a/tests/blueprints/data_solutions/vertex_mlops/test_plan.py b/tests/blueprints/data_solutions/vertex_mlops/test_plan.py new file mode 100644 index 000000000..eac30ad57 --- /dev/null +++ b/tests/blueprints/data_solutions/vertex_mlops/test_plan.py @@ -0,0 +1,23 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import pytest + +FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture') + +def test_resources(e2e_plan_runner): + "Test that plan works and the numbers of resources is as expected." + modules, resources = e2e_plan_runner(FIXTURES_DIR) + # TODO: to re-enable per-module resource count check print _, then test + assert len(modules) > 0 and len(resources) > 0 \ No newline at end of file diff --git a/tests/blueprints/factories/bigquery_factory/examples/simple.yaml b/tests/blueprints/factories/bigquery_factory/examples/simple.yaml new file mode 100644 index 000000000..d32492d6c --- /dev/null +++ b/tests/blueprints/factories/bigquery_factory/examples/simple.yaml @@ -0,0 +1,40 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.bq.module.bq["my_dataset"].google_bigquery_dataset.default: + dataset_id: my_dataset + project: project-id + module.bq.module.bq["my_dataset"].google_bigquery_table.default["countries"]: + dataset_id: my_dataset + friendly_name: countries + labels: + env: prod + project: project-id + schema: '[{"name":"country","type":"STRING"},{"name":"population","type":"INT64"}]' + table_id: countries + module.bq.module.bq["my_dataset"].google_bigquery_table.views["department"]: + dataset_id: my_dataset + friendly_name: department + labels: + env: prod + project: project-id + table_id: department + view: + - query: SELECT SUM(population) from my_dataset.countries + use_legacy_sql: false + +counts: + google_bigquery_dataset: 1 + google_bigquery_table: 2 diff --git a/tests/blueprints/factories/bigquery_factory/fixture/variables.tf b/tests/blueprints/factories/bigquery_factory/fixture/variables.tf deleted file mode 100644 index 8269dbbe1..000000000 --- a/tests/blueprints/factories/bigquery_factory/fixture/variables.tf +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -variable "views_dir" { - description = "Relative path for the folder storing view data." - type = string - default = "/views" -} - -variable "tables_dir" { - description = "Relative path for the folder storing table data." - type = string - default = "tables" -} - -variable "project_id" { - description = "Project ID" - type = string - default = "test-project" - -} diff --git a/tests/blueprints/gke/binauthz/test_plan.py b/tests/blueprints/gke/binauthz/test_plan.py index cf012c061..b4437b6f3 100644 --- a/tests/blueprints/gke/binauthz/test_plan.py +++ b/tests/blueprints/gke/binauthz/test_plan.py @@ -16,4 +16,4 @@ def test_resources(e2e_plan_runner): "Test that plan works and the numbers of resources is as expected." modules, resources = e2e_plan_runner() assert len(modules) == 13 - assert len(resources) == 43 + assert len(resources) == 44 diff --git a/tests/blueprints/gke/multi_cluster_mesh_gke_fleet_api/test_plan.py b/tests/blueprints/gke/multi_cluster_mesh_gke_fleet_api/test_plan.py index 270a142d1..2379849dc 100644 --- a/tests/blueprints/gke/multi_cluster_mesh_gke_fleet_api/test_plan.py +++ b/tests/blueprints/gke/multi_cluster_mesh_gke_fleet_api/test_plan.py @@ -16,4 +16,4 @@ def test_resources(e2e_plan_runner): "Test that plan works and the numbers of resources is as expected." modules, resources = e2e_plan_runner() assert len(modules) == 12 - assert len(resources) == 53 + assert len(resources) == 55 diff --git a/tests/blueprints/gke/multitenant_fleet/test_plan.py b/tests/blueprints/gke/multitenant_fleet/test_plan.py index 2b94b766f..c8a836949 100644 --- a/tests/blueprints/gke/multitenant_fleet/test_plan.py +++ b/tests/blueprints/gke/multitenant_fleet/test_plan.py @@ -17,4 +17,4 @@ def test_resources(e2e_plan_runner): "Test that plan works and the numbers of resources is as expected." modules, resources = e2e_plan_runner() assert len(modules) == 4 - assert len(resources) == 22 + assert len(resources) == 23 diff --git a/tests/blueprints/networking/onprem_google_access_dns/test_plan.py b/tests/blueprints/networking/onprem_google_access_dns/test_plan.py.disabled similarity index 100% rename from tests/blueprints/networking/onprem_google_access_dns/test_plan.py rename to tests/blueprints/networking/onprem_google_access_dns/test_plan.py.disabled diff --git a/tests/blueprints/networking/private_cloud_function_from_onprem/test_plan.py b/tests/blueprints/networking/private_cloud_function_from_onprem/test_plan.py index 2b3f8d7f7..81225db36 100644 --- a/tests/blueprints/networking/private_cloud_function_from_onprem/test_plan.py +++ b/tests/blueprints/networking/private_cloud_function_from_onprem/test_plan.py @@ -16,4 +16,4 @@ def test_resources(e2e_plan_runner): "Test that plan works and the numbers of resources is as expected." modules, resources = e2e_plan_runner() assert len(modules) == 10 - assert len(resources) == 38 + assert len(resources) == 39 diff --git a/tests/blueprints/serverless/api_gateway/test_plan.py b/tests/blueprints/serverless/api_gateway/test_plan.py index 6cf48a87b..9d658398e 100644 --- a/tests/blueprints/serverless/api_gateway/test_plan.py +++ b/tests/blueprints/serverless/api_gateway/test_plan.py @@ -16,4 +16,4 @@ def test_resources(e2e_plan_runner): "Test that plan works and the numbers of resources is as expected." modules, resources = e2e_plan_runner() assert len(modules) == 7 - assert len(resources) == 31 + assert len(resources) == 32 diff --git a/tests/examples/conftest.py b/tests/examples/conftest.py index b2b915ea1..4d3d85ee6 100644 --- a/tests/examples/conftest.py +++ b/tests/examples/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,50 +11,50 @@ # 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. +"""Pytest configuration for testing code examples.""" import collections import re from pathlib import Path import marko -import pytest FABRIC_ROOT = Path(__file__).parents[2] -MODULES_PATH = FABRIC_ROOT / 'modules/' -BLUEPRINTS_PATH = FABRIC_ROOT / 'blueprints/' -FILE_TEST_RE = re.compile(r'# tftest file (\w+) ([\S]+)') +FILE_TEST_RE = re.compile(r'# tftest-file +id=([\w_.-]+) +path=([\S]+)') -Example = collections.namedtuple('Example', 'code files') +Example = collections.namedtuple('Example', 'name code module files') File = collections.namedtuple('File', 'path content') def pytest_generate_tests(metafunc): + """Find all README.md files and collect code examples tagged for testing.""" if 'example' in metafunc.fixturenames: - modules = [x for x in MODULES_PATH.iterdir() if x.is_dir()] - modules.extend(x for x in BLUEPRINTS_PATH.glob("*/*") if x.is_dir()) - modules.sort() + readmes = FABRIC_ROOT.glob('**/README.md') examples = [] ids = [] - for module in modules: - readme = module / 'README.md' - if not readme.exists(): - continue + for readme in readmes: + module = readme.parent doc = marko.parse(readme.read_text()) index = 0 - last_header = None - files = {} + files = collections.defaultdict(dict) - #first pass: collect all tftest tagged files + # first pass: collect all examples tagged with tftest-file + last_header = None for child in doc.children: if isinstance(child, marko.block.FencedCode): code = child.children[0].children match = FILE_TEST_RE.search(code) if match: name, path = match.groups() - files[name] = File(path, code) + files[last_header][name] = File(path, code) + elif isinstance(child, marko.block.Heading): + last_header = child.children[0].children + # second pass: collect all examples tagged with tftest + last_header = None + index = 0 for child in doc.children: if isinstance(child, marko.block.FencedCode): index += 1 @@ -62,12 +62,12 @@ def pytest_generate_tests(metafunc): if 'tftest skip' in code: continue if child.lang == 'hcl': - examples.append(Example(code, files)) path = module.relative_to(FABRIC_ROOT) name = f'{path}:{last_header}' if index > 1: name += f' {index}' ids.append(name) + examples.append(Example(name, code, path, files[last_header])) elif isinstance(child, marko.block.Heading): last_header = child.children[0].children index = 0 diff --git a/tests/examples/test_plan.py b/tests/examples/test_plan.py index 2bb45af12..261276f73 100644 --- a/tests/examples/test_plan.py +++ b/tests/examples/test_plan.py @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,19 +13,24 @@ # limitations under the License. import re +import subprocess from pathlib import Path BASE_PATH = Path(__file__).parent -COUNT_TEST_RE = re.compile( - r'# tftest modules=(\d+) resources=(\d+)(?: files=([\w,]+))?') +COUNT_TEST_RE = re.compile(r'# tftest +modules=(\d+) +resources=(\d+)' + + r'(?: +files=([\w,_-]+))?' + + r'(?: +inventory=([\w\-.]+))?') -def test_example(recursive_e2e_plan_runner, tmp_path, example): +def test_example(plan_validator, tmp_path, example): if match := COUNT_TEST_RE.search(example.code): - (tmp_path / 'fabric').symlink_to(Path(BASE_PATH, '../../')) - (tmp_path / 'variables.tf').symlink_to(Path(BASE_PATH, 'variables.tf')) + (tmp_path / 'fabric').symlink_to(BASE_PATH.parents[1]) + (tmp_path / 'variables.tf').symlink_to(BASE_PATH / 'variables.tf') (tmp_path / 'main.tf').write_text(example.code) + expected_modules = int(match.group(1)) + expected_resources = int(match.group(2)) + if match.group(3) is not None: requested_files = match.group(3).split(',') for f in requested_files: @@ -33,13 +38,33 @@ def test_example(recursive_e2e_plan_runner, tmp_path, example): destination.parent.mkdir(parents=True, exist_ok=True) destination.write_text(example.files[f].content) - expected_modules = int(match.group(1)) if match is not None else 1 - expected_resources = int(match.group(2)) if match is not None else 1 + inventory = [] + if match.group(4) is not None: + python_test_path = str(example.module).replace('-', '_') + inventory = BASE_PATH.parent / python_test_path / 'examples' + inventory = inventory / match.group(4) - num_modules, num_resources = recursive_e2e_plan_runner( - str(tmp_path), tmpdir=False) + # TODO: force plan_validator to never copy files (we're already + # running from a temp dir) + summary = plan_validator(module_path=tmp_path, inventory_paths=inventory, + tf_var_files=[]) + + import yaml + print(yaml.dump({"values": summary.values})) + print(yaml.dump({"counts": summary.counts})) + print(yaml.dump({"outputs": summary.outputs})) + + counts = summary.counts + num_modules, num_resources = counts['modules'], counts['resources'] assert expected_modules == num_modules, 'wrong number of modules' assert expected_resources == num_resources, 'wrong number of resources' + # TODO(jccb): this should probably be done in check_documentation + # but we already have all the data here. + result = subprocess.run( + 'terraform fmt -check -diff -no-color main.tf'.split(), cwd=tmp_path, + stdout=subprocess.PIPE, encoding='utf-8') + assert result.returncode == 0, f'terraform code not formatted correctly\n{result.stdout}' + else: assert False, "can't find tftest directive" diff --git a/tests/examples/variables.tf b/tests/examples/variables.tf index 76c3770de..3a5a3f758 100644 --- a/tests/examples/variables.tf +++ b/tests/examples/variables.tf @@ -19,7 +19,7 @@ variable "bucket" { } variable "billing_account_id" { - default = "billing_account_id" + default = "123456-123456-123456" } variable "kms_key" { diff --git a/tests/fast/stages/s01_resman/fixture/main.tf b/tests/fast/stages/s01_resman/fixture/main.tf deleted file mode 100644 index 57d35b163..000000000 --- a/tests/fast/stages/s01_resman/fixture/main.tf +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module "stage" { - source = "../../../../../fast/stages/01-resman" - automation = { - federated_identity_pool = null - federated_identity_providers = null - project_id = "fast-prod-automation" - project_number = 123456 - outputs_bucket = "test" - } - billing_account = { - id = "000000-111111-222222" - organization_id = 123456789012 - } - custom_roles = { - # organization_iam_admin = "organizations/123456789012/roles/organizationIamAdmin", - service_project_network_admin = "organizations/123456789012/roles/xpnServiceAdmin" - } - groups = { - gcp-billing-admins = "gcp-billing-admins", - gcp-devops = "gcp-devops", - gcp-network-admins = "gcp-network-admins", - gcp-organization-admins = "gcp-organization-admins", - gcp-security-admins = "gcp-security-admins", - gcp-support = "gcp-support" - } - organization = { - domain = "fast.example.com" - id = 123456789012 - customer_id = "C00000000" - } - prefix = "fast2" -} diff --git a/tests/fast/stages/s02_networking_nva/fixture/main.tf b/tests/fast/stages/s02_networking_nva/fixture/main.tf deleted file mode 100644 index 2fc748014..000000000 --- a/tests/fast/stages/s02_networking_nva/fixture/main.tf +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module "stage" { - source = "../../../../../fast/stages/02-networking-nva" - data_dir = "../../../../../fast/stages/02-networking-nva/data/" - automation = { - outputs_bucket = "test" - } - billing_account = { - id = "000000-111111-222222" - organization_id = 123456789012 - } - custom_roles = { - service_project_network_admin = "organizations/123456789012/roles/foo" - } - folder_ids = { - networking = null - networking-dev = null - networking-prod = null - } - service_accounts = { - data-platform-dev = "string" - data-platform-prod = "string" - gke-dev = "string" - gke-prod = "string" - project-factory-dev = "string" - project-factory-prod = "string" - } - organization = { - domain = "fast.example.com" - id = 123456789012 - customer_id = "C00000000" - } - prefix = "fast2" -} diff --git a/tests/fast/stages/s02_networking_peering/fixture/main.tf b/tests/fast/stages/s02_networking_peering/fixture/main.tf deleted file mode 100644 index a2ed52474..000000000 --- a/tests/fast/stages/s02_networking_peering/fixture/main.tf +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module "stage" { - source = "../../../../../fast/stages/02-networking-peering" - data_dir = "../../../../../fast/stages/02-networking-peering/data/" - automation = { - outputs_bucket = "test" - } - billing_account = { - id = "000000-111111-222222" - organization_id = 123456789012 - } - custom_roles = { - service_project_network_admin = "organizations/123456789012/roles/foo" - } - folder_ids = { - networking = null - networking-dev = null - networking-prod = null - } - region_trigram = { - europe-west1 = "ew1" - europe-west3 = "ew3" - europe-west8 = "ew8" - } - service_accounts = { - data-platform-dev = "string" - data-platform-prod = "string" - gke-dev = "string" - gke-prod = "string" - project-factory-dev = "string" - project-factory-prod = "string" - } - organization = { - domain = "fast.example.com" - id = 123456789012 - customer_id = "C00000000" - } - prefix = "fast2" -} diff --git a/tests/fast/stages/s02_networking_separate_envs/fixture/main.tf b/tests/fast/stages/s02_networking_separate_envs/fixture/main.tf deleted file mode 100644 index 9ecef1ac0..000000000 --- a/tests/fast/stages/s02_networking_separate_envs/fixture/main.tf +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module "stage" { - source = "../../../../../fast/stages/02-networking-separate-envs" - data_dir = "../../../../../fast/stages/02-networking-separate-envs/data/" - automation = { - outputs_bucket = "test" - } - billing_account = { - id = "000000-111111-222222" - organization_id = 123456789012 - } - custom_roles = { - service_project_network_admin = "organizations/123456789012/roles/foo" - } - folder_ids = { - networking = null - networking-dev = null - networking-prod = null - } - service_accounts = { - data-platform-dev = "string" - data-platform-prod = "string" - project-factory-dev = "string" - project-factory-prod = "string" - } - organization = { - domain = "fast.example.com" - id = 123456789012 - customer_id = "C00000000" - } - prefix = "fast2" -} diff --git a/tests/fast/stages/s02_security/fixture/main.tf b/tests/fast/stages/s02_security/fixture/main.tf deleted file mode 100644 index d65805f9e..000000000 --- a/tests/fast/stages/s02_security/fixture/main.tf +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module "stage" { - source = "../../../../../fast/stages/02-security" - automation = { - outputs_bucket = "test" - } - billing_account = { - id = "000000-111111-222222" - organization_id = 123456789012 - } - folder_ids = { - security = null - } - organization = { - domain = "gcp-pso-italy.net" - id = 856933387836 - customer_id = "C01lmug8b" - } - prefix = "fast" - kms_keys = { - compute = { - iam = { - "roles/cloudkms.admin" = ["user:user1@example.com"] - } - labels = { service = "compute" } - locations = null - rotation_period = null - } - } - service_accounts = { - security = "foobar@iam.gserviceaccount.com" - data-platform-dev = "foobar@iam.gserviceaccount.com" - data-platform-prod = "foobar@iam.gserviceaccount.com" - project-factory-dev = "foobar@iam.gserviceaccount.com" - project-factory-prod = "foobar@iam.gserviceaccount.com" - } - vpc_sc_access_levels = { - onprem = { - conditions = [{ - ip_subnetworks = ["101.101.101.0/24"] - }] - } - } - vpc_sc_egress_policies = { - iac-gcs = { - from = { - identities = [ - "serviceAccount:xxx-prod-resman-security-0@xxx-prod-iac-core-0.iam.gserviceaccount.com" - ] - } - to = { - operations = [{ - method_selectors = ["*"] - service_name = "storage.googleapis.com" - }] - resources = ["projects/123456782"] - } - } - } - vpc_sc_ingress_policies = { - iac = { - from = { - identities = [ - "serviceAccount:xxx-prod-resman-security-0@xxx-prod-iac-core-0.iam.gserviceaccount.com" - ] - access_levels = ["*"] - } - to = { - operations = [{ method_selectors = [], service_name = "*" }] - resources = ["*"] - } - } - } - vpc_sc_perimeters = { - dev = { - egress_policies = ["iac-gcs"] - ingress_policies = ["iac"] - resources = ["projects/1111111111"] - } - dev = { - egress_policies = ["iac-gcs"] - ingress_policies = ["iac"] - resources = ["projects/0000000000"] - } - dev = { - access_levels = ["onprem"] - egress_policies = ["iac-gcs"] - ingress_policies = ["iac"] - resources = ["projects/2222222222"] - } - } -} diff --git a/tests/fast/stages/s02_security/test_plan.py b/tests/fast/stages/s02_security/test_plan.py deleted file mode 100644 index 5f1058065..000000000 --- a/tests/fast/stages/s02_security/test_plan.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -def test_counts(recursive_e2e_plan_runner): - "Test stage." - num_modules, num_resources = recursive_e2e_plan_runner() - # TODO: to re-enable per-module resource count check print _, then test - assert num_modules > 0 and num_resources > 0 diff --git a/tests/fast/stages/s03_data_platform/fixture/main.tf b/tests/fast/stages/s03_data_platform/fixture/main.tf deleted file mode 100644 index 0e356d222..000000000 --- a/tests/fast/stages/s03_data_platform/fixture/main.tf +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -# tfdoc: Data platform stage test - -module "stage" { - source = "../../../../../fast/stages/03-data-platform/dev/" - automation = { - outputs_bucket = "test" - } - billing_account = { - id = "012345-67890A-BCDEF0", - organization_id = 123456 - } - folder_ids = { - data-platform-dev = "folders/12345678" - } - host_project_ids = { - dev-spoke-0 = "fast-dev-net-spoke-0" - } - organization = { - domain = "example.com" - id = 123456789012 - customer_id = "A11aaaaa1" - } - prefix = "fast" - subnet_self_links = { - dev-spoke-0 = { - "europe-west1/dev-dataplatform-ew1" : "https://www.googleapis.com/compute/v1/projects/fast-dev-net-spoke-0/regions/europe-west1/subnetworks/dev-dataplatform-ew1", - "europe-west1/dev-default-ew1" : "https://www.googleapis.com/compute/v1/projects/fast-dev-net-spoke-0/regions/europe-west1/subnetworks/dev-default-ew1" - } - } - vpc_self_links = { dev-spoke-0 = "https://www.googleapis.com/compute/v1/projects/fast-dev-net-spoke-0/global/networks/dev-spoke-0" } -} diff --git a/tests/fast/stages/s03_data_platform/test_plan.py b/tests/fast/stages/s03_data_platform/test_plan.py deleted file mode 100644 index 5f1058065..000000000 --- a/tests/fast/stages/s03_data_platform/test_plan.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -def test_counts(recursive_e2e_plan_runner): - "Test stage." - num_modules, num_resources = recursive_e2e_plan_runner() - # TODO: to re-enable per-module resource count check print _, then test - assert num_modules > 0 and num_resources > 0 diff --git a/tests/fast/stages/s03_gke_multitenant/fixture/main.tf b/tests/fast/stages/s03_gke_multitenant/fixture/main.tf deleted file mode 100644 index b69951bad..000000000 --- a/tests/fast/stages/s03_gke_multitenant/fixture/main.tf +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -# tfdoc: Data platform stage test - -module "stage" { - source = "../../../../../fast/stages/03-gke-multitenant/dev/" - automation = { - outputs_bucket = "test" - } - billing_account = { - id = "012345-67890A-BCDEF0", - organization_id = 123456 - } - clusters = { - mycluster = { - cluster_autoscaling = null - description = "my cluster" - dns_domain = null - location = "europe-west1" - labels = {} - private_cluster_config = { - enable_private_endpoint = true - master_global_access = true - } - vpc_config = { - subnetwork = "projects/prj-host/regions/europe-west1/subnetworks/gke-0" - master_ipv4_cidr_block = "172.16.20.0/28" - } - } - } - nodepools = { - mycluster = { - mynodepool = { - node_count = { initial = 1 } - } - } - } - folder_ids = { - gke-dev = "folders/12345678" - } - host_project_ids = { - dev-spoke-0 = "fast-dev-net-spoke-0" - } - prefix = "fast" - vpc_self_links = { - dev-spoke-0 = "projects/fast-dev-net-spoke-0/global/networks/dev-spoke-0" - } -} diff --git a/tests/fast/stages/s03_gke_multitenant/test_plan.py b/tests/fast/stages/s03_gke_multitenant/test_plan.py deleted file mode 100644 index 5f1058065..000000000 --- a/tests/fast/stages/s03_gke_multitenant/test_plan.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -def test_counts(recursive_e2e_plan_runner): - "Test stage." - num_modules, num_resources = recursive_e2e_plan_runner() - # TODO: to re-enable per-module resource count check print _, then test - assert num_modules > 0 and num_resources > 0 diff --git a/tests/fast/stages/s03_project_factory/test_plan.py b/tests/fast/stages/s03_project_factory/test_plan.py deleted file mode 100644 index 5f1058065..000000000 --- a/tests/fast/stages/s03_project_factory/test_plan.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -def test_counts(recursive_e2e_plan_runner): - "Test stage." - num_modules, num_resources = recursive_e2e_plan_runner() - # TODO: to re-enable per-module resource count check print _, then test - assert num_modules > 0 and num_resources > 0 diff --git a/tests/fast/stages/s02_networking_peering/__init__.py b/tests/fast/stages/s0_bootstrap/__init__.py similarity index 100% rename from tests/fast/stages/s02_networking_peering/__init__.py rename to tests/fast/stages/s0_bootstrap/__init__.py diff --git a/tests/fast/stages/s00_bootstrap/simple.tfvars b/tests/fast/stages/s0_bootstrap/simple.tfvars similarity index 71% rename from tests/fast/stages/s00_bootstrap/simple.tfvars rename to tests/fast/stages/s0_bootstrap/simple.tfvars index f8ef5735b..5c389f53a 100644 --- a/tests/fast/stages/s00_bootstrap/simple.tfvars +++ b/tests/fast/stages/s0_bootstrap/simple.tfvars @@ -4,8 +4,7 @@ organization = { customer_id = "C00000000" } billing_account = { - id = "000000-111111-222222" - organization_id = 123456789012 + id = "000000-111111-222222" } prefix = "fast" outputs_location = "/fast-config" diff --git a/tests/fast/stages/s00_bootstrap/simple.yaml b/tests/fast/stages/s0_bootstrap/simple.yaml similarity index 77% rename from tests/fast/stages/s00_bootstrap/simple.yaml rename to tests/fast/stages/s0_bootstrap/simple.yaml index 703b84b45..63a64cb9e 100644 --- a/tests/fast/stages/s00_bootstrap/simple.yaml +++ b/tests/fast/stages/s0_bootstrap/simple.yaml @@ -13,23 +13,22 @@ # limitations under the License. counts: - google_bigquery_dataset: 2 - google_bigquery_dataset_iam_member: 2 + google_bigquery_dataset: 1 google_bigquery_default_service_account: 3 google_logging_organization_sink: 2 google_organization_iam_binding: 19 - google_organization_iam_custom_role: 2 + google_organization_iam_custom_role: 3 google_organization_iam_member: 16 google_project: 3 google_project_iam_binding: 9 - google_project_iam_member: 1 + google_project_iam_member: 3 google_project_service: 29 - google_project_service_identity: 2 - google_service_account: 3 - google_service_account_iam_binding: 3 - google_storage_bucket: 4 - google_storage_bucket_iam_binding: 2 - google_storage_bucket_iam_member: 3 + google_project_service_identity: 3 + google_service_account: 2 + google_service_account_iam_binding: 1 + google_storage_bucket: 3 + google_storage_bucket_iam_binding: 1 + google_storage_bucket_iam_member: 2 google_storage_bucket_object: 5 google_storage_project_service_account: 3 local_file: 5 @@ -38,6 +37,7 @@ outputs: custom_roles: organization_iam_admin: organizations/123456789012/roles/organizationIamAdmin service_project_network_admin: organizations/123456789012/roles/serviceProjectNetworkAdmin + tenant_network_admin: organizations/123456789012/roles/tenantNetworkAdmin outputs_bucket: fast-prod-iac-core-outputs-0 project_ids: automation: fast-prod-iac-core-0 @@ -45,5 +45,4 @@ outputs: log-export: fast-prod-audit-logs-0 service_accounts: bootstrap: fast-prod-bootstrap-0@fast-prod-iac-core-0.iam.gserviceaccount.com - cicd: fast-prod-cicd-0@fast-prod-iac-core-0.iam.gserviceaccount.com resman: fast-prod-resman-0@fast-prod-iac-core-0.iam.gserviceaccount.com diff --git a/tests/fast/stages/s00_bootstrap/simple_projects.yaml b/tests/fast/stages/s0_bootstrap/simple_projects.yaml similarity index 100% rename from tests/fast/stages/s00_bootstrap/simple_projects.yaml rename to tests/fast/stages/s0_bootstrap/simple_projects.yaml diff --git a/tests/fast/stages/s00_bootstrap/simple_sas.yaml b/tests/fast/stages/s0_bootstrap/simple_sas.yaml similarity index 82% rename from tests/fast/stages/s00_bootstrap/simple_sas.yaml rename to tests/fast/stages/s0_bootstrap/simple_sas.yaml index ba84948d8..0424e5983 100644 --- a/tests/fast/stages/s00_bootstrap/simple_sas.yaml +++ b/tests/fast/stages/s0_bootstrap/simple_sas.yaml @@ -17,10 +17,6 @@ values: account_id: fast-prod-bootstrap-0 display_name: Terraform organization bootstrap service account. project: fast-prod-iac-core-0 - module.automation-tf-cicd-provisioning-sa.google_service_account.service_account[0]: - account_id: fast-prod-cicd-0 - display_name: Terraform stage 1 CICD service account. - project: fast-prod-iac-core-0 module.automation-tf-resman-sa.google_service_account.service_account[0]: account_id: fast-prod-resman-0 display_name: Terraform stage 1 resman service account. diff --git a/tests/fast/stages/s00_bootstrap/tftest.yaml b/tests/fast/stages/s0_bootstrap/tftest.yaml similarity index 83% rename from tests/fast/stages/s00_bootstrap/tftest.yaml rename to tests/fast/stages/s0_bootstrap/tftest.yaml index 4656859bc..b53749adc 100644 --- a/tests/fast/stages/s00_bootstrap/tftest.yaml +++ b/tests/fast/stages/s0_bootstrap/tftest.yaml @@ -1,6 +1,6 @@ # skip boilerplate check -module: fast/stages/00-bootstrap +module: fast/stages/0-bootstrap tests: simple: diff --git a/tests/fast/stages/s02_networking_separate_envs/__init__.py b/tests/fast/stages/s1_resman/__init__.py similarity index 100% rename from tests/fast/stages/s02_networking_separate_envs/__init__.py rename to tests/fast/stages/s1_resman/__init__.py diff --git a/tests/fast/stages/s1_resman/common.tfvars b/tests/fast/stages/s1_resman/common.tfvars new file mode 100644 index 000000000..34c61351e --- /dev/null +++ b/tests/fast/stages/s1_resman/common.tfvars @@ -0,0 +1,28 @@ +automation = { + federated_identity_pool = null + federated_identity_providers = null + project_id = "fast-prod-automation" + project_number = 123456 + outputs_bucket = "test" +} +billing_account = { + id = "000000-111111-222222" +} +custom_roles = { + # organization_iam_admin = "organizations/123456789012/roles/organizationIamAdmin", + service_project_network_admin = "organizations/123456789012/roles/xpnServiceAdmin" +} +groups = { + gcp-billing-admins = "gcp-billing-admins", + gcp-devops = "gcp-devops", + gcp-network-admins = "gcp-network-admins", + gcp-organization-admins = "gcp-organization-admins", + gcp-security-admins = "gcp-security-admins", + gcp-support = "gcp-support" +} +organization = { + domain = "fast.example.com" + id = 123456789012 + customer_id = "C00000000" +} +prefix = "fast2" diff --git a/tests/fast/stages/s02_networking_separate_envs/test_plan.py b/tests/fast/stages/s1_resman/test_plan.py similarity index 72% rename from tests/fast/stages/s02_networking_separate_envs/test_plan.py rename to tests/fast/stages/s1_resman/test_plan.py index 1e697dc98..39bdfc11e 100644 --- a/tests/fast/stages/s02_networking_separate_envs/test_plan.py +++ b/tests/fast/stages/s1_resman/test_plan.py @@ -13,8 +13,9 @@ # limitations under the License. -def test_counts(recursive_e2e_plan_runner): +def test_counts(plan_summary): "Test stage." - num_modules, num_resources = recursive_e2e_plan_runner() - # TODO: to re-enable per-module resource count check print _, then test - assert num_modules > 0 and num_resources > 0 \ No newline at end of file + summary = plan_summary("fast/stages/1-resman", + tf_var_files=["common.tfvars"]) + assert summary.counts["modules"] > 0 + assert summary.counts["resources"] > 0 diff --git a/tests/fast/stages/s02_networking_vpn/__init__.py b/tests/fast/stages/s2_networking_a_peering/__init__.py similarity index 100% rename from tests/fast/stages/s02_networking_vpn/__init__.py rename to tests/fast/stages/s2_networking_a_peering/__init__.py diff --git a/tests/fast/stages/s2_networking_a_peering/common.tfvars b/tests/fast/stages/s2_networking_a_peering/common.tfvars new file mode 100644 index 000000000..6c2b0c030 --- /dev/null +++ b/tests/fast/stages/s2_networking_a_peering/common.tfvars @@ -0,0 +1,29 @@ +data_dir = "../../../fast/stages/2-networking-a-peering/data/" +automation = { + outputs_bucket = "test" +} +billing_account = { + id = "000000-111111-222222" +} +custom_roles = { + service_project_network_admin = "organizations/123456789012/roles/foo" +} +folder_ids = { + networking = null + networking-dev = null + networking-prod = null +} +service_accounts = { + data-platform-dev = "string" + data-platform-prod = "string" + gke-dev = "string" + gke-prod = "string" + project-factory-dev = "string" + project-factory-prod = "string" +} +organization = { + domain = "fast.example.com" + id = 123456789012 + customer_id = "C00000000" +} +prefix = "fast2" diff --git a/tests/fast/stages/s02_networking_peering/test_plan.py b/tests/fast/stages/s2_networking_a_peering/test_plan.py similarity index 66% rename from tests/fast/stages/s02_networking_peering/test_plan.py rename to tests/fast/stages/s2_networking_a_peering/test_plan.py index 917c90c15..09590d361 100644 --- a/tests/fast/stages/s02_networking_peering/test_plan.py +++ b/tests/fast/stages/s2_networking_a_peering/test_plan.py @@ -20,31 +20,34 @@ from deepdiff import DeepDiff BASEDIR = Path(__file__).parent FIXTURE_PEERING = BASEDIR / 'fixture' -FIXTURE_VPN = BASEDIR.parent / 's02_networking_vpn/fixture' +FIXTURE_VPN = BASEDIR.parent / 's2_networking_b_vpn/fixture' STAGES = Path(__file__).parents[4] / 'fast/stages' -STAGE_PEERING = STAGES / '02-networking-peering' -STAGE_VPN = STAGES / '02-networking-vpn' +STAGE_PEERING = STAGES / '2-networking-a-peering' +STAGE_VPN = STAGES / '2-networking-b-vpn' -def test_counts(recursive_e2e_plan_runner): - 'Test stage.' - num_modules, num_resources = recursive_e2e_plan_runner() - # TODO: to re-enable per-module resource count check print _, then test - assert num_modules > 0 and num_resources > 0 +def test_counts(plan_summary): + "Test stage." + summary = plan_summary("fast/stages/2-networking-a-peering", + tf_var_files=["common.tfvars"]) + assert summary.counts["modules"] > 0 + assert summary.counts["resources"] > 0 -def test_vpn_peering_parity(e2e_plan_runner): +def test_vpn_peering_parity(plan_summary): '''Ensure VPN- and peering-based networking stages are identical except for VPN and VPC peering resources''' - _, plan_peering = e2e_plan_runner(fixture_path=FIXTURE_PEERING) - _, plan_vpn = e2e_plan_runner(fixture_path=FIXTURE_VPN) + summary_peering = plan_summary("fast/stages/2-networking-a-peering", + tf_var_files=["common.tfvars"]) + summary_vpn = plan_summary("fast/stages/2-networking-b-vpn", + tf_var_files=["common.tfvars"]) - ddiff = DeepDiff(plan_vpn, plan_peering, ignore_order=True, - group_by='address', view='tree') + ddiff = DeepDiff(summary_vpn.values, summary_peering.values, + ignore_order=True) - removed_types = {x.t1['type'] for x in ddiff['dictionary_item_removed']} - added_types = {x.t2['type'] for x in ddiff['dictionary_item_added']} + removed_types = {x.split('.')[-2] for x in ddiff['dictionary_item_removed']} + added_types = {x.split('.')[-2] for x in ddiff['dictionary_item_added']} assert added_types == {'google_compute_network_peering'} assert removed_types == { diff --git a/tests/fast/stages/s02_security/__init__.py b/tests/fast/stages/s2_networking_b_vpn/__init__.py similarity index 100% rename from tests/fast/stages/s02_security/__init__.py rename to tests/fast/stages/s2_networking_b_vpn/__init__.py diff --git a/tests/fast/stages/s2_networking_b_vpn/common.tfvars b/tests/fast/stages/s2_networking_b_vpn/common.tfvars new file mode 100644 index 000000000..66a7a6090 --- /dev/null +++ b/tests/fast/stages/s2_networking_b_vpn/common.tfvars @@ -0,0 +1,34 @@ +data_dir = "../../../../../fast/stages/2-networking-b-vpn/data/" +automation = { + outputs_bucket = "test" +} +billing_account = { + id = "000000-111111-222222" +} +custom_roles = { + service_project_network_admin = "organizations/123456789012/roles/foo" +} +folder_ids = { + networking = null + networking-dev = null + networking-prod = null +} +region_trigram = { + europe-west1 = "ew1" + europe-west3 = "ew3" + europe-west8 = "ew8" +} +service_accounts = { + data-platform-dev = "string" + data-platform-prod = "string" + gke-dev = "string" + gke-prod = "string" + project-factory-dev = "string" + project-factory-prod = "string" +} +organization = { + domain = "fast.example.com" + id = 123456789012 + customer_id = "C00000000" +} +prefix = "fast2" diff --git a/tests/fast/stages/s02_networking_vpn/fixture/main.tf b/tests/fast/stages/s2_networking_b_vpn/fixture/main.tf similarity index 100% rename from tests/fast/stages/s02_networking_vpn/fixture/main.tf rename to tests/fast/stages/s2_networking_b_vpn/fixture/main.tf diff --git a/tests/fast/stages/s02_networking_nva/test_plan.py b/tests/fast/stages/s2_networking_b_vpn/test_plan.py similarity index 72% rename from tests/fast/stages/s02_networking_nva/test_plan.py rename to tests/fast/stages/s2_networking_b_vpn/test_plan.py index 5f1058065..8ac1bade1 100644 --- a/tests/fast/stages/s02_networking_nva/test_plan.py +++ b/tests/fast/stages/s2_networking_b_vpn/test_plan.py @@ -13,8 +13,9 @@ # limitations under the License. -def test_counts(recursive_e2e_plan_runner): +def test_counts(plan_summary): "Test stage." - num_modules, num_resources = recursive_e2e_plan_runner() - # TODO: to re-enable per-module resource count check print _, then test - assert num_modules > 0 and num_resources > 0 + summary = plan_summary("fast/stages/2-networking-b-vpn", + tf_var_files=["common.tfvars"]) + assert summary.counts["modules"] > 0 + assert summary.counts["resources"] > 0 diff --git a/tests/fast/stages/s03_data_platform/__init__.py b/tests/fast/stages/s2_networking_c_nva/__init__.py similarity index 100% rename from tests/fast/stages/s03_data_platform/__init__.py rename to tests/fast/stages/s2_networking_c_nva/__init__.py diff --git a/tests/fast/stages/s2_networking_c_nva/common.tfvars b/tests/fast/stages/s2_networking_c_nva/common.tfvars new file mode 100644 index 000000000..ad12b8d33 --- /dev/null +++ b/tests/fast/stages/s2_networking_c_nva/common.tfvars @@ -0,0 +1,29 @@ +data_dir = "../../../fast/stages/2-networking-c-nva/data/" +automation = { + outputs_bucket = "test" +} +billing_account = { + id = "000000-111111-222222" +} +custom_roles = { + service_project_network_admin = "organizations/123456789012/roles/foo" +} +folder_ids = { + networking = null + networking-dev = null + networking-prod = null +} +service_accounts = { + data-platform-dev = "string" + data-platform-prod = "string" + gke-dev = "string" + gke-prod = "string" + project-factory-dev = "string" + project-factory-prod = "string" +} +organization = { + domain = "fast.example.com" + id = 123456789012 + customer_id = "C00000000" +} +prefix = "fast2" diff --git a/tests/fast/stages/s02_networking_vpn/test_plan.py b/tests/fast/stages/s2_networking_c_nva/test_plan.py similarity index 72% rename from tests/fast/stages/s02_networking_vpn/test_plan.py rename to tests/fast/stages/s2_networking_c_nva/test_plan.py index 5f1058065..70c37bc38 100644 --- a/tests/fast/stages/s02_networking_vpn/test_plan.py +++ b/tests/fast/stages/s2_networking_c_nva/test_plan.py @@ -13,8 +13,9 @@ # limitations under the License. -def test_counts(recursive_e2e_plan_runner): +def test_counts(plan_summary): "Test stage." - num_modules, num_resources = recursive_e2e_plan_runner() - # TODO: to re-enable per-module resource count check print _, then test - assert num_modules > 0 and num_resources > 0 + summary = plan_summary("fast/stages/2-networking-c-nva", + tf_var_files=["common.tfvars"]) + assert summary.counts["modules"] > 0 + assert summary.counts["resources"] > 0 diff --git a/tests/fast/stages/s03_gke_multitenant/__init__.py b/tests/fast/stages/s2_networking_d_separate_envs/__init__.py similarity index 100% rename from tests/fast/stages/s03_gke_multitenant/__init__.py rename to tests/fast/stages/s2_networking_d_separate_envs/__init__.py diff --git a/tests/fast/stages/s2_networking_d_separate_envs/common.tfvars b/tests/fast/stages/s2_networking_d_separate_envs/common.tfvars new file mode 100644 index 000000000..3ff0020ac --- /dev/null +++ b/tests/fast/stages/s2_networking_d_separate_envs/common.tfvars @@ -0,0 +1,27 @@ +data_dir = "../../../../../fast/stages/2-networking-d-separate-envs/data/" +automation = { + outputs_bucket = "test" +} +billing_account = { + id = "000000-111111-222222" +} +custom_roles = { + service_project_network_admin = "organizations/123456789012/roles/foo" +} +folder_ids = { + networking = null + networking-dev = null + networking-prod = null +} +service_accounts = { + data-platform-dev = "string" + data-platform-prod = "string" + project-factory-dev = "string" + project-factory-prod = "string" +} +organization = { + domain = "fast.example.com" + id = 123456789012 + customer_id = "C00000000" +} +prefix = "fast2" diff --git a/tests/fast/stages/s2_networking_d_separate_envs/test_plan.py b/tests/fast/stages/s2_networking_d_separate_envs/test_plan.py new file mode 100644 index 000000000..51a9257af --- /dev/null +++ b/tests/fast/stages/s2_networking_d_separate_envs/test_plan.py @@ -0,0 +1,21 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_counts(plan_summary): + "Test stage." + summary = plan_summary("fast/stages/2-networking-d-separate-envs", + tf_var_files=["common.tfvars"]) + assert summary.counts["modules"] > 0 + assert summary.counts["resources"] > 0 diff --git a/tests/fast/stages/s03_project_factory/__init__.py b/tests/fast/stages/s2_security/__init__.py similarity index 100% rename from tests/fast/stages/s03_project_factory/__init__.py rename to tests/fast/stages/s2_security/__init__.py diff --git a/tests/fast/stages/s2_security/common.tfvars b/tests/fast/stages/s2_security/common.tfvars new file mode 100644 index 000000000..6fbb60b64 --- /dev/null +++ b/tests/fast/stages/s2_security/common.tfvars @@ -0,0 +1,87 @@ +automation = { + outputs_bucket = "test" +} +billing_account = { + id = "000000-111111-222222" +} +folder_ids = { + security = null +} +organization = { + domain = "fast.example.com" + id = 123456789012 + customer_id = "C00000000" +} +prefix = "fast" +kms_keys = { + compute = { + iam = { + "roles/cloudkms.admin" = ["user:user1@example.com"] + } + labels = { service = "compute" } + locations = null + rotation_period = null + } +} +service_accounts = { + security = "foobar@iam.gserviceaccount.com" + data-platform-dev = "foobar@iam.gserviceaccount.com" + data-platform-prod = "foobar@iam.gserviceaccount.com" + project-factory-dev = "foobar@iam.gserviceaccount.com" + project-factory-prod = "foobar@iam.gserviceaccount.com" +} +vpc_sc_access_levels = { + onprem = { + conditions = [{ + ip_subnetworks = ["101.101.101.0/24"] + }] + } +} +vpc_sc_egress_policies = { + iac-gcs = { + from = { + identities = [ + "serviceAccount:xxx-prod-resman-security-0@xxx-prod-iac-core-0.iam.gserviceaccount.com" + ] + } + to = { + operations = [{ + method_selectors = ["*"] + service_name = "storage.googleapis.com" + }] + resources = ["projects/123456782"] + } + } +} +vpc_sc_ingress_policies = { + iac = { + from = { + identities = [ + "serviceAccount:xxx-prod-resman-security-0@xxx-prod-iac-core-0.iam.gserviceaccount.com" + ] + access_levels = ["*"] + } + to = { + operations = [{ method_selectors = [], service_name = "*" }] + resources = ["*"] + } + } +} +vpc_sc_perimeters = { + dev = { + egress_policies = ["iac-gcs"] + ingress_policies = ["iac"] + resources = ["projects/1111111111"] + } + dev = { + egress_policies = ["iac-gcs"] + ingress_policies = ["iac"] + resources = ["projects/0000000000"] + } + dev = { + access_levels = ["onprem"] + egress_policies = ["iac-gcs"] + ingress_policies = ["iac"] + resources = ["projects/2222222222"] + } +} diff --git a/tests/fast/stages/s01_resman/test_plan.py b/tests/fast/stages/s2_security/test_plan.py similarity index 72% rename from tests/fast/stages/s01_resman/test_plan.py rename to tests/fast/stages/s2_security/test_plan.py index 5f1058065..edf5622e7 100644 --- a/tests/fast/stages/s01_resman/test_plan.py +++ b/tests/fast/stages/s2_security/test_plan.py @@ -13,8 +13,9 @@ # limitations under the License. -def test_counts(recursive_e2e_plan_runner): +def test_counts(plan_summary): "Test stage." - num_modules, num_resources = recursive_e2e_plan_runner() - # TODO: to re-enable per-module resource count check print _, then test - assert num_modules > 0 and num_resources > 0 + summary = plan_summary("fast/stages/2-security", + tf_var_files=["common.tfvars"]) + assert summary.counts["modules"] > 0 + assert summary.counts["resources"] > 0 diff --git a/tests/modules/api_gateway/__init__.py b/tests/fast/stages/s3_data_platform/__init__.py similarity index 100% rename from tests/modules/api_gateway/__init__.py rename to tests/fast/stages/s3_data_platform/__init__.py diff --git a/tests/fast/stages/s3_data_platform/common.tfvars b/tests/fast/stages/s3_data_platform/common.tfvars new file mode 100644 index 000000000..2ec41d37a --- /dev/null +++ b/tests/fast/stages/s3_data_platform/common.tfvars @@ -0,0 +1,25 @@ +automation = { + outputs_bucket = "test" +} +billing_account = { + id = "012345-67890A-BCDEF0", +} +folder_ids = { + data-platform-dev = "folders/12345678" +} +host_project_ids = { + dev-spoke-0 = "fast-dev-net-spoke-0" +} +organization = { + domain = "fast.example.com" + id = 123456789012 + customer_id = "C00000000" +} +prefix = "fast" +subnet_self_links = { + dev-spoke-0 = { + "europe-west1/dev-dataplatform-ew1" : "https://www.googleapis.com/compute/v1/projects/fast-dev-net-spoke-0/regions/europe-west1/subnetworks/dev-dataplatform-ew1", + "europe-west1/dev-default-ew1" : "https://www.googleapis.com/compute/v1/projects/fast-dev-net-spoke-0/regions/europe-west1/subnetworks/dev-default-ew1" + } +} +vpc_self_links = { dev-spoke-0 = "https://www.googleapis.com/compute/v1/projects/fast-dev-net-spoke-0/global/networks/dev-spoke-0" } diff --git a/tests/fast/stages/s3_data_platform/test_plan.py b/tests/fast/stages/s3_data_platform/test_plan.py new file mode 100644 index 000000000..ad7fa3d28 --- /dev/null +++ b/tests/fast/stages/s3_data_platform/test_plan.py @@ -0,0 +1,21 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_counts(plan_summary): + "Test stage." + summary = plan_summary("fast/stages/3-data-platform/dev/", + tf_var_files=["common.tfvars"]) + assert summary.counts["modules"] > 0 + assert summary.counts["resources"] > 0 diff --git a/tests/modules/compute_vm/__init__.py b/tests/fast/stages/s3_gke_multitenant/__init__.py similarity index 100% rename from tests/modules/compute_vm/__init__.py rename to tests/fast/stages/s3_gke_multitenant/__init__.py diff --git a/tests/fast/stages/s3_gke_multitenant/common.tfvars b/tests/fast/stages/s3_gke_multitenant/common.tfvars new file mode 100644 index 000000000..1cafdd9ab --- /dev/null +++ b/tests/fast/stages/s3_gke_multitenant/common.tfvars @@ -0,0 +1,40 @@ +automation = { + outputs_bucket = "test" +} +billing_account = { + id = "012345-67890A-BCDEF0", +} +clusters = { + mycluster = { + cluster_autoscaling = null + description = "my cluster" + dns_domain = null + location = "europe-west1" + labels = {} + private_cluster_config = { + enable_private_endpoint = true + master_global_access = true + } + vpc_config = { + subnetwork = "projects/prj-host/regions/europe-west1/subnetworks/gke-0" + master_ipv4_cidr_block = "172.16.20.0/28" + } + } +} +nodepools = { + mycluster = { + mynodepool = { + node_count = { initial = 1 } + } + } +} +folder_ids = { + gke-dev = "folders/12345678" +} +host_project_ids = { + dev-spoke-0 = "fast-dev-net-spoke-0" +} +prefix = "fast" +vpc_self_links = { + dev-spoke-0 = "projects/fast-dev-net-spoke-0/global/networks/dev-spoke-0" +} diff --git a/tests/fast/stages/s3_gke_multitenant/test_plan.py b/tests/fast/stages/s3_gke_multitenant/test_plan.py new file mode 100644 index 000000000..c517cb933 --- /dev/null +++ b/tests/fast/stages/s3_gke_multitenant/test_plan.py @@ -0,0 +1,21 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_counts(plan_summary): + "Test stage." + summary = plan_summary("fast/stages/3-gke-multitenant/dev/", + tf_var_files=["common.tfvars"]) + assert summary.counts["modules"] > 0 + assert summary.counts["resources"] > 0 diff --git a/tests/modules/dns/__init__.py b/tests/fast/stages/s3_project_factory/__init__.py similarity index 100% rename from tests/modules/dns/__init__.py rename to tests/fast/stages/s3_project_factory/__init__.py diff --git a/tests/fast/stages/s3_project_factory/common.tfvars b/tests/fast/stages/s3_project_factory/common.tfvars new file mode 100644 index 000000000..d3f8c6f9a --- /dev/null +++ b/tests/fast/stages/s3_project_factory/common.tfvars @@ -0,0 +1,10 @@ +data_dir = "../../../../tests/fast/stages/s3_project_factory/data/projects/" +defaults_file = "../../../../tests/fast/stages/s3_project_factory/data/defaults.yaml" +prefix = "test" +environment_dns_zone = "dev" +billing_account = { + id = "000000-111111-222222" +} +vpc_self_links = { + dev-spoke-0 = "link" +} diff --git a/tests/fast/stages/s03_project_factory/fixture/data/defaults.yaml b/tests/fast/stages/s3_project_factory/data/defaults.yaml similarity index 100% rename from tests/fast/stages/s03_project_factory/fixture/data/defaults.yaml rename to tests/fast/stages/s3_project_factory/data/defaults.yaml diff --git a/tests/fast/stages/s03_project_factory/fixture/data/projects/project.yaml b/tests/fast/stages/s3_project_factory/data/projects/project.yaml similarity index 100% rename from tests/fast/stages/s03_project_factory/fixture/data/projects/project.yaml rename to tests/fast/stages/s3_project_factory/data/projects/project.yaml diff --git a/tests/fast/stages/s3_project_factory/test_plan.py b/tests/fast/stages/s3_project_factory/test_plan.py new file mode 100644 index 000000000..fa293da84 --- /dev/null +++ b/tests/fast/stages/s3_project_factory/test_plan.py @@ -0,0 +1,21 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_counts(plan_summary): + "Test stage." + summary = plan_summary("fast/stages/3-project-factory/dev", + tf_var_files=["common.tfvars"]) + assert summary.counts["modules"] > 0 + assert summary.counts["resources"] > 0 diff --git a/tests/modules/gcs/__init__.py b/tests/fast/stages_multitenant/__init__.py similarity index 100% rename from tests/modules/gcs/__init__.py rename to tests/fast/stages_multitenant/__init__.py diff --git a/tests/modules/gke_cluster/__init__.py b/tests/fast/stages_multitenant/s0_bootstrap_tenant/__init__.py similarity index 100% rename from tests/modules/gke_cluster/__init__.py rename to tests/fast/stages_multitenant/s0_bootstrap_tenant/__init__.py diff --git a/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.tfvars b/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.tfvars new file mode 100644 index 000000000..52ca76a3d --- /dev/null +++ b/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.tfvars @@ -0,0 +1,61 @@ +automation = { + federated_identity_pool = null + federated_identity_providers = null + project_id = "fast-prod-automation" + project_number = 123456 + outputs_bucket = "test" +} +billing_account = { + id = "000000-111111-222222" +} +custom_roles = { + # organization_iam_admin = "organizations/123456789012/roles/organizationIamAdmin", + service_project_network_admin = "organizations/123456789012/roles/xpnServiceAdmin" + tenant_network_admin = "organizations/123456789012/roles/TenantNetworkAdmin" +} +groups = { + gcp-billing-admins = "gcp-billing-admins", + gcp-devops = "gcp-devops", + gcp-network-admins = "gcp-network-admins", + gcp-organization-admins = "gcp-organization-admins", + gcp-security-admins = "gcp-security-admins", + gcp-support = "gcp-support" +} +organization = { + domain = "fast.example.com" + id = 123456789012 + customer_id = "C00000000" +} +prefix = "fast2" +tag_keys = { + context = "tagKeys/1234567890" + environment = "tagKeys/4567890123" + tenant = "tagKeys/7890123456" +} +tag_names = { + context = "context" + environment = "environment" + tenant = "tenant" +} +tag_values = { + "context/data" : "tagValues/1234567890", + "context/gke" : "tagValues/1234567890", + "context/networking" : "tagValues/1234567890", + "context/sandbox" : "tagValues/1234567890", + "context/security" : "tagValues/1234567890", + "context/teams" : "tagValues/1234567890", + "environment/development" : "tagValues/1234567890", + "environment/production" : "tagValues/1234567890" +} +tenant_config = { + groups = { + gcp-admins = "gcp-tn01-admins" + } + descriptive_name = "Tenant 01" + locations = { + gcs = "europe-west8" + logging = "europe-west8" + } + short_name = "tn01" +} +test_principal = "foo-prod-resman-0@foo-prod-iac-core-0.iam.gserviceaccount.com" diff --git a/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.yaml b/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.yaml new file mode 100644 index 000000000..e5ccc4fd5 --- /dev/null +++ b/tests/fast/stages_multitenant/s0_bootstrap_tenant/simple.yaml @@ -0,0 +1,33 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +counts: + google_bigquery_default_service_account: 2 + google_folder: 2 + google_folder_iam_binding: 5 + google_organization_iam_member: 39 + google_project: 2 + google_project_iam_binding: 8 + google_project_service: 26 + google_project_service_identity: 3 + google_service_account: 11 + google_storage_bucket: 2 + google_storage_bucket_iam_binding: 1 + google_storage_bucket_iam_member: 1 + google_storage_bucket_object: 2 + google_storage_project_service_account: 2 + google_tags_tag_binding: 1 + google_tags_tag_value: 1 + modules: 19 + resources: 129 diff --git a/tests/fast/stages_multitenant/s0_bootstrap_tenant/tftest.yaml b/tests/fast/stages_multitenant/s0_bootstrap_tenant/tftest.yaml new file mode 100644 index 000000000..c2fa9fa8e --- /dev/null +++ b/tests/fast/stages_multitenant/s0_bootstrap_tenant/tftest.yaml @@ -0,0 +1,10 @@ +# skip boilerplate check + +module: fast/stages-multitenant/0-bootstrap-tenant + +tests: + simple: + tfvars: + - simple.tfvars + inventory: + - simple.yaml diff --git a/tests/modules/gke_nodepool/__init__.py b/tests/fast/stages_multitenant/s1_resman_tenant/__init__.py similarity index 100% rename from tests/modules/gke_nodepool/__init__.py rename to tests/fast/stages_multitenant/s1_resman_tenant/__init__.py diff --git a/tests/fast/stages_multitenant/s1_resman_tenant/simple.tfvars b/tests/fast/stages_multitenant/s1_resman_tenant/simple.tfvars new file mode 100644 index 000000000..33cf46198 --- /dev/null +++ b/tests/fast/stages_multitenant/s1_resman_tenant/simple.tfvars @@ -0,0 +1,70 @@ +automation = { + federated_identity_pools = null + federated_identity_providers = null + project_id = "tn0-prod-automation-0" + project_number = 123456 + outputs_bucket = "tn0-prod-automation-0" + service_accounts = { + networking = "foo-tn0-net-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + resman = "foo-tn0-resman-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + security = "foo-tn0-sec-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + dp-dev = "foo-tn0-dp-dev-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + dp-prod = "foo-tn0-dp-prod-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + gke-dev = "foo-tn0-gke-dev-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + gke-prod = "foo-tn0-gke-prod-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + pf-dev = "foo-tn0-pf-dev-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + pf-prod = "foo-tn0-pf-prod-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + sandbox = "foo-tn0-sandbox-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + teams = "foo-tn0-teams-0@foo-tn0-prod-iac-core-0.iam.gserviceaccount.com" + } +} +billing_account = { + id = "000000-111111-222222" +} +custom_roles = { + # organization_iam_admin = "organizations/123456789012/roles/organizationIamAdmin", + service_project_network_admin = "organizations/123456789012/roles/xpnServiceAdmin" +} +fast_features = { + data_platform = true + gke = true + project_factory = true + sandbox = true + teams = true +} +groups = { + gcp-devops = "gcp-devops", + gcp-network-admins = "gcp-network-admins", + gcp-security-admins = "gcp-security-admins", +} +organization = { + domain = "fast.example.com" + id = 123456789012 + customer_id = "C00000000" +} +prefix = "foo-tn0" +root_node = "folders/1234567890" +short_name = "tn0" +tags = { + keys = { + context = "tagKeys/1234567890" + environment = "tagKeys/4567890123" + tenant = "tagKeys/7890123456" + } + names = { + context = "context" + environment = "environment" + tenant = "tenant" + } + values = { + "context/data" : "tagValues/1234567890", + "context/gke" : "tagValues/1234567890", + "context/networking" : "tagValues/1234567890", + "context/sandbox" : "tagValues/1234567890", + "context/security" : "tagValues/1234567890", + "context/teams" : "tagValues/1234567890", + "environment/development" : "tagValues/1234567890", + "environment/production" : "tagValues/1234567890" + } +} +test_skip_data_sources = true diff --git a/tests/fast/stages_multitenant/s1_resman_tenant/simple.yaml b/tests/fast/stages_multitenant/s1_resman_tenant/simple.yaml new file mode 100644 index 000000000..44c07c62f --- /dev/null +++ b/tests/fast/stages_multitenant/s1_resman_tenant/simple.yaml @@ -0,0 +1,28 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +counts: + google_folder: 13 + google_folder_iam_binding: 42 + google_folder_iam_member: 3 + google_org_policy_policy: 2 + google_service_account: 9 + google_service_account_iam_binding: 8 + google_storage_bucket: 10 + google_storage_bucket_iam_binding: 10 + google_storage_bucket_iam_member: 9 + google_storage_bucket_object: 11 + google_tags_tag_binding: 12 + modules: 32 + resources: 129 diff --git a/tests/fast/stages_multitenant/s1_resman_tenant/tftest.yaml b/tests/fast/stages_multitenant/s1_resman_tenant/tftest.yaml new file mode 100644 index 000000000..2e107e08d --- /dev/null +++ b/tests/fast/stages_multitenant/s1_resman_tenant/tftest.yaml @@ -0,0 +1,10 @@ +# skip boilerplate check + +module: fast/stages-multitenant/1-resman-tenant + +tests: + simple: + tfvars: + - simple.tfvars + inventory: + - simple.yaml diff --git a/tests/fixtures.py b/tests/fixtures.py index c483063f4..11a397de7 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -31,7 +31,7 @@ PlanSummary = collections.namedtuple('PlanSummary', 'values counts outputs') @contextlib.contextmanager def _prepare_root_module(path): """Context manager to prepare a terraform module to be tested. - + If the TFTEST_COPY environment variable is set, `path` is copied to a temporary directory and a few terraform files (e.g. terraform.tfvars) are delete to ensure a clean test environment. @@ -49,6 +49,7 @@ def _prepare_root_module(path): # deployment with links to configs) ignore_patterns = shutil.ignore_patterns('*.auto.tfvars', '*.auto.tfvars.json', + '[0-9]-*-providers.tf', 'terraform.tfstate*', 'terraform.tfvars', '.terraform') @@ -103,6 +104,7 @@ def plan_summary(module_path, basedir, tf_var_files=None, **tf_vars): # compute resource type counts and address->values map values = {} counts = collections.defaultdict(int) + counts['modules'] = counts['resources'] = 0 q = collections.deque([plan.root_module]) while q: e = q.popleft() @@ -113,8 +115,10 @@ def plan_summary(module_path, basedir, tf_var_files=None, **tf_vars): values[e['address']] = e['values'] for x in e.get('resources', []): + counts['resources'] += 1 q.append(x) for x in e.get('child_modules', []): + counts['modules'] += 1 q.append(x) # extract planned outputs @@ -177,19 +181,19 @@ def plan_validator(module_path, inventory_paths, basedir, tf_var_files=None, expected_values = inventory['values'] for address, expected_value in expected_values.items(): assert address in summary.values, \ - f'{address} is not a valid address in the plan' + f'{address} is not a valid address in the plan' for k, v in expected_value.items(): assert k in summary.values[address], \ - f'{k} not found at {address}' + f'{k} not found at {address}' plan_value = summary.values[address][k] assert plan_value == v, \ - f'{k} at {address} failed. Got `{plan_value}`, expected `{v}`' + f'{k} at {address} failed. Got `{plan_value}`, expected `{v}`' if 'counts' in inventory: expected_counts = inventory['counts'] for type_, expected_count in expected_counts.items(): assert type_ in summary.counts, \ - f'module does not create any resources of type `{type_}`' + f'module does not create any resources of type `{type_}`' plan_count = summary.counts[type_] assert plan_count == expected_count, \ f'count of {type_} resources failed. Got {plan_count}, expected {expected_count}' @@ -198,7 +202,7 @@ def plan_validator(module_path, inventory_paths, basedir, tf_var_files=None, expected_outputs = inventory['outputs'] for output_name, expected_output in expected_outputs.items(): assert output_name in summary.outputs, \ - f'module does not output `{output_name}`' + f'module does not output `{output_name}`' output = summary.outputs[output_name] # assert 'value' in output, \ # f'output `{output_name}` does not have a value (is it sensitive or dynamic?)' @@ -224,7 +228,7 @@ def plan_validator_fixture(request): basedir = Path(request.fspath).parent return plan_validator(module_path=module_path, inventory_paths=inventory_paths, basedir=basedir, - tf_var_files=tf_var_paths, **tf_vars) + tf_var_files=tf_var_files, **tf_vars) return inner diff --git a/tests/legacy_fixtures.py b/tests/legacy_fixtures.py index 5891d704b..8b23ea5ee 100644 --- a/tests/legacy_fixtures.py +++ b/tests/legacy_fixtures.py @@ -96,34 +96,6 @@ def e2e_plan_runner(_plan_runner): return run_plan -@pytest.fixture(scope='session') -def recursive_e2e_plan_runner(_plan_runner): - """ - Plan runner for end-to-end root module, returns total number of - (nested) modules and resources - """ - - def walk_plan(node, modules, resources): - new_modules = node.get('child_modules', []) - resources += node.get('resources', []) - modules += new_modules - for module in new_modules: - walk_plan(module, modules, resources) - - def run_plan(fixture_path=None, tf_var_file=None, targets=None, refresh=True, - include_bare_resources=False, compute_sums=True, tmpdir=True, - **tf_vars): - 'Run Terraform plan on a root module using defaults, returns data.' - plan = _plan_runner(fixture_path, tf_var_file=tf_var_file, targets=targets, - refresh=refresh, tmpdir=tmpdir, **tf_vars) - modules = [] - resources = [] - walk_plan(plan.root_module, modules, resources) - return len(modules), len(resources) - - return run_plan - - @pytest.fixture(scope='session') def apply_runner(): 'Return a function to run Terraform apply on a fixture.' diff --git a/tests/modules/api_gateway/examples/basic.yaml b/tests/modules/api_gateway/examples/basic.yaml new file mode 100644 index 000000000..a17fc3ca4 --- /dev/null +++ b/tests/modules/api_gateway/examples/basic.yaml @@ -0,0 +1,42 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.gateway.google_api_gateway_api.api: + api_id: api + display_name: api + project: my-project + module.gateway.google_api_gateway_api_config.api_config: + api: api + gateway_config: [] + grpc_services: [] + labels: null + managed_service_configs: [] + project: my-project + module.gateway.google_api_gateway_gateway.gateway: + display_name: gw-api + gateway_id: gw-api + labels: null + project: my-project + region: europe-west1 + module.gateway.google_project_service.service: + disable_dependent_services: true + disable_on_destroy: true + project: my-project + +counts: + google_api_gateway_api: 1 + google_api_gateway_api_config: 1 + google_api_gateway_gateway: 1 + google_project_service: 1 diff --git a/tests/modules/api_gateway/examples/create-sa.yaml b/tests/modules/api_gateway/examples/create-sa.yaml new file mode 100644 index 000000000..2c8d7c763 --- /dev/null +++ b/tests/modules/api_gateway/examples/create-sa.yaml @@ -0,0 +1,90 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.gateway.google_api_gateway_api.api: + api_id: api + display_name: api + labels: null + project: my-project + module.gateway.google_api_gateway_api_config.api_config: + api: api + grpc_services: [] + labels: null + managed_service_configs: [] + project: my-project + module.gateway.google_api_gateway_api_config_iam_binding.api_config_iam_bindings["roles/apigateway.admin"]: + api: api + condition: [] + members: + - user:mirene@google.com + project: my-project + role: roles/apigateway.admin + module.gateway.google_api_gateway_api_config_iam_binding.api_config_iam_bindings["roles/apigateway.viewer"]: + api: api + condition: [] + members: + - user:mirene@google.com + project: my-project + role: roles/apigateway.viewer + module.gateway.google_api_gateway_api_iam_binding.api_iam_bindings["roles/apigateway.admin"]: + api: api + condition: [] + members: + - user:mirene@google.com + project: my-project + role: roles/apigateway.admin + module.gateway.google_api_gateway_api_iam_binding.api_iam_bindings["roles/apigateway.viewer"]: + api: api + condition: [] + members: + - user:mirene@google.com + project: my-project + role: roles/apigateway.viewer + module.gateway.google_api_gateway_gateway.gateway: + display_name: gw-api + gateway_id: gw-api + labels: null + project: my-project + region: europe-west1 + module.gateway.google_api_gateway_gateway_iam_binding.gateway_iam_bindings["roles/apigateway.admin"]: + condition: [] + gateway: gw-api + members: + - user:mirene@google.com + project: my-project + region: europe-west1 + role: roles/apigateway.admin + module.gateway.google_api_gateway_gateway_iam_binding.gateway_iam_bindings["roles/apigateway.viewer"]: + condition: [] + gateway: gw-api + members: + - user:mirene@google.com + project: my-project + region: europe-west1 + role: roles/apigateway.viewer + module.gateway.google_project_service.service: {} + module.gateway.google_service_account.service_account[0]: + account_id: sa-api-cfg-api + project: my-project + +counts: + google_api_gateway_api: 1 + google_api_gateway_api_config: 1 + google_api_gateway_api_config_iam_binding: 2 + google_api_gateway_api_iam_binding: 2 + google_api_gateway_gateway: 1 + google_api_gateway_gateway_iam_binding: 2 + google_project_service: 1 + google_service_account: 1 diff --git a/tests/modules/api_gateway/examples/existing-sa.yaml b/tests/modules/api_gateway/examples/existing-sa.yaml new file mode 100644 index 000000000..f0befa79a --- /dev/null +++ b/tests/modules/api_gateway/examples/existing-sa.yaml @@ -0,0 +1,71 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.gateway.google_api_gateway_api.api: + api_id: api + display_name: api + labels: null + project: my-project + module.gateway.google_api_gateway_api_config.api_config: + api: api + gateway_config: + - backend_config: + - google_service_account: sa@my-project.iam.gserviceaccount.com + grpc_services: [] + labels: null + managed_service_configs: [] + project: my-project + module.gateway.google_api_gateway_api_config_iam_binding.api_config_iam_bindings["roles/apigateway.admin"]: + api: api + api_config: api-cfg-api-8656c6040d6d9ba18a8b9b5f3955c223 + condition: [] + members: + - user:user@example.com + project: my-project + role: roles/apigateway.admin + module.gateway.google_api_gateway_api_iam_binding.api_iam_bindings["roles/apigateway.admin"]: + api: api + condition: [] + members: + - user:user@example.com + project: my-project + role: roles/apigateway.admin + module.gateway.google_api_gateway_gateway.gateway: + display_name: gw-api + gateway_id: gw-api + labels: null + project: my-project + region: europe-west1 + module.gateway.google_api_gateway_gateway_iam_binding.gateway_iam_bindings["roles/apigateway.admin"]: + condition: [] + gateway: gw-api + members: + - user:user@example.com + project: my-project + region: europe-west1 + role: roles/apigateway.admin + module.gateway.google_project_service.service: + disable_dependent_services: true + disable_on_destroy: true + project: my-project + +counts: + google_api_gateway_api: 1 + google_api_gateway_api_config: 1 + google_api_gateway_api_config_iam_binding: 1 + google_api_gateway_api_iam_binding: 1 + google_api_gateway_gateway: 1 + google_api_gateway_gateway_iam_binding: 1 + google_project_service: 1 diff --git a/tests/modules/api_gateway/fixture/main.tf b/tests/modules/api_gateway/fixture/main.tf deleted file mode 100644 index d4cd134f2..000000000 --- a/tests/modules/api_gateway/fixture/main.tf +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module "gateway" { - source = "../../../../modules/api-gateway" - api_id = var.api_id - project_id = var.project_id - labels = var.labels - iam = var.iam - region = var.region - spec = var.spec - service_account_create = true -} diff --git a/tests/modules/api_gateway/fixture/variables.tf b/tests/modules/api_gateway/fixture/variables.tf deleted file mode 100644 index 977af921d..000000000 --- a/tests/modules/api_gateway/fixture/variables.tf +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -variable "api_id" { - type = string - default = "my-api" -} - -variable "iam" { - type = map(list(string)) - default = null -} - -variable "labels" { - type = map(string) - default = null -} - -variable "project_id" { - type = string - default = "my-project" -} - -variable "region" { - type = string - default = "europe-west1" -} - -variable "service_account_create" { - type = bool - default = true -} - -variable "service_account_email" { - type = string - default = null -} - -variable "spec" { - type = string - default = "Spec contents" -} diff --git a/tests/modules/apigee/fixture/test.all.tfvars b/tests/modules/apigee/fixture/test.all.tfvars index d0c29921c..9eb337b74 100644 --- a/tests/modules/apigee/fixture/test.all.tfvars +++ b/tests/modules/apigee/fixture/test.all.tfvars @@ -29,14 +29,16 @@ environments = { } instances = { instance-test-ew1 = { - region = "europe-west1" - environments = ["apis-test"] - psa_ip_cidr_range = "10.0.4.0/22" + region = "europe-west1" + environments = ["apis-test"] + runtime_ip_cidr_range = "10.0.4.0/22" + troubleshooting_ip_cidr_range = "10.1.0.0/28" } instance-prod-ew3 = { - region = "europe-west3" - environments = ["apis-prod"] - psa_ip_cidr_range = "10.0.5.0/22" + region = "europe-west3" + environments = ["apis-prod"] + runtime_ip_cidr_range = "10.0.6.0/22" + troubleshooting_ip_cidr_range = "10.1.0.16/28" } } endpoint_attachments = { diff --git a/tests/modules/apigee/fixture/test.env_only_with_api_proxy_type.tfvars b/tests/modules/apigee/fixture/test.env_only_with_api_proxy_type.tfvars new file mode 100644 index 000000000..2a9164bf4 --- /dev/null +++ b/tests/modules/apigee/fixture/test.env_only_with_api_proxy_type.tfvars @@ -0,0 +1,13 @@ +project_id = "my-project" +environments = { + apis-test = { + display_name = "APIs test" + description = "APIs Test" + api_proxy_type = "PROGRAMMABLE" + envgroups = ["test"] + node_config = { + min_node_count = 2 + max_node_count = 5 + } + } +} diff --git a/tests/modules/apigee/fixture/test.env_only_with_deployment_type.tfvars b/tests/modules/apigee/fixture/test.env_only_with_deployment_type.tfvars new file mode 100644 index 000000000..48ef24e68 --- /dev/null +++ b/tests/modules/apigee/fixture/test.env_only_with_deployment_type.tfvars @@ -0,0 +1,13 @@ +project_id = "my-project" +environments = { + apis-test = { + display_name = "APIs test" + description = "APIs Test" + deployment_type = "ARCHIVE" + envgroups = ["test"] + node_config = { + min_node_count = 2 + max_node_count = 5 + } + } +} diff --git a/tests/modules/apigee/fixture/test.instance_only.tfvars b/tests/modules/apigee/fixture/test.instance_only.tfvars index 3d3eb1be1..d9399bfa9 100644 --- a/tests/modules/apigee/fixture/test.instance_only.tfvars +++ b/tests/modules/apigee/fixture/test.instance_only.tfvars @@ -1,8 +1,9 @@ project_id = "my-project" instances = { instance-test-ew1 = { - region = "europe-west1" - environments = ["apis-test"] - psa_ip_cidr_range = "10.0.4.0/22" + region = "europe-west1" + environments = ["apis-test"] + runtime_ip_cidr_range = "10.0.4.0/22" + troubleshooting_ip_cidr_range = "10.1.1.0.0/28" } -} \ No newline at end of file +} diff --git a/tests/modules/apigee/fixture/variables.tf b/tests/modules/apigee/fixture/variables.tf index 266f0d34e..00961aac2 100644 --- a/tests/modules/apigee/fixture/variables.tf +++ b/tests/modules/apigee/fixture/variables.tf @@ -32,8 +32,10 @@ variable "envgroups" { variable "environments" { description = "Environments." type = map(object({ - display_name = optional(string) - description = optional(string, "Terraform-managed") + display_name = optional(string) + description = optional(string, "Terraform-managed") + deployment_type = optional(string) + api_proxy_type = optional(string) node_config = optional(object({ min_node_count = optional(number) max_node_count = optional(number) @@ -47,13 +49,14 @@ variable "environments" { variable "instances" { description = "Instances." type = map(object({ - display_name = optional(string) - description = optional(string, "Terraform-managed") - region = string - environments = list(string) - psa_ip_cidr_range = string - disk_encryption_key = optional(string) - consumer_accept_list = optional(list(string)) + display_name = optional(string) + description = optional(string, "Terraform-managed") + region = string + environments = list(string) + runtime_ip_cidr_range = string + troubleshooting_ip_cidr_range = string + disk_encryption_key = optional(string) + consumer_accept_list = optional(list(string)) })) default = null } diff --git a/tests/modules/apigee/test_plan.py b/tests/modules/apigee/test_plan.py index e693ddbb2..d16ef2963 100644 --- a/tests/modules/apigee/test_plan.py +++ b/tests/modules/apigee/test_plan.py @@ -54,6 +54,18 @@ def test_env_only(plan_runner): 'google_apigee_envgroup_attachment.envgroup_attachments': 1, } +def test_env_only_with_deployment_type(plan_runner): + "Test that creates an environment in an existing environment group, with deployment_type set." + _, resources = plan_runner(tf_var_file='test.env_only_with_deployment_type.tfvars') + assert [r['values'].get('deployment_type') for r in resources + ] == [None, 'ARCHIVE'] + +def test_env_only_with_api_proxy_type(plan_runner): + "Test that creates an environment in an existing environment group, with api_proxy_type set." + _, resources = plan_runner(tf_var_file='test.env_only_with_api_proxy_type.tfvars') + assert [r['values'].get('api_proxy_type') for r in resources + ] == [None, 'PROGRAMMABLE'] + def test_instance_only(plan_runner): "Test that creates only an instance." _, resources = plan_runner(tf_var_file='test.instance_only.tfvars') diff --git a/tests/modules/bigtable_instance/fixture/main.tf b/tests/modules/bigtable_instance/fixture/main.tf index fa74a6c8e..4fa83ce2b 100644 --- a/tests/modules/bigtable_instance/fixture/main.tf +++ b/tests/modules/bigtable_instance/fixture/main.tf @@ -22,12 +22,15 @@ module "test" { "roles/bigtable.user" = ["user:me@example.com"] } tables = { - test-1 = null, + test-1 = {}, test-2 = { - split_keys = ["a", "b", "c"] - column_family = null + split_keys = ["a", "b", "c"] } } - zone = var.zone + clusters = { + test = { + zone = var.zone + } + } } diff --git a/tests/modules/compute_mig/test_plan.py b/tests/modules/compute_mig/test_plan.py index e24a7ca70..7fec3c1bd 100644 --- a/tests/modules/compute_mig/test_plan.py +++ b/tests/modules/compute_mig/test_plan.py @@ -84,7 +84,7 @@ def test_stateful_mig(plan_runner): "Test stateful instances - mig." stateful_disks = '''{ - persistent-disk-1 = null + persistent-disk-1 = false }''' _, resources = plan_runner(stateful_disks=stateful_disks) assert len(resources) == 1 diff --git a/tests/modules/compute_vm/examples/alias-ips.yaml b/tests/modules/compute_vm/examples/alias-ips.yaml new file mode 100644 index 000000000..016f96609 --- /dev/null +++ b/tests/modules/compute_vm/examples/alias-ips.yaml @@ -0,0 +1,36 @@ +# 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.vm-with-alias-ips.google_compute_instance.default[0]: + name: test + network_interface: + - access_config: [] + alias_ip_range: + - ip_cidr_range: 10.16.0.10/32 + subnetwork_range_name: alias1 + ipv6_access_config: [] + network: projects/xxx/global/networks/aaa + nic_type: null + queue_count: null + subnetwork: subnet_self_link + project: my-project + zone: europe-west1-b + +counts: + google_compute_instance: 1 + modules: 1 + resources: 1 + +outputs: {} diff --git a/tests/modules/compute_vm/examples/cmek.yaml b/tests/modules/compute_vm/examples/cmek.yaml new file mode 100644 index 000000000..cf390fde0 --- /dev/null +++ b/tests/modules/compute_vm/examples/cmek.yaml @@ -0,0 +1,57 @@ +# 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.kms-vm-example.google_compute_disk.disks["attached-disk"]: + disk_encryption_key: + - kms_key_self_link: kms_key_self_link + kms_key_service_account: null + raw_key: null + labels: + disk_name: attached-disk + disk_type: pd-balanced + name: kms-test-attached-disk + project: project-id + size: 10 + type: pd-balanced + zone: europe-west1-b + module.kms-vm-example.google_compute_instance.default[0]: + attached_disk: + - device_name: attached-disk + disk_encryption_key_raw: null + mode: READ_WRITE + source: kms-test-attached-disk + boot_disk: + - auto_delete: true + disk_encryption_key_raw: null + initialize_params: + - image: projects/debian-cloud/global/images/family/debian-10 + size: 10 + type: pd-balanced + kms_key_self_link: kms_key_self_link + mode: READ_WRITE + name: kms-test + zone: europe-west1-b + module.kms-vm-example.google_service_account.service_account[0]: + account_id: tf-vm-kms-test + description: null + disabled: false + display_name: Terraform VM kms-test. + project: project-id + timeouts: null + +counts: + google_compute_disk: 1 + google_compute_instance: 1 + google_service_account: 1 diff --git a/tests/modules/compute_vm/examples/confidential.yaml b/tests/modules/compute_vm/examples/confidential.yaml new file mode 100644 index 000000000..e842d4cb4 --- /dev/null +++ b/tests/modules/compute_vm/examples/confidential.yaml @@ -0,0 +1,31 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.template-confidential-example.google_compute_instance_template.default[0]: + confidential_instance_config: + - enable_confidential_compute: true + name_prefix: confidential-template- + project: project-id + region: europe-west1 + module.vm-confidential-example.google_compute_instance.default[0]: + confidential_instance_config: + - enable_confidential_compute: true + name: confidential-vm + project: project-id + zone: europe-west1-b + +counts: + google_compute_instance: 1 + google_compute_instance_template: 1 diff --git a/tests/modules/compute_vm/examples/disk-options.yaml b/tests/modules/compute_vm/examples/disk-options.yaml new file mode 100644 index 000000000..91c11b419 --- /dev/null +++ b/tests/modules/compute_vm/examples/disk-options.yaml @@ -0,0 +1,59 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.vm-disk-options-example.google_compute_disk.disks["data2"]: + name: test-data2 + project: project-id + size: 20 + snapshot: snapshot-2 + type: pd-ssd + zone: europe-west1-b + module.vm-disk-options-example.google_compute_instance.default[0]: + attached_disk: + - device_name: data2 + disk_encryption_key_raw: null + mode: READ_ONLY + source: test-data2 + - device_name: data1 + disk_encryption_key_raw: null + mode: READ_WRITE + source: test-data1 + boot_disk: + - auto_delete: true + disk_encryption_key_raw: null + initialize_params: + - image: projects/debian-cloud/global/images/family/debian-11 + size: 10 + type: pd-balanced + mode: READ_WRITE + description: Managed by the compute-vm Terraform module. + name: test + project: project-id + zone: europe-west1-b + module.vm-disk-options-example.google_compute_region_disk.disks["data1"]: + name: test-data1 + project: project-id + region: europe-west1 + replica_zones: + - europe-west1-b + - europe-west1-c + size: 10 + type: pd-balanced + +counts: + google_compute_disk: 1 + google_compute_instance: 1 + google_compute_region_disk: 1 + google_service_account: 1 diff --git a/tests/modules/compute_vm/examples/group.yaml b/tests/modules/compute_vm/examples/group.yaml new file mode 100644 index 000000000..c28c47648 --- /dev/null +++ b/tests/modules/compute_vm/examples/group.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.instance-group.google_compute_instance.default[0]: {} + module.instance-group.google_compute_instance_group.unmanaged[0]: + name: ilb-test + named_port: [] + network: projects/xxx/global/networks/aaa + project: my-project + timeouts: null + zone: europe-west1-b + +counts: + google_compute_instance: 1 + google_compute_instance_group: 1 diff --git a/tests/modules/compute_vm/examples/gvnic.yaml b/tests/modules/compute_vm/examples/gvnic.yaml new file mode 100644 index 000000000..da95de9e4 --- /dev/null +++ b/tests/modules/compute_vm/examples/gvnic.yaml @@ -0,0 +1,43 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + google_compute_image.cos-gvnic: + guest_os_features: + - type: GVNIC + - type: SEV_CAPABLE + - type: UEFI_COMPATIBLE + - type: VIRTIO_SCSI_MULTIQUEUE + name: my-image + project: my-project + source_image: https://www.googleapis.com/compute/v1/projects/cos-cloud/global/images/cos-89-16108-534-18 + module.vm-with-gvnic.google_compute_instance.default[0]: + name: test + network_interface: + - access_config: [] + alias_ip_range: [] + ipv6_access_config: [] + network: projects/xxx/global/networks/aaa + nic_type: GVNIC + queue_count: null + subnetwork: subnet_self_link + project: my-project + zone: europe-west1-b + +counts: + google_compute_image: 1 + google_compute_instance: 1 + google_service_account: 1 + modules: 1 + resources: 3 diff --git a/tests/modules/compute_vm/examples/iam.yaml b/tests/modules/compute_vm/examples/iam.yaml new file mode 100644 index 000000000..254d266d7 --- /dev/null +++ b/tests/modules/compute_vm/examples/iam.yaml @@ -0,0 +1,34 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.vm-iam-example.google_compute_instance.default[0]: + name: webserver + module.vm-iam-example.google_compute_instance_iam_binding.default["roles/compute.instanceAdmin"]: + condition: [] + instance_name: webserver + members: + - group:admin@example.com + - group:webserver@example.com + project: project-id + role: roles/compute.instanceAdmin + zone: europe-west1-b + +counts: + google_compute_instance: 1 + google_compute_instance_iam_binding: 1 + modules: 1 + resources: 2 + +outputs: {} diff --git a/tests/modules/compute_vm/examples/ips.yaml b/tests/modules/compute_vm/examples/ips.yaml new file mode 100644 index 000000000..65931abb5 --- /dev/null +++ b/tests/modules/compute_vm/examples/ips.yaml @@ -0,0 +1,45 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.vm-external-ip.google_compute_instance.default[0]: + name: vm-external-ip + network_interface: + - access_config: + - nat_ip: 8.8.8.8 + public_ptr_domain_name: null + alias_ip_range: [] + ipv6_access_config: [] + network: projects/xxx/global/networks/aaa + nic_type: null + queue_count: null + subnetwork: subnet_self_link + project: my-project + zone: europe-west1-b + module.vm-internal-ip.google_compute_instance.default[0]: + name: vm-internal-ip + network_interface: + - access_config: [] + alias_ip_range: [] + ipv6_access_config: [] + network: projects/xxx/global/networks/aaa + network_ip: 10.0.0.2 + nic_type: null + queue_count: null + subnetwork: subnet_self_link + project: my-project + zone: europe-west1-b + +counts: + google_compute_instance: 2 diff --git a/tests/modules/net_vpc/simple.yaml b/tests/modules/compute_vm/examples/metadata.yaml similarity index 54% rename from tests/modules/net_vpc/simple.yaml rename to tests/modules/compute_vm/examples/metadata.yaml index 004be7ecf..fbe0d06ff 100644 --- a/tests/modules/net_vpc/simple.yaml +++ b/tests/modules/compute_vm/examples/metadata.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,24 +13,20 @@ # limitations under the License. values: - google_compute_network.network[0]: - auto_create_subnetworks: false - delete_default_routes_on_create: false - description: Terraform-managed. - name: test - project: test-project - routing_mode: GLOBAL + module.vm-metadata-example.google_compute_instance.default[0]: + metadata: + startup-script: | + #! /bin/bash + apt-get update + apt-get install -y nginx + name: nginx-server + project: project-id + zone: europe-west1-b + labels: + env: dev + system: crm + module.vm-metadata-example.google_service_account.service_account[0]: {} counts: - google_compute_network: 1 - -outputs: - bindings: {} - project_id: test-project - subnet_ips: {} - subnet_regions: {} - subnet_secondary_ranges: {} - subnet_self_links: {} - subnets: {} - subnets_proxy_only: {} - subnets_psc: {} + google_compute_instance: 1 + google_service_account: 1 diff --git a/tests/modules/compute_vm/examples/sas.yaml b/tests/modules/compute_vm/examples/sas.yaml new file mode 100644 index 000000000..96a948317 --- /dev/null +++ b/tests/modules/compute_vm/examples/sas.yaml @@ -0,0 +1,49 @@ +# 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.vm-default-sa-example2.google_compute_instance.default[0]: + name: test3 + project: project-id + service_account: + - scopes: + - https://www.googleapis.com/auth/devstorage.read_only + - https://www.googleapis.com/auth/logging.write + - https://www.googleapis.com/auth/monitoring.write + zone: europe-west1-b + module.vm-managed-sa-example.google_compute_instance.default[0]: + name: test1 + project: project-id + service_account: + - scopes: + - https://www.googleapis.com/auth/cloud-platform + - https://www.googleapis.com/auth/userinfo.email + zone: europe-west1-b + module.vm-managed-sa-example.google_service_account.service_account[0]: + account_id: tf-vm-test1 + display_name: Terraform VM test1. + project: project-id + module.vm-managed-sa-example2.google_compute_instance.default[0]: + name: test2 + project: project-id + service_account: + - scopes: + - https://www.googleapis.com/auth/cloud-platform + zone: europe-west1-b + +counts: + google_compute_instance: 3 + google_service_account: 1 + modules: 3 + resources: 4 diff --git a/tests/modules/compute_vm/examples/simple.yaml b/tests/modules/compute_vm/examples/simple.yaml new file mode 100644 index 000000000..6754efaae --- /dev/null +++ b/tests/modules/compute_vm/examples/simple.yaml @@ -0,0 +1,72 @@ +# 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.simple-vm-example.google_compute_instance.default[0]: + advanced_machine_features: [] + allow_stopping_for_update: true + attached_disk: [] + boot_disk: + - auto_delete: true + disk_encryption_key_raw: null + initialize_params: + - image: projects/debian-cloud/global/images/family/debian-11 + size: 10 + type: pd-balanced + mode: READ_WRITE + can_ip_forward: false + deletion_protection: false + description: Managed by the compute-vm Terraform module. + enable_display: false + hostname: null + labels: null + machine_type: f1-micro + metadata: null + metadata_startup_script: null + name: test + network_interface: + - access_config: [] + alias_ip_range: [] + ipv6_access_config: [] + network: projects/xxx/global/networks/aaa + nic_type: null + queue_count: null + subnetwork: subnet_self_link + project: project-id + scheduling: + - automatic_restart: true + instance_termination_action: null + max_run_duration: [] + min_node_cpus: null + node_affinities: [] + on_host_maintenance: MIGRATE + preemptible: false + provisioning_model: STANDARD + scratch_disk: [] + service_account: + - scopes: + - https://www.googleapis.com/auth/cloud-platform + - https://www.googleapis.com/auth/userinfo.email + shielded_instance_config: [] + tags: null + zone: europe-west1-b + module.simple-vm-example.google_service_account.service_account[0]: + account_id: tf-vm-test + display_name: Terraform VM test. + project: project-id + + +counts: + google_compute_instance: 1 + google_service_account: 1 diff --git a/tests/modules/organization/iam_additive.yaml b/tests/modules/compute_vm/examples/spot.yaml similarity index 56% rename from tests/modules/organization/iam_additive.yaml rename to tests/modules/compute_vm/examples/spot.yaml index 68eda8c27..c15852dbc 100644 --- a/tests/modules/organization/iam_additive.yaml +++ b/tests/modules/compute_vm/examples/spot.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,19 +13,19 @@ # limitations under the License. values: - google_organization_iam_binding.authoritative["user:one@example.org"]: - condition: [] - members: - - roles/owner - org_id: '1234567890' - role: user:one@example.org - google_organization_iam_binding.authoritative["user:two@example.org"]: - condition: [] - members: - - roles/editor - - roles/owner - org_id: '1234567890' - role: user:two@example.org + module.spot-vm-example.google_compute_instance.default[0]: + name: test + project: project-id + scheduling: + - automatic_restart: false + instance_termination_action: STOP + max_run_duration: [] + min_node_cpus: null + node_affinities: [] + on_host_maintenance: TERMINATE + preemptible: true + provisioning_model: SPOT + zone: europe-west1-b counts: - google_organization_iam_binding: 2 + google_compute_instance: 1 diff --git a/tests/modules/compute_vm/examples/template.yaml b/tests/modules/compute_vm/examples/template.yaml new file mode 100644 index 000000000..1f1888bfc --- /dev/null +++ b/tests/modules/compute_vm/examples/template.yaml @@ -0,0 +1,65 @@ +# 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.cos-test.google_compute_instance_template.default[0]: + disk: + - auto_delete: true + boot: true + disk_encryption_key: [] + disk_name: null + disk_size_gb: 10 + disk_type: pd-balanced + labels: null + resource_policies: null + source: null + source_image: projects/cos-cloud/global/images/family/cos-stable + source_image_encryption_key: [] + source_snapshot: null + source_snapshot_encryption_key: [] + - auto_delete: true + device_name: disk-1 + disk_encryption_key: [] + disk_name: disk-1 + disk_size_gb: 10 + disk_type: pd-balanced + labels: null + mode: READ_WRITE + resource_policies: null + source: null + source_image_encryption_key: [] + source_snapshot: null + source_snapshot_encryption_key: [] + type: PERSISTENT + name_prefix: test- + network_interface: + - access_config: [] + alias_ip_range: [] + ipv6_access_config: [] + network: projects/xxx/global/networks/aaa + network_ip: null + nic_type: null + queue_count: null + subnetwork: subnet_self_link + project: my-project + region: europe-west1 + service_account: + - email: vm-default@my-project.iam.gserviceaccount.com + scopes: + - https://www.googleapis.com/auth/devstorage.read_only + - https://www.googleapis.com/auth/logging.write + - https://www.googleapis.com/auth/monitoring.write + +counts: + google_compute_instance_template: 1 diff --git a/tests/modules/compute_vm/fixture/main.tf b/tests/modules/compute_vm/fixture/main.tf deleted file mode 100644 index 5815f25f7..000000000 --- a/tests/modules/compute_vm/fixture/main.tf +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module "test" { - source = "../../../../modules/compute-vm" - project_id = "my-project" - zone = "europe-west1-b" - name = "test" - attached_disks = var.attached_disks - attached_disk_defaults = var.attached_disk_defaults - create_template = var.create_template - confidential_compute = var.confidential_compute - group = var.group - iam = var.iam - metadata = var.metadata - network_interfaces = var.network_interfaces - service_account_create = var.service_account_create -} diff --git a/tests/modules/compute_vm/fixture/variables.tf b/tests/modules/compute_vm/fixture/variables.tf deleted file mode 100644 index 02d839f64..000000000 --- a/tests/modules/compute_vm/fixture/variables.tf +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -variable "attached_disks" { - description = "Additional disks, if options is null defaults will be used in its place. Source type is one of 'image' (zonal disks in vms and template), 'snapshot' (vm), 'existing', and null." - type = any - default = [] -} - -variable "attached_disk_defaults" { - description = "Defaults for attached disks options." - type = any - default = { - auto_delete = true - mode = "READ_WRITE" - replica_zone = null - type = "pd-balanced" - } -} - -variable "confidential_compute" { - type = bool - default = false -} - -variable "create_template" { - type = bool - default = false -} - -variable "group" { - type = any - default = null -} - -variable "iam" { - type = map(set(string)) - default = {} -} - -variable "metadata" { - type = map(string) - default = {} -} - -variable "network_interfaces" { - type = any - default = [{ - network = "https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default", - subnetwork = "https://www.googleapis.com/compute/v1/projects/my-project/regions/europe-west1/subnetworks/default-default", - }] -} - -variable "service_account_create" { - type = bool - default = false -} diff --git a/tests/modules/compute_vm/test_plan.py b/tests/modules/compute_vm/test_plan.py deleted file mode 100644 index 701891c5b..000000000 --- a/tests/modules/compute_vm/test_plan.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -def test_defaults(plan_runner): - _, resources = plan_runner() - assert len(resources) == 1 - assert resources[0]['type'] == 'google_compute_instance' - - -def test_service_account(plan_runner): - _, resources = plan_runner(service_account_create='true') - assert len(resources) == 2 - assert set(r['type'] for r in resources) == set([ - 'google_compute_instance', 'google_service_account' - ]) - - -def test_template(plan_runner): - _, resources = plan_runner(create_template='true') - assert len(resources) == 1 - assert resources[0]['type'] == 'google_compute_instance_template' - assert resources[0]['values']['name_prefix'] == 'test-' - - -def test_group(plan_runner): - _, resources = plan_runner(group='{named_ports={}}') - assert len(resources) == 2 - assert set(r['type'] for r in resources) == set([ - 'google_compute_instance_group', 'google_compute_instance' - ]) - - -def test_iam(plan_runner): - iam = ( - '{"roles/compute.instanceAdmin" = ["user:a@a.com", "user:b@a.com"],' - '"roles/iam.serviceAccountUser" = ["user:a@a.com"]}' - ) - _, resources = plan_runner(iam=iam) - assert len(resources) == 3 - assert set(r['type'] for r in resources) == set([ - 'google_compute_instance', 'google_compute_instance_iam_binding']) - iam_bindings = dict( - (r['index'], r['values']['members']) for r in resources if r['type'] - == 'google_compute_instance_iam_binding' - ) - assert iam_bindings == { - 'roles/compute.instanceAdmin': ['user:a@a.com', 'user:b@a.com'], - 'roles/iam.serviceAccountUser': ['user:a@a.com'], - } - - -def test_confidential_compute(plan_runner): - _, resources = plan_runner(confidential_compute='true') - assert len(resources) == 1 - assert resources[0]['values']['confidential_instance_config'] == [ - {'enable_confidential_compute': True}] - assert resources[0]['values']['scheduling'][0]['on_host_maintenance'] == 'TERMINATE' - - -def test_confidential_compute_template(plan_runner): - _, resources = plan_runner(confidential_compute='true', - create_template='true') - assert len(resources) == 1 - assert resources[0]['values']['confidential_instance_config'] == [ - {'enable_confidential_compute': True}] - assert resources[0]['values']['scheduling'][0]['on_host_maintenance'] == 'TERMINATE' diff --git a/tests/modules/compute_vm/test_plan_disks.py b/tests/modules/compute_vm/test_plan_disks.py deleted file mode 100644 index 153c072f5..000000000 --- a/tests/modules/compute_vm/test_plan_disks.py +++ /dev/null @@ -1,165 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -def test_types(plan_runner): - _disks = '''[{ - name = "data1" - size = "10" - source_type = "image" - source = "image-1" - options = null - }, { - name = "data2" - size = "20" - source_type = "snapshot" - source = "snapshot-2" - options = null - }, { - name = "data3" - size = null - source_type = "attach" - source = "disk-3" - options = null - }] - ''' - _, resources = plan_runner(attached_disks=_disks) - assert len(resources) == 3 - disks = { - r['values']['name']: r['values'] - for r in resources if r['type'] == 'google_compute_disk' - } - assert disks['test-data1']['size'] == 10 - assert disks['test-data2']['size'] == 20 - assert disks['test-data1']['image'] == 'image-1' - assert disks['test-data1']['snapshot'] is None - assert disks['test-data2']['snapshot'] == 'snapshot-2' - assert disks['test-data2']['image'] is None - instance = [ - r['values'] for r in resources - if r['type'] == 'google_compute_instance' - ][0] - instance_disks = { - d['source']: d['device_name'] - for d in instance['attached_disk'] - } - assert instance_disks == { - 'test-data1': 'data1', - 'test-data2': 'data2', - 'disk-3': 'data3' - } - - -def test_options(plan_runner): - _disks = '''[{ - name = "data1" - size = "10" - source_type = "image" - source = "image-1" - options = { - mode = null, replica_zone = null, type = "pd-standard" - } - }, { - name = "data2" - size = "20" - source_type = null - source = null - options = { - mode = null, replica_zone = "europe-west1-c", type = "pd-ssd" - } - }] - ''' - _, resources = plan_runner(attached_disks=_disks) - assert len(resources) == 3 - disks_z = [ - r['values'] for r in resources if r['type'] == 'google_compute_disk' - ] - disks_r = [ - r['values'] for r in resources - if r['type'] == 'google_compute_region_disk' - ] - assert len(disks_z) == len(disks_r) == 1 - instance = [ - r['values'] for r in resources - if r['type'] == 'google_compute_instance' - ][0] - instance_disks = [d['device_name'] for d in instance['attached_disk']] - assert instance_disks == ['data1', 'data2'] - - -def test_template(plan_runner): - _disks = '''[{ - name = "data1" - size = "10" - source_type = "image" - source = "image-1" - options = { - mode = null, replica_zone = null, type = "pd-standard" - } - }, { - name = "data2" - size = "20" - source_type = null - source = null - options = { - mode = null, replica_zone = "europe-west1-c", type = "pd-ssd" - } - }] - ''' - _, resources = plan_runner(attached_disks=_disks, create_template="true") - assert len(resources) == 1 - template = [ - r['values'] for r in resources - if r['type'] == 'google_compute_instance_template' - ][0] - assert len(template['disk']) == 3 - - -def test_auto_delete(plan_runner): - _disks = '''[{ - name = "data1" - size = "10" - options = { - auto_delete = true, mode = "READ_WRITE" - } - }, { - name = "data2" - size = "20" - options = { - auto_delete = false, mode = "READ_WRITE" - }, - }, { - name = "data3" - size = "20" - options = { - mode = "READ_ONLY" - } - }] - ''' - _, resources = plan_runner(attached_disks=_disks, create_template="true") - assert len(resources) == 1 - template = [ - r['values'] for r in resources - if r['type'] == 'google_compute_instance_template' - ][0] - additional_disks = [ - d for d in template['disk'] if 'boot' not in d or d['boot'] != True - ] - assert len(additional_disks) == 3 - disk_data1 = [d for d in additional_disks if d['disk_name'] == 'data1'] - disk_data2 = [d for d in additional_disks if d['disk_name'] == 'data2'] - disk_data3 = [d for d in additional_disks if d['disk_name'] == 'data3'] - assert len(disk_data1) == 1 and disk_data1[0]['auto_delete'] == True - assert len(disk_data2) == 1 and disk_data2[0]['auto_delete'] == False - assert len(disk_data3) == 1 and disk_data3[0]['auto_delete'] == False diff --git a/tests/modules/compute_vm/test_plan_interfaces.py b/tests/modules/compute_vm/test_plan_interfaces.py deleted file mode 100644 index e88c087be..000000000 --- a/tests/modules/compute_vm/test_plan_interfaces.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -def test_address(plan_runner): - nics = '''[{ - network = "https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default", - subnetwork = "https://www.googleapis.com/compute/v1/projects/my-project/regions/europe-west1/subnetworks/default-default", - nat = false, - addresses = {external=null, internal="10.0.0.2"} - }] - ''' - _, resources = plan_runner(network_interfaces=nics) - assert len(resources) == 1 - n = resources[0]['values']['network_interface'][0] - assert n['network_ip'] == "10.0.0.2" - assert n['access_config'] == [] - - -def test_nat_address(plan_runner): - nics = '''[{ - network = "https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default", - subnetwork = "https://www.googleapis.com/compute/v1/projects/my-project/regions/europe-west1/subnetworks/default-default", - nat = true, - addresses = {external="8.8.8.8", internal=null} - }] - ''' - _, resources = plan_runner(network_interfaces=nics) - assert len(resources) == 1 - n = resources[0]['values']['network_interface'][0] - assert 'network_ip' not in n - assert n['access_config'][0]['nat_ip'] == '8.8.8.8' diff --git a/tests/modules/organization/logging_exclusions.yaml b/tests/modules/dns/examples/forwarding-zone.yaml similarity index 52% rename from tests/modules/organization/logging_exclusions.yaml rename to tests/modules/dns/examples/forwarding-zone.yaml index 4d51dd7cd..4a09114ee 100644 --- a/tests/modules/organization/logging_exclusions.yaml +++ b/tests/modules/dns/examples/forwarding-zone.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,18 +13,22 @@ # limitations under the License. values: - google_logging_organization_exclusion.logging-exclusion["exclusion1"]: - description: exclusion1 (Terraform-managed). - disabled: null - filter: resource.type=gce_instance - name: exclusion1 - org_id: '1234567890' - google_logging_organization_exclusion.logging-exclusion["exclusion2"]: - description: exclusion2 (Terraform-managed). - disabled: null - filter: severity=NOTICE - name: exclusion2 - org_id: '1234567890' + module.private-dns.google_dns_managed_zone.non-public[0]: + dns_name: test.example. + forwarding_config: + - target_name_servers: + - forwarding_path: '' + ipv4_address: 10.0.1.1 + - forwarding_path: private + ipv4_address: 1.2.3.4 + name: test-example + private_visibility_config: + - gke_clusters: [] + networks: + - network_url: projects/xxx/global/networks/aaa + project: myproject + visibility: private counts: - google_logging_organization_exclusion: 2 + google_dns_managed_zone: 1 + diff --git a/tests/modules/dns/examples/peering-zone.yaml b/tests/modules/dns/examples/peering-zone.yaml new file mode 100644 index 000000000..9f16adab6 --- /dev/null +++ b/tests/modules/dns/examples/peering-zone.yaml @@ -0,0 +1,34 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.private-dns.google_dns_managed_zone.non-public[0]: + description: Forwarding zone for . + dns_name: . + forwarding_config: [] + name: test-example + peering_config: + - target_network: + - network_url: projects/xxx/global/networks/ccc + private_visibility_config: + - gke_clusters: [] + networks: + - network_url: projects/xxx/global/networks/aaa + project: myproject + visibility: private + +counts: + google_dns_managed_zone: 1 + +outputs: {} diff --git a/tests/modules/dns/examples/private-zone.yaml b/tests/modules/dns/examples/private-zone.yaml new file mode 100644 index 000000000..f64266450 --- /dev/null +++ b/tests/modules/dns/examples/private-zone.yaml @@ -0,0 +1,50 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.private-dns.google_dns_managed_zone.non-public[0]: + description: Terraform managed. + dns_name: test.example. + force_destroy: false + forwarding_config: [] + name: test-example + peering_config: [] + private_visibility_config: + - gke_clusters: [] + networks: + - network_url: projects/xxx/global/networks/aaa + project: myproject + visibility: private + module.private-dns.google_dns_record_set.cloud-static-records["A localhost"]: + managed_zone: test-example + name: localhost.test.example. + project: myproject + routing_policy: [] + rrdatas: + - 127.0.0.1 + ttl: 300 + type: A + module.private-dns.google_dns_record_set.cloud-static-records["A myhost"]: + managed_zone: test-example + name: myhost.test.example. + project: myproject + routing_policy: [] + rrdatas: + - 10.0.0.120 + ttl: 600 + type: A + +counts: + google_dns_managed_zone: 1 + google_dns_record_set: 2 diff --git a/tests/modules/dns/examples/public-zone.yaml b/tests/modules/dns/examples/public-zone.yaml new file mode 100644 index 000000000..0f8067a76 --- /dev/null +++ b/tests/modules/dns/examples/public-zone.yaml @@ -0,0 +1,38 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.public-dns.google_dns_managed_zone.public[0]: + dns_name: example.com. + name: example + project: myproject + visibility: public + module.public-dns.google_dns_record_set.cloud-static-records["A myhost"]: + managed_zone: example + name: myhost.example.com. + project: myproject + routing_policy: [] + rrdatas: + - 127.0.0.1 + ttl: 300 + type: A + +counts: + google_dns_keys: 1 + google_dns_managed_zone: 1 + google_dns_record_set: 1 + modules: 1 + resources: 3 + +outputs: {} diff --git a/tests/blueprints/cloud_operations/apigee/test_plan.py b/tests/modules/dns/examples/reverse-zone.yaml similarity index 63% rename from tests/blueprints/cloud_operations/apigee/test_plan.py rename to tests/modules/dns/examples/reverse-zone.yaml index 34ad83a86..17e76a12c 100644 --- a/tests/blueprints/cloud_operations/apigee/test_plan.py +++ b/tests/modules/dns/examples/reverse-zone.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,10 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -import collections +values: + module.private-dns.google_dns_managed_zone.non-public[0]: + description: Terraform managed. + dns_name: 0.0.10.in-addr.arpa. + name: test-example + project: myproject + reverse_lookup: true + visibility: private -def test_blueprint(recursive_e2e_plan_runner): - "Test that all blueprint resources are created." - count_modules, count_resources = recursive_e2e_plan_runner(tf_var_file='test.regular.tfvars') - assert count_modules == 10 - assert count_resources == 59 +counts: + google_dns_managed_zone: 1 + +outputs: {} diff --git a/tests/modules/dns/examples/routing-policies.yaml b/tests/modules/dns/examples/routing-policies.yaml new file mode 100644 index 000000000..45b19276c --- /dev/null +++ b/tests/modules/dns/examples/routing-policies.yaml @@ -0,0 +1,80 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.private-dns.google_dns_managed_zone.non-public[0]: + dns_name: test.example. + name: test-example + project: myproject + module.private-dns.google_dns_record_set.cloud-geo-records["A geo"]: + managed_zone: test-example + name: geo.test.example. + project: myproject + routing_policy: + - enable_geo_fencing: null + geo: + - health_checked_targets: [] + location: europe-west1 + rrdatas: + - 10.0.0.1 + - health_checked_targets: [] + location: europe-west2 + rrdatas: + - 10.0.0.2 + - health_checked_targets: [] + location: europe-west3 + rrdatas: + - 10.0.0.3 + primary_backup: [] + wrr: [] + rrdatas: null + ttl: 300 + type: A + module.private-dns.google_dns_record_set.cloud-static-records["A regular"]: + managed_zone: test-example + name: regular.test.example. + project: myproject + routing_policy: [] + rrdatas: + - 10.20.0.1 + ttl: 300 + type: A + module.private-dns.google_dns_record_set.cloud-wrr-records["A wrr"]: + managed_zone: test-example + name: wrr.test.example. + project: myproject + routing_policy: + - enable_geo_fencing: null + geo: [] + primary_backup: [] + wrr: + - health_checked_targets: [] + rrdatas: + - 10.10.0.1 + weight: 0.6 + - health_checked_targets: [] + rrdatas: + - 10.10.0.2 + weight: 0.2 + - health_checked_targets: [] + rrdatas: + - 10.10.0.3 + weight: 0.2 + rrdatas: null + ttl: 600 + type: A + +counts: + google_dns_managed_zone: 1 + google_dns_record_set: 3 diff --git a/tests/modules/dns/fixture/main.tf b/tests/modules/dns/fixture/main.tf deleted file mode 100644 index bab319204..000000000 --- a/tests/modules/dns/fixture/main.tf +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module "test" { - source = "../../../../modules/dns" - project_id = "my-project" - name = "test" - domain = "test.example." - client_networks = var.client_networks - type = var.type - forwarders = var.forwarders - peer_network = var.peer_network - recordsets = var.recordsets -} diff --git a/tests/modules/dns/fixture/variables.tf b/tests/modules/dns/fixture/variables.tf deleted file mode 100644 index 8e55a287a..000000000 --- a/tests/modules/dns/fixture/variables.tf +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -variable "client_networks" { - type = list(string) - default = [ - "https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default" - ] -} - -variable "forwarders" { - type = map(string) - default = {} -} - -variable "peer_network" { - type = string - default = null -} - -variable "recordsets" { - type = any - default = { - "A localhost" = { ttl = 300, records = ["127.0.0.1"] } - "A local-host.test.example." = { ttl = 300, records = ["127.0.0.2"] } - "CNAME *" = { ttl = 300, records = ["localhost.example.org."] } - "A " = { ttl = 300, records = ["127.0.0.3"] } - "A geo" = { - geo_routing = [ - { location = "europe-west1", records = ["127.0.0.4"] }, - { location = "europe-west2", records = ["127.0.0.5"] }, - { location = "europe-west3", records = ["127.0.0.6"] } - ] - } - "A wrr" = { - ttl = 600 - wrr_routing = [ - { weight = 0.6, records = ["127.0.0.7"] }, - { weight = 0.2, records = ["127.0.0.8"] }, - { weight = 0.2, records = ["127.0.0.9"] } - ] - } - } -} - -variable "type" { - type = string - default = "private" -} diff --git a/tests/modules/dns/no_clients.tfvars b/tests/modules/dns/no_clients.tfvars new file mode 100644 index 000000000..97b722734 --- /dev/null +++ b/tests/modules/dns/no_clients.tfvars @@ -0,0 +1,5 @@ +type = "private" +domain = "test.example." +name = "test" +project_id = "my-project" +client_networks = [] diff --git a/tests/modules/dns/no_clients.yaml b/tests/modules/dns/no_clients.yaml new file mode 100644 index 000000000..42f628c9c --- /dev/null +++ b/tests/modules/dns/no_clients.yaml @@ -0,0 +1,25 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + google_dns_managed_zone.non-public[0]: + dns_name: test.example. + name: test + private_visibility_config: [] + visibility: private + +counts: + google_dns_managed_zone: 1 + modules: 0 + resources: 1 diff --git a/tests/modules/dns/null_forwarders.tfvars b/tests/modules/dns/null_forwarders.tfvars new file mode 100644 index 000000000..4514d6395 --- /dev/null +++ b/tests/modules/dns/null_forwarders.tfvars @@ -0,0 +1,4 @@ +type = "forwarding" +domain = "test.example." +name = "test" +project_id = "my-project" diff --git a/tests/modules/dns/null_forwarders.yaml b/tests/modules/dns/null_forwarders.yaml new file mode 100644 index 000000000..bbe637fc2 --- /dev/null +++ b/tests/modules/dns/null_forwarders.yaml @@ -0,0 +1,20 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + google_dns_managed_zone.non-public[0]: + forwarding_config: [] + +counts: + google_dns_managed_zone: 1 diff --git a/tests/modules/dns/test_plan.py b/tests/modules/dns/test_plan.py deleted file mode 100644 index 5cc1ba709..000000000 --- a/tests/modules/dns/test_plan.py +++ /dev/null @@ -1,138 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -def test_private(plan_runner): - "Test private zone with three recordsets." - _, resources = plan_runner() - assert len(resources) == 7 - assert set(r['type'] for r in resources) == { - 'google_dns_record_set', 'google_dns_managed_zone' - } - for r in resources: - if r['type'] != 'google_dns_managed_zone': - continue - assert r['values']['visibility'] == 'private' - assert len(r['values']['private_visibility_config']) == 1 - - -def test_private_recordsets(plan_runner): - "Test recordsets in private zone." - _, resources = plan_runner() - recordsets = [ - r['values'] for r in resources if r['type'] == 'google_dns_record_set' - ] - - assert set(r['name'] for r in recordsets) == { - 'localhost.test.example.', 'local-host.test.example.', '*.test.example.', - "test.example.", "geo.test.example.", "wrr.test.example." - } - - for r in recordsets: - if r['name'] not in ['wrr.test.example.', 'geo.test.example.']: - assert r['routing_policy'] == [] - assert r['rrdatas'] != [] - - -def test_routing_policies(plan_runner): - "Test recordsets with routing policies." - _, resources = plan_runner() - recordsets = [ - r['values'] for r in resources if r['type'] == 'google_dns_record_set' - ] - geo_zone = [ - r['values'] for r in resources if r['address'] == - 'module.test.google_dns_record_set.cloud-geo-records["A geo"]' - ][0] - assert geo_zone['name'] == 'geo.test.example.' - assert geo_zone['routing_policy'][0]['wrr'] == [] - geo_policy = geo_zone['routing_policy'][0]['geo'] - assert geo_policy[0]['location'] == 'europe-west1' - assert geo_policy[0]['rrdatas'] == ['127.0.0.4'] - assert geo_policy[1]['location'] == 'europe-west2' - assert geo_policy[1]['rrdatas'] == ['127.0.0.5'] - assert geo_policy[2]['location'] == 'europe-west3' - assert geo_policy[2]['rrdatas'] == ['127.0.0.6'] - - wrr_zone = [ - r['values'] for r in resources if r['address'] == - 'module.test.google_dns_record_set.cloud-wrr-records["A wrr"]' - ][0] - assert wrr_zone['name'] == 'wrr.test.example.' - wrr_policy = wrr_zone['routing_policy'][0]['wrr'] - assert wrr_policy[0]['weight'] == 0.6 - assert wrr_policy[0]['rrdatas'] == ['127.0.0.7'] - assert wrr_policy[1]['weight'] == 0.2 - assert wrr_policy[1]['rrdatas'] == ['127.0.0.8'] - assert wrr_policy[2]['weight'] == 0.2 - assert wrr_policy[2]['rrdatas'] == ['127.0.0.9'] - assert wrr_zone['routing_policy'][0]['geo'] == [] - - -def test_private_no_networks(plan_runner): - "Test private zone not exposed to any network." - _, resources = plan_runner(client_networks='[]') - for r in resources: - if r['type'] != 'google_dns_managed_zone': - continue - assert r['values']['visibility'] == 'private' - assert len(r['values']['private_visibility_config']) == 0 - - -def test_forwarding_recordsets_null_forwarders(plan_runner): - "Test forwarding zone with wrong set of attributes does not break." - _, resources = plan_runner(type='forwarding') - assert len(resources) == 1 - resource = resources[0] - assert resource['type'] == 'google_dns_managed_zone' - assert resource['values']['forwarding_config'] == [] - - -def test_forwarding(plan_runner): - "Test forwarding zone with single forwarder." - _, resources = plan_runner(type='forwarding', recordsets='null', - forwarders='{ "1.2.3.4" = null }') - assert len(resources) == 1 - resource = resources[0] - assert resource['type'] == 'google_dns_managed_zone' - assert resource['values']['forwarding_config'] == [{ - 'target_name_servers': [{ - 'forwarding_path': '', - 'ipv4_address': '1.2.3.4' - }] - }] - - -def test_peering(plan_runner): - "Test peering zone." - _, resources = plan_runner(type='peering', recordsets='null', - peer_network='dummy-vpc-self-link') - assert len(resources) == 1 - resource = resources[0] - assert resource['type'] == 'google_dns_managed_zone' - assert resource['values']['peering_config'] == [{ - 'target_network': [{ - 'network_url': 'dummy-vpc-self-link' - }] - }] - - -def test_public(plan_runner): - "Test public zone with two recordsets." - _, resources = plan_runner(type='public') - for r in resources: - if r['type'] != 'google_dns_managed_zone': - continue - assert r['values']['visibility'] == 'public' - assert r['values']['private_visibility_config'] == [] diff --git a/tests/modules/dns/tftest.yaml b/tests/modules/dns/tftest.yaml new file mode 100644 index 000000000..5172a013b --- /dev/null +++ b/tests/modules/dns/tftest.yaml @@ -0,0 +1,19 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module: modules/dns + +tests: + no_clients: + null_forwarders: diff --git a/tests/modules/folder/common.tfvars b/tests/modules/folder/common.tfvars new file mode 100644 index 000000000..aebc74a01 --- /dev/null +++ b/tests/modules/folder/common.tfvars @@ -0,0 +1,2 @@ +parent = "organizations/12345678" +name = "folder-a" diff --git a/tests/modules/organization/firewall_policies.yaml b/tests/modules/folder/examples/hfw.yaml similarity index 51% rename from tests/modules/organization/firewall_policies.yaml rename to tests/modules/folder/examples/hfw.yaml index 4ecc5c72c..57abe480e 100644 --- a/tests/modules/organization/firewall_policies.yaml +++ b/tests/modules/folder/examples/hfw.yaml @@ -13,14 +13,31 @@ # limitations under the License. values: - google_compute_firewall_policy.policy["policy1"]: - parent: organizations/1234567890 - short_name: policy1 - google_compute_firewall_policy.policy["policy2"]: - parent: organizations/1234567890 - short_name: policy2 - google_compute_firewall_policy_rule.rule["policy1-allow-ingress"]: + module.folder1.google_compute_firewall_policy.policy["iap-policy"]: + description: null + short_name: iap-policy + module.folder1.google_compute_firewall_policy_association.association["iap-policy"]: {} + module.folder1.google_compute_firewall_policy_rule.rule["iap-policy-allow-admins"]: action: allow + description: Access from the admin subnet to all subnets + direction: INGRESS + disabled: null + enable_logging: false + match: + - dest_ip_ranges: null + layer4_configs: + - ip_protocol: all + ports: [] + src_ip_ranges: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + priority: 1000 + target_resources: null + target_service_accounts: null + module.folder1.google_compute_firewall_policy_rule.rule["iap-policy-allow-iap-ssh"]: + action: allow + description: Always allow ssh from IAP direction: INGRESS disabled: null enable_logging: false @@ -31,43 +48,20 @@ values: ports: - '22' src_ip_ranges: - - 10.0.0.0/8 - priority: 100 - target_resources: null - target_service_accounts: null - google_compute_firewall_policy_rule.rule["policy1-deny-egress"]: - action: deny - direction: EGRESS - disabled: null - enable_logging: false - match: - - dest_ip_ranges: - - 192.168.0.0/24 - layer4_configs: - - ip_protocol: tcp - ports: - - '443' - src_ip_ranges: null - priority: 200 - target_resources: null - target_service_accounts: null - google_compute_firewall_policy_rule.rule["policy2-allow-ingress"]: - action: allow - direction: INGRESS - disabled: null - enable_logging: false - match: - - dest_ip_ranges: null - layer4_configs: - - ip_protocol: tcp - ports: - - '22' - src_ip_ranges: - - 10.0.0.0/8 + - 35.235.240.0/20 priority: 100 target_resources: null target_service_accounts: null + module.folder1.google_folder.folder[0]: + display_name: policy-container + parent: organizations/1122334455 + module.folder2.google_compute_firewall_policy_association.association["iap-policy"]: {} + module.folder2.google_folder.folder[0]: + display_name: hf2 + parent: organizations/1122334455 counts: - google_compute_firewall_policy: 2 - google_compute_firewall_policy_rule: 3 + google_compute_firewall_policy: 1 + google_compute_firewall_policy_association: 2 + google_compute_firewall_policy_rule: 2 + google_folder: 2 diff --git a/tests/modules/folder/examples/iam.yaml b/tests/modules/folder/examples/iam.yaml new file mode 100644 index 000000000..6f0fe2e51 --- /dev/null +++ b/tests/modules/folder/examples/iam.yaml @@ -0,0 +1,59 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.folder.google_folder.folder[0]: + display_name: Folder name + parent: organizations/1234567890 + module.folder.google_folder_iam_binding.authoritative["roles/owner"]: + condition: [] + members: + - group:cloud-owners@example.org + - user:one@example.org + role: roles/owner + module.folder.google_folder_iam_binding.authoritative["roles/resourcemanager.folderAdmin"]: + condition: [] + members: + - group:cloud-owners@example.org + role: roles/resourcemanager.folderAdmin + module.folder.google_folder_iam_binding.authoritative["roles/resourcemanager.projectCreator"]: + condition: [] + members: + - group:cloud-owners@example.org + role: roles/resourcemanager.projectCreator + module.folder.google_folder_iam_member.additive["roles/compute.admin-user:a1@example.org"]: + condition: [] + member: user:a1@example.org + role: roles/compute.admin + module.folder.google_folder_iam_member.additive["roles/compute.admin-user:a2@example.org"]: + condition: [] + member: user:a2@example.org + role: roles/compute.admin + module.folder.google_folder_iam_member.additive["roles/compute.viewer-user:a2@example.org"]: + condition: [] + member: user:a2@example.org + role: roles/compute.viewer + module.folder.google_folder_iam_member.additive["roles/storage.admin-user:am1@example.org"]: + condition: [] + member: user:am1@example.org + role: roles/storage.admin + module.folder.google_folder_iam_member.additive["roles/storage.objectViewer-user:am2@example.org"]: + condition: [] + member: user:am2@example.org + role: roles/storage.objectViewer + +counts: + google_folder: 1 + google_folder_iam_binding: 3 + google_folder_iam_member: 5 diff --git a/tests/modules/folder/examples/logging.yaml b/tests/modules/folder/examples/logging.yaml new file mode 100644 index 000000000..79b0e0078 --- /dev/null +++ b/tests/modules/folder/examples/logging.yaml @@ -0,0 +1,75 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.folder-sink.google_bigquery_dataset_iam_member.bq-sinks-binding["info"]: + role: roles/bigquery.dataEditor + module.folder-sink.google_folder.folder[0]: + display_name: my-folder + parent: folders/657104291943 + module.folder-sink.google_logging_folder_exclusion.logging-exclusion["no-gce-instances"]: + description: no-gce-instances (Terraform-managed). + filter: resource.type=gce_instance + name: no-gce-instances + module.folder-sink.google_logging_folder_sink.sink["debug"]: + disabled: false + exclusions: + - description: null + disabled: false + filter: logName:compute + name: no-compute + filter: severity=DEBUG + include_children: true + name: debug + module.folder-sink.google_logging_folder_sink.sink["info"]: + disabled: false + exclusions: [] + filter: severity=INFO + include_children: true + name: info + module.folder-sink.google_logging_folder_sink.sink["notice"]: + disabled: false + exclusions: [] + filter: severity=NOTICE + include_children: true + name: notice + module.folder-sink.google_logging_folder_sink.sink["warnings"]: + description: warnings (Terraform-managed). + destination: storage.googleapis.com/gcs_sink + disabled: false + exclusions: [] + filter: severity=WARNING + include_children: true + name: warnings + module.folder-sink.google_project_iam_member.bucket-sinks-binding["debug"]: + condition: + - title: debug bucket writer + role: roles/logging.bucketWriter + module.folder-sink.google_pubsub_topic_iam_member.pubsub-sinks-binding["notice"]: + condition: [] + role: roles/pubsub.publisher + module.folder-sink.google_storage_bucket_iam_member.gcs-sinks-binding["warnings"]: + bucket: gcs_sink + condition: [] + role: roles/storage.objectCreator + +counts: + google_bigquery_dataset_iam_member: 1 + google_folder: 1 + google_logging_folder_exclusion: 1 + google_logging_folder_sink: 4 + google_logging_project_bucket_config: 1 + google_project_iam_member: 1 + google_pubsub_topic_iam_member: 1 + google_storage_bucket_iam_member: 1 diff --git a/tests/modules/folder/examples/org-policies.yaml b/tests/modules/folder/examples/org-policies.yaml new file mode 100644 index 000000000..f8bf41879 --- /dev/null +++ b/tests/modules/folder/examples/org-policies.yaml @@ -0,0 +1,108 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.folder.google_folder.folder[0]: + display_name: Folder name + parent: organizations/1234567890 + module.folder.google_org_policy_policy.default["compute.disableGuestAttributesAccess"]: + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: 'TRUE' + values: [] + module.folder.google_org_policy_policy.default["constraints/compute.skipDefaultNetworkCreation"]: + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: 'TRUE' + values: [] + module.folder.google_org_policy_policy.default["constraints/compute.trustedImageProjects"]: + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: null + values: + - allowed_values: + - projects/my-project + denied_values: null + module.folder.google_org_policy_policy.default["constraints/compute.vmExternalIpAccess"]: + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: 'TRUE' + enforce: null + values: [] + module.folder.google_org_policy_policy.default["constraints/iam.allowedPolicyMemberDomains"]: + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: null + values: + - allowed_values: + - C0xxxxxxx + - C0yyyyyyy + denied_values: null + module.folder.google_org_policy_policy.default["iam.disableServiceAccountKeyCreation"]: + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: 'TRUE' + values: [] + module.folder.google_org_policy_policy.default["iam.disableServiceAccountKeyUpload"]: + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: + - description: test condition + expression: resource.matchTagId("tagKeys/1234", "tagValues/1234") + location: somewhere + title: condition + deny_all: null + enforce: 'TRUE' + values: [] + - allow_all: null + condition: [] + deny_all: null + enforce: 'FALSE' + values: [] + +counts: + google_folder: 1 + google_org_policy_policy: 7 diff --git a/tests/modules/folder/examples/tags.yaml b/tests/modules/folder/examples/tags.yaml new file mode 100644 index 000000000..047fea06d --- /dev/null +++ b/tests/modules/folder/examples/tags.yaml @@ -0,0 +1,41 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +tests/examples/test_plan.py::test_example[modules/folder:Tags] values: + module.folder.google_folder.folder[0]: + display_name: Test + parent: organizations/1122334455 + module.folder.google_tags_tag_binding.binding["env-prod"]: {} + module.folder.google_tags_tag_binding.binding["foo"]: + tag_value: tagValues/12345678 + module.org.google_tags_tag_key.default["environment"]: + description: Environment specification. + parent: organizations/1122334455 + purpose: null + purpose_data: null + short_name: environment + timeouts: null + module.org.google_tags_tag_value.default["environment/dev"]: + description: Managed by the Terraform organization module. + short_name: dev + module.org.google_tags_tag_value.default["environment/prod"]: + description: Managed by the Terraform organization module. + short_name: prod + +counts: + google_folder: 1 + google_tags_tag_binding: 2 + google_tags_tag_key: 1 + google_tags_tag_value: 2 diff --git a/tests/modules/folder/fixture/main.tf b/tests/modules/folder/fixture/main.tf deleted file mode 100644 index a347f61bb..000000000 --- a/tests/modules/folder/fixture/main.tf +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module "test" { - source = "../../../../modules/folder" - parent = "organizations/12345678" - name = "folder-a" - group_iam = var.group_iam - iam = var.iam - iam_additive = var.iam_additive - iam_additive_members = var.iam_additive_members - firewall_policies = var.firewall_policies - firewall_policy_association = var.firewall_policy_association - logging_sinks = var.logging_sinks - logging_exclusions = var.logging_exclusions - org_policies = var.org_policies - org_policies_data_path = var.org_policies_data_path -} diff --git a/tests/modules/folder/fixture/test.logging-sinks.tfvars b/tests/modules/folder/fixture/test.logging-sinks.tfvars deleted file mode 100644 index 95a272e1f..000000000 --- a/tests/modules/folder/fixture/test.logging-sinks.tfvars +++ /dev/null @@ -1,29 +0,0 @@ -logging_sinks = { - warning = { - destination = "mybucket" - type = "storage" - filter = "severity=WARNING" - } - info = { - destination = "projects/myproject/datasets/mydataset" - type = "bigquery" - filter = "severity=INFO" - disabled = true - } - notice = { - destination = "projects/myproject/topics/mytopic" - type = "pubsub" - filter = "severity=NOTICE" - include_children = false - } - debug = { - destination = "projects/myproject/locations/global/buckets/mybucket" - type = "logging" - filter = "severity=DEBUG" - include_children = false - exclusions = { - no-compute = "logName:compute" - no-container = "logName:container" - } - } -} diff --git a/tests/modules/folder/fixture/variables.tf b/tests/modules/folder/fixture/variables.tf deleted file mode 100644 index e2d7a293b..000000000 --- a/tests/modules/folder/fixture/variables.tf +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -variable "group_iam" { - type = any - default = {} -} - -variable "iam" { - type = any - default = {} -} - -variable "iam_additive" { - type = any - default = {} -} - -variable "iam_additive_members" { - type = any - default = {} -} - -variable "firewall_policies" { - type = any - default = {} -} - -variable "firewall_policy_association" { - type = any - default = {} -} - -variable "logging_sinks" { - type = any - default = {} -} - -variable "logging_exclusions" { - type = any - default = {} -} - -variable "org_policies" { - type = any - default = {} -} - -variable "org_policies_data_path" { - type = any - default = null -} diff --git a/tests/modules/folder/fixture/test.orgpolicies-boolean.tfvars b/tests/modules/folder/org_policies_boolean.tfvars similarity index 100% rename from tests/modules/folder/fixture/test.orgpolicies-boolean.tfvars rename to tests/modules/folder/org_policies_boolean.tfvars diff --git a/tests/modules/folder/fixture/test.orgpolicies-list.tfvars b/tests/modules/folder/org_policies_list.tfvars similarity index 100% rename from tests/modules/folder/fixture/test.orgpolicies-list.tfvars rename to tests/modules/folder/org_policies_list.tfvars diff --git a/tests/modules/folder/test_plan.py b/tests/modules/folder/test_plan.py deleted file mode 100644 index 0ce1ae4a8..000000000 --- a/tests/modules/folder/test_plan.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -def test_folder(plan_runner): - "Test folder resources." - _, resources = plan_runner() - assert len(resources) == 1 - resource = resources[0] - assert resource['type'] == 'google_folder' - assert resource['values']['display_name'] == 'folder-a' - assert resource['values']['parent'] == 'organizations/12345678' - - -def test_iam(plan_runner): - "Test IAM." - group_iam = ( - '{' - '"owners@example.org" = ["roles/owner", "roles/resourcemanager.folderAdmin"],' - '"viewers@example.org" = ["roles/viewer"]' - '}') - iam = ('{' - '"roles/owner" = ["user:one@example.org", "user:two@example.org"],' - '"roles/browser" = ["domain:example.org"]' - '}') - _, resources = plan_runner(group_iam=group_iam, iam=iam) - roles = sorted([(r['values']['role'], sorted(r['values']['members'])) - for r in resources - if r['type'] == 'google_folder_iam_binding']) - assert roles == [ - ('roles/browser', ['domain:example.org']), - ('roles/owner', [ - 'group:owners@example.org', 'user:one@example.org', - 'user:two@example.org' - ]), - ('roles/resourcemanager.folderAdmin', ['group:owners@example.org']), - ('roles/viewer', ['group:viewers@example.org']), - ] - - -def test_iam_multiple_roles(plan_runner): - "Test folder resources with multiple iam roles." - iam = ('{ ' - '"roles/owner" = ["user:a@b.com"], ' - '"roles/viewer" = ["user:c@d.com"] ' - '} ') - _, resources = plan_runner(iam=iam) - assert len(resources) == 3 - - -def test_iam_additive_members(plan_runner): - "Test IAM additive members." - iam = ('{"user:one@example.org" = ["roles/owner"],' - '"user:two@example.org" = ["roles/owner", "roles/editor"]}') - _, resources = plan_runner(iam_additive_members=iam) - roles = set((r['values']['role'], r['values']['member']) - for r in resources - if r['type'] == 'google_folder_iam_member') - assert roles == set([('roles/owner', 'user:one@example.org'), - ('roles/owner', 'user:two@example.org'), - ('roles/editor', 'user:two@example.org')]) diff --git a/tests/modules/folder/test_plan_firewall_policy.py b/tests/modules/folder/test_plan_firewall_policy.py deleted file mode 100644 index 4364fbdf1..000000000 --- a/tests/modules/folder/test_plan_firewall_policy.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -def test_firweall_policy(plan_runner): - "Test boolean folder policy." - policy = """ - { - policy1 = { - allow-ingress = { - description = "" - direction = "INGRESS" - action = "allow" - priority = 100 - ranges = ["10.0.0.0/8"] - ports = { - tcp = ["22"] - } - target_service_accounts = null - target_resources = null - logging = false - } - deny-egress = { - description = "" - direction = "EGRESS" - action = "deny" - priority = 200 - ranges = ["192.168.0.0/24"] - ports = { - tcp = ["443"] - } - target_service_accounts = null - target_resources = null - logging = false - } - } - } - """ - association = '{policy1="policy1"}' - _, resources = plan_runner(firewall_policies=policy, - firewall_policy_association=association) - assert len(resources) == 5 - - policies = [r for r in resources - if r['type'] == 'google_compute_firewall_policy'] - assert len(policies) == 1 - - rules = [r for r in resources - if r['type'] == 'google_compute_firewall_policy_rule'] - assert len(rules) == 2 - - rule_values = [] - for rule in rules: - name = rule['name'] - index = rule['index'] - action = rule['values']['action'] - direction = rule['values']['direction'] - priority = rule['values']['priority'] - match = rule['values']['match'] - rule_values.append((name, index, action, direction, priority, match)) - - assert sorted(rule_values) == sorted([ - ('rule', 'policy1-allow-ingress', 'allow', 'INGRESS', 100, [ - { - 'dest_ip_ranges': None, - 'layer4_configs': [{'ip_protocol': 'tcp', 'ports': ['22']}], - 'src_ip_ranges': ['10.0.0.0/8'] - }]), - ('rule', 'policy1-deny-egress', 'deny', 'EGRESS', 200, [ - { - 'dest_ip_ranges': ['192.168.0.0/24'], - 'layer4_configs': [{'ip_protocol': 'tcp', 'ports': ['443']}], - 'src_ip_ranges': None - }]) - ]) diff --git a/tests/modules/folder/test_plan_logging.py b/tests/modules/folder/test_plan_logging.py deleted file mode 100644 index 6b305d0b1..000000000 --- a/tests/modules/folder/test_plan_logging.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections import Counter - - -def test_sinks(plan_runner): - "Test folder-level sinks." - tfvars = 'test.logging-sinks.tfvars' - _, resources = plan_runner(tf_var_file=tfvars) - assert len(resources) == 9 - - resource_types = Counter([r["type"] for r in resources]) - assert resource_types == { - "google_logging_folder_sink": 4, - "google_folder": 1, - "google_bigquery_dataset_iam_member": 1, - "google_project_iam_member": 1, - "google_pubsub_topic_iam_member": 1, - "google_storage_bucket_iam_member": 1, - } - - sinks = [r for r in resources if r["type"] == "google_logging_folder_sink"] - assert sorted([r["index"] for r in sinks]) == [ - "debug", - "info", - "notice", - "warning", - ] - values = [( - r["index"], - r["values"]["filter"], - r["values"]["destination"], - r["values"]["description"], - r["values"]["include_children"], - r["values"]["disabled"], - ) for r in sinks] - assert sorted(values) == [ - ("debug", "severity=DEBUG", - "logging.googleapis.com/projects/myproject/locations/global/buckets/mybucket", - "debug (Terraform-managed).", False, False), - ("info", "severity=INFO", - "bigquery.googleapis.com/projects/myproject/datasets/mydataset", - "info (Terraform-managed).", True, True), - ("notice", "severity=NOTICE", - "pubsub.googleapis.com/projects/myproject/topics/mytopic", - "notice (Terraform-managed).", False, False), - ("warning", "severity=WARNING", "storage.googleapis.com/mybucket", - "warning (Terraform-managed).", True, False), - ] - - bindings = [r for r in resources if "member" in r["type"]] - values = [(r["index"], r["type"], r["values"]["role"], - r["values"]["condition"]) for r in bindings] - assert sorted(values) == [ - ("debug", "google_project_iam_member", "roles/logging.bucketWriter", [{ - 'expression': - "resource.name.endsWith('projects/myproject/locations/global/buckets/mybucket')", - 'title': - 'debug bucket writer' - }]), - ("info", "google_bigquery_dataset_iam_member", - "roles/bigquery.dataEditor", []), - ("notice", "google_pubsub_topic_iam_member", "roles/pubsub.publisher", - []), - ("warning", "google_storage_bucket_iam_member", - "roles/storage.objectCreator", []), - ] - - exclusions = [(r["index"], r["values"]["exclusions"]) for r in sinks] - assert sorted(exclusions) == [ - ("debug", [{ - "description": None, - "disabled": False, - "filter": "logName:compute", - "name": "no-compute" - }, { - "description": None, - "disabled": False, - "filter": "logName:container", - "name": "no-container" - }]), - ("info", []), - ("notice", []), - ("warning", []), - ] - - -def test_exclusions(plan_runner): - "Test folder-level logging exclusions." - logging_exclusions = ("{" - 'exclusion1 = "resource.type=gce_instance", ' - 'exclusion2 = "severity=NOTICE", ' - "}") - _, resources = plan_runner(logging_exclusions=logging_exclusions) - assert len(resources) == 3 - exclusions = [ - r for r in resources if r["type"] == "google_logging_folder_exclusion" - ] - assert sorted([r["index"] for r in exclusions]) == [ - "exclusion1", - "exclusion2", - ] - values = [(r["index"], r["values"]["filter"]) for r in exclusions] - assert sorted(values) == [ - ("exclusion1", "resource.type=gce_instance"), - ("exclusion2", "severity=NOTICE"), - ] diff --git a/tests/modules/folder/test_plan_org_policies.py b/tests/modules/folder/test_plan_org_policies.py index 8463761e8..161845376 100644 --- a/tests/modules/folder/test_plan_org_policies.py +++ b/tests/modules/folder/test_plan_org_policies.py @@ -12,33 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .validate_policies import validate_policy_boolean, validate_policy_list +import pytest + +_params = ['boolean', 'list'] -def test_policy_boolean(plan_runner): - "Test boolean org policy." - tfvars = 'test.orgpolicies-boolean.tfvars' - _, resources = plan_runner(tf_var_file=tfvars) - validate_policy_boolean(resources) - - -def test_policy_list(plan_runner): - "Test list org policy." - tfvars = 'test.orgpolicies-list.tfvars' - _, resources = plan_runner(tf_var_file=tfvars) - validate_policy_list(resources) - - -def test_factory_policy_boolean(plan_runner, tfvars_to_yaml, tmp_path): +@pytest.mark.parametrize('policy_type', _params) +def test_policy_factory(plan_summary, tfvars_to_yaml, tmp_path, policy_type): dest = tmp_path / 'policies.yaml' - tfvars_to_yaml('fixture/test.orgpolicies-boolean.tfvars', dest, - 'org_policies') - _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') - validate_policy_boolean(resources) - - -def test_factory_policy_list(plan_runner, tfvars_to_yaml, tmp_path): - dest = tmp_path / 'policies.yaml' - tfvars_to_yaml('fixture/test.orgpolicies-list.tfvars', dest, 'org_policies') - _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') - validate_policy_list(resources) + tfvars_to_yaml(f'org_policies_{policy_type}.tfvars', dest, 'org_policies') + tfvars_plan = plan_summary( + 'modules/folder', + tf_var_files=['common.tfvars', f'org_policies_{policy_type}.tfvars']) + yaml_plan = plan_summary('modules/folder', tf_var_files=['common.tfvars'], + org_policies_data_path=f'{tmp_path}') + assert tfvars_plan.values == yaml_plan.values diff --git a/tests/modules/folder/validate_policies.py b/tests/modules/folder/validate_policies.py deleted file mode 100644 index 385898b17..000000000 --- a/tests/modules/folder/validate_policies.py +++ /dev/null @@ -1,158 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -def validate_policy_boolean(resources): - assert len(resources) == 3 - policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] - assert len(policies) == 2 - - p1 = [ - r['values']['spec'][0] - for r in policies - if r['index'] == 'iam.disableServiceAccountKeyCreation' - ][0] - - assert p1['inherit_from_parent'] is None - assert p1['reset'] is None - assert p1['rules'] == [{ - 'allow_all': None, - 'condition': [], - 'deny_all': None, - 'enforce': 'TRUE', - 'values': [] - }] - - p2 = [ - r['values']['spec'][0] - for r in policies - if r['index'] == 'iam.disableServiceAccountKeyUpload' - ][0] - - assert p2['inherit_from_parent'] is None - assert p2['reset'] is None - assert len(p2['rules']) == 2 - assert p2['rules'][0] == { - 'allow_all': None, - 'condition': [], - 'deny_all': None, - 'enforce': 'FALSE', - 'values': [] - } - assert p2['rules'][1] == { - 'allow_all': None, - 'condition': [{ - 'description': 'test condition', - 'expression': 'resource.matchTagId(aa, bb)', - 'location': 'xxx', - 'title': 'condition' - }], - 'deny_all': None, - 'enforce': 'TRUE', - 'values': [] - } - - -def validate_policy_list(resources): - assert len(resources) == 4 - - policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] - assert len(policies) == 3 - - p1 = [ - r['values']['spec'][0] - for r in policies - if r['index'] == 'compute.vmExternalIpAccess' - ][0] - assert p1['inherit_from_parent'] is None - assert p1['reset'] is None - assert p1['rules'] == [{ - 'allow_all': None, - 'condition': [], - 'deny_all': 'TRUE', - 'enforce': None, - 'values': [] - }] - - p2 = [ - r['values']['spec'][0] - for r in policies - if r['index'] == 'iam.allowedPolicyMemberDomains' - ][0] - assert p2['inherit_from_parent'] is None - assert p2['reset'] is None - assert p2['rules'] == [{ - 'allow_all': - None, - 'condition': [], - 'deny_all': - None, - 'enforce': - None, - 'values': [{ - 'allowed_values': [ - 'C0xxxxxxx', - 'C0yyyyyyy', - ], - 'denied_values': None - }] - }] - - p3 = [ - r['values']['spec'][0] - for r in policies - if r['index'] == 'compute.restrictLoadBalancerCreationForTypes' - ][0] - assert p3['inherit_from_parent'] is None - assert p3['reset'] is None - assert len(p3['rules']) == 3 - assert p3['rules'][0] == { - 'allow_all': None, - 'condition': [], - 'deny_all': None, - 'enforce': None, - 'values': [{ - 'allowed_values': None, - 'denied_values': ['in:EXTERNAL'] - }] - } - - assert p3['rules'][1] == { - 'allow_all': None, - 'condition': [{ - 'description': 'test condition', - 'expression': 'resource.matchTagId(aa, bb)', - 'location': 'xxx', - 'title': 'condition' - }], - 'deny_all': None, - 'enforce': None, - 'values': [{ - 'allowed_values': ['EXTERNAL_1'], - 'denied_values': None - }] - } - - assert p3['rules'][2] == { - 'allow_all': 'TRUE', - 'condition': [{ - 'description': 'test condition2', - 'expression': 'resource.matchTagId(cc, dd)', - 'location': 'xxx', - 'title': 'condition2' - }], - 'deny_all': None, - 'enforce': None, - 'values': [] - } diff --git a/tests/modules/gcs/examples/cmek.yaml b/tests/modules/gcs/examples/cmek.yaml new file mode 100644 index 000000000..ee92a5d22 --- /dev/null +++ b/tests/modules/gcs/examples/cmek.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.bucket.google_storage_bucket.bucket: + encryption: + - default_kms_key_name: my-encryption-key + name: my-bucket + project: myproject + +counts: + google_storage_bucket: 1 diff --git a/tests/modules/gcs/examples/lifecycle.yaml b/tests/modules/gcs/examples/lifecycle.yaml new file mode 100644 index 000000000..69eeea41f --- /dev/null +++ b/tests/modules/gcs/examples/lifecycle.yaml @@ -0,0 +1,38 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.bucket.google_storage_bucket.bucket: + lifecycle_rule: + - action: + - storage_class: STANDARD + type: SetStorageClass + condition: + - age: 30 + created_before: '' + custom_time_before: '' + days_since_custom_time: null + days_since_noncurrent_time: null + matches_prefix: [] + matches_storage_class: [] + matches_suffix: [] + noncurrent_time_before: '' + num_newer_versions: null + name: my-bucket + project: myproject + +counts: + google_storage_bucket: 1 + +outputs: {} diff --git a/tests/modules/gcs/examples/notification.yaml b/tests/modules/gcs/examples/notification.yaml new file mode 100644 index 000000000..9536e89b4 --- /dev/null +++ b/tests/modules/gcs/examples/notification.yaml @@ -0,0 +1,31 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.bucket-gcs-notification.google_pubsub_topic.topic[0]: {} + module.bucket-gcs-notification.google_pubsub_topic_iam_binding.binding[0]: {} + module.bucket-gcs-notification.google_storage_bucket.bucket: + name: my-bucket + project: myproject + module.bucket-gcs-notification.google_storage_notification.notification[0]: + bucket: my-bucket + event_types: + - OBJECT_FINALIZE + payload_format: JSON_API_V1 + +counts: + google_pubsub_topic: 1 + google_pubsub_topic_iam_binding: 1 + google_storage_bucket: 1 + google_storage_notification: 1 diff --git a/tests/modules/gcs/examples/retention-logging.yaml b/tests/modules/gcs/examples/retention-logging.yaml new file mode 100644 index 000000000..962414207 --- /dev/null +++ b/tests/modules/gcs/examples/retention-logging.yaml @@ -0,0 +1,26 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.bucket.google_storage_bucket.bucket: + logging: + - log_bucket: log-bucket + name: my-bucket + project: myproject + retention_policy: + - is_locked: true + retention_period: 100 + +counts: + google_storage_bucket: 1 diff --git a/tests/modules/gcs/examples/simple.yaml b/tests/modules/gcs/examples/simple.yaml new file mode 100644 index 000000000..bc2630b87 --- /dev/null +++ b/tests/modules/gcs/examples/simple.yaml @@ -0,0 +1,46 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.bucket.google_storage_bucket.bucket: + autoclass: [] + cors: [] + custom_placement_config: [] + default_event_based_hold: null + encryption: [] + force_destroy: false + labels: + cost-center: devops + lifecycle_rule: [] + location: EU + logging: [] + name: test-my-bucket + project: myproject + requester_pays: null + retention_policy: [] + storage_class: MULTI_REGIONAL + timeouts: null + uniform_bucket_level_access: true + versioning: + - enabled: true + module.bucket.google_storage_bucket_iam_binding.bindings["roles/storage.admin"]: + bucket: test-my-bucket + condition: [] + members: + - group:storage@example.com + role: roles/storage.admin + +counts: + google_storage_bucket: 1 + google_storage_bucket_iam_binding: 1 diff --git a/tests/modules/gcs/fixture/main.tf b/tests/modules/gcs/fixture/main.tf deleted file mode 100644 index ea2e994f6..000000000 --- a/tests/modules/gcs/fixture/main.tf +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module "test" { - source = "../../../../modules/gcs" - project_id = "my-project" - uniform_bucket_level_access = var.uniform_bucket_level_access - force_destroy = var.force_destroy - iam = var.iam - labels = var.labels - logging_config = var.logging_config - name = "bucket-a" - prefix = var.prefix - retention_policy = var.retention_policy - versioning = var.versioning -} diff --git a/tests/modules/gcs/fixture/variables.tf b/tests/modules/gcs/fixture/variables.tf deleted file mode 100644 index 455d9a4bb..000000000 --- a/tests/modules/gcs/fixture/variables.tf +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -variable "uniform_bucket_level_access" { - type = bool - default = false -} - -variable "force_destroy" { - type = bool - default = true -} - -variable "iam" { - type = map(list(string)) - default = {} -} - -variable "labels" { - type = map(string) - default = { environment = "test" } -} - -variable "logging_config" { - type = object({ - log_bucket = string - log_object_prefix = string - }) - default = { - log_bucket = "foo" - log_object_prefix = null - } -} - -variable "prefix" { - type = string - default = null -} - -variable "project_id" { - type = string - default = "my-project" -} - -variable "retention_policy" { - type = object({ - retention_period = number - is_locked = bool - }) - default = { - retention_period = 5 - is_locked = false - } -} - -variable "storage_class" { - type = string - default = "MULTI_REGIONAL" -} - -variable "versioning" { - type = bool - default = true -} diff --git a/tests/modules/gcs/test_plan.py b/tests/modules/gcs/test_plan.py deleted file mode 100644 index 22775a589..000000000 --- a/tests/modules/gcs/test_plan.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -def test_buckets(plan_runner): - "Test bucket resources." - _, resources = plan_runner() - assert len(resources) == 1 - r = resources[0] - assert r['type'] == 'google_storage_bucket' - assert r['values']['name'] == 'bucket-a' - assert r['values']['project'] == 'my-project' - - -def test_prefix(plan_runner): - "Test bucket name when prefix is set." - _, resources = plan_runner(prefix='foo') - assert resources[0]['values']['name'] == 'foo-bucket-a' - - -def test_config_values(plan_runner): - "Test that variables set the correct attributes on buckets." - variables = dict( - uniform_bucket_level_access='true', - force_destroy='true', - versioning='true' - ) - _, resources = plan_runner(**variables) - assert len(resources) == 1 - r = resources[0] - assert r['values']['uniform_bucket_level_access'] is True - assert r['values']['force_destroy'] is True - assert r['values']['versioning'] == [{'enabled': True}] - assert r['values']['logging'] == [{'log_bucket': 'foo'}] - assert r['values']['retention_policy'] == [ - {'is_locked': False, 'retention_period': 5} - ] - - -def test_iam(plan_runner): - "Test bucket resources with iam roles and members." - iam = '{ "roles/storage.admin" = ["user:a@b.com"] }' - _, resources = plan_runner(iam=iam) - assert len(resources) == 2 diff --git a/tests/modules/gke_cluster/examples/autopilot.yaml b/tests/modules/gke_cluster/examples/autopilot.yaml new file mode 100644 index 000000000..0a5380dbb --- /dev/null +++ b/tests/modules/gke_cluster/examples/autopilot.yaml @@ -0,0 +1,32 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.cluster-autopilot.google_container_cluster.cluster: + enable_autopilot: true + ip_allocation_policy: + - cluster_secondary_range_name: pods + services_secondary_range_name: services + location: europe-west1-b + master_authorized_networks_config: + - cidr_blocks: + - cidr_block: 10.0.0.0/8 + display_name: internal-vms + name: cluster-autopilot + network: projects/xxx/global/networks/aaa + project: myproject + subnetwork: subnet_self_link + +counts: + google_container_cluster: 1 diff --git a/tests/modules/gke_cluster/examples/basic.yaml b/tests/modules/gke_cluster/examples/basic.yaml new file mode 100644 index 000000000..fe6648c8d --- /dev/null +++ b/tests/modules/gke_cluster/examples/basic.yaml @@ -0,0 +1,42 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.cluster-1.google_container_cluster.cluster: + default_max_pods_per_node: 32 + ip_allocation_policy: + - cluster_secondary_range_name: pods + services_secondary_range_name: services + location: europe-west1-b + master_authorized_networks_config: + - cidr_blocks: + - cidr_block: 10.0.0.0/8 + display_name: internal-vms + name: cluster-1 + network: projects/xxx/global/networks/aaa + private_cluster_config: + - enable_private_endpoint: true + enable_private_nodes: true + master_global_access_config: + - enabled: false + master_ipv4_cidr_block: 192.168.0.0/28 + private_endpoint_subnetwork: null + project: myproject + remove_default_node_pool: true + resource_labels: + environment: dev + subnetwork: subnet_self_link + +counts: + google_container_cluster: 1 diff --git a/tests/modules/gke_cluster/examples/dataplane-v2.yaml b/tests/modules/gke_cluster/examples/dataplane-v2.yaml new file mode 100644 index 000000000..ef7ca642f --- /dev/null +++ b/tests/modules/gke_cluster/examples/dataplane-v2.yaml @@ -0,0 +1,45 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.cluster-1.google_container_cluster.cluster: + datapath_provider: ADVANCED_DATAPATH + ip_allocation_policy: + - cluster_secondary_range_name: pods + services_secondary_range_name: services + location: europe-west1-b + master_authorized_networks_config: + - cidr_blocks: + - cidr_block: 10.0.0.0/8 + display_name: internal-vms + min_master_version: null + name: cluster-dataplane-v2 + network: projects/xxx/global/networks/aaa + private_cluster_config: + - enable_private_endpoint: true + enable_private_nodes: true + master_global_access_config: + - enabled: false + master_ipv4_cidr_block: 192.168.0.0/28 + private_endpoint_subnetwork: null + project: myproject + remove_default_node_pool: true + resource_labels: + environment: dev + subnetwork: subnet_self_link + workload_identity_config: + - workload_pool: myproject.svc.id.goog + +counts: + google_container_cluster: 1 diff --git a/tests/modules/gke_cluster/examples/dns.yaml b/tests/modules/gke_cluster/examples/dns.yaml new file mode 100644 index 000000000..53792e051 --- /dev/null +++ b/tests/modules/gke_cluster/examples/dns.yaml @@ -0,0 +1,28 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.cluster-1.google_container_cluster.cluster: + dns_config: + - cluster_dns: CLOUD_DNS + cluster_dns_domain: gke.local + cluster_dns_scope: CLUSTER_SCOPE + ip_allocation_policy: + - cluster_secondary_range_name: pods + services_secondary_range_name: services + location: europe-west1-b + name: cluster-1 + +counts: + google_container_cluster: 1 diff --git a/tests/modules/gke_cluster/test_plan.py b/tests/modules/gke_cluster/test_plan.py deleted file mode 100644 index acd97bede..000000000 --- a/tests/modules/gke_cluster/test_plan.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -def test_standard(plan_runner): - "Test resources created with variable defaults." - _, resources = plan_runner() - assert len(resources) == 1 - - cluster_config = resources[0]['values'] - assert cluster_config['name'] == "cluster-1" - assert cluster_config['network'] == "mynetwork" - assert cluster_config['subnetwork'] == "mysubnet" - assert cluster_config['enable_autopilot'] is None - # assert 'service_account' not in node_config - - -def test_autopilot(plan_runner): - "Test resources created with variable defaults." - _, resources = plan_runner(enable_features='{ autopilot=true }') - assert len(resources) == 1 - cluster_config = resources[0]['values'] - assert cluster_config['name'] == "cluster-1" - assert cluster_config['network'] == "mynetwork" - assert cluster_config['subnetwork'] == "mysubnet" - assert cluster_config['enable_autopilot'] == True - # assert 'service_account' not in node_config diff --git a/tests/modules/gke_hub/fixture/variables.tf b/tests/modules/gke_hub/fixture/variables.tf index 5c5c106f2..1d76d4f97 100644 --- a/tests/modules/gke_hub/fixture/variables.tf +++ b/tests/modules/gke_hub/fixture/variables.tf @@ -31,7 +31,7 @@ variable "features" { configmanagement = true identityservice = false multiclusteringress = null - servicemesh = false + servicemesh = true multiclusterservicediscovery = false } } diff --git a/tests/modules/gke_hub/test_plan.py b/tests/modules/gke_hub/test_plan.py index 355218134..8a71d12b5 100644 --- a/tests/modules/gke_hub/test_plan.py +++ b/tests/modules/gke_hub/test_plan.py @@ -23,11 +23,14 @@ def resources(plan_runner): def test_resource_count(resources): "Test number of resources created." - assert len(resources) == 5 + assert len(resources) == 8 assert sorted(r['address'] for r in resources) == [ 'module.hub.google_gke_hub_feature.default["configmanagement"]', + 'module.hub.google_gke_hub_feature.default["servicemesh"]', 'module.hub.google_gke_hub_feature_membership.default["cluster-1"]', 'module.hub.google_gke_hub_feature_membership.default["cluster-2"]', + 'module.hub.google_gke_hub_feature_membership.servicemesh["cluster-1"]', + 'module.hub.google_gke_hub_feature_membership.servicemesh["cluster-2"]', 'module.hub.google_gke_hub_membership.default["cluster-1"]', 'module.hub.google_gke_hub_membership.default["cluster-2"]' ] @@ -58,6 +61,7 @@ def test_configmanagement_setup(resources): 'sync_wait_secs': None }], + 'oci': [], 'prevent_drift': False, 'source_format': 'hierarchy' }], diff --git a/tests/modules/gke_nodepool/examples/basic.yaml b/tests/modules/gke_nodepool/examples/basic.yaml new file mode 100644 index 000000000..010b98cda --- /dev/null +++ b/tests/modules/gke_nodepool/examples/basic.yaml @@ -0,0 +1,23 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.cluster-1-nodepool-1.google_container_node_pool.nodepool: + cluster: cluster-1 + location: europe-west1-b + name: nodepool-1 + project: myproject + +counts: + google_container_node_pool: 1 diff --git a/tests/modules/gke_nodepool/examples/config.yaml b/tests/modules/gke_nodepool/examples/config.yaml new file mode 100644 index 000000000..858e5ca58 --- /dev/null +++ b/tests/modules/gke_nodepool/examples/config.yaml @@ -0,0 +1,59 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.cluster-1-nodepool-1.google_container_node_pool.nodepool: + autoscaling: + - max_node_count: 10 + min_node_count: 1 + total_max_node_count: null + total_min_node_count: null + cluster: cluster-1 + initial_node_count: 1 + location: europe-west1-b + management: + - auto_repair: true + auto_upgrade: false + name: nodepool-1 + node_config: + - boot_disk_kms_key: null + disk_size_gb: 50 + disk_type: pd-ssd + ephemeral_storage_config: + - local_ssd_count: 1 + gcfs_config: [] + gvnic: [] + kubelet_config: [] + labels: + environment: dev + linux_node_config: [] + logging_variant: DEFAULT + machine_type: n2-standard-2 + node_group: null + oauth_scopes: + - https://www.googleapis.com/auth/cloud-platform + preemptible: false + reservation_affinity: [] + resource_labels: null + sandbox_config: [] + spot: true + tags: null + taint: [] + placement_policy: [] + project: myproject + module.cluster-1-nodepool-1.google_service_account.service_account[0]: {} + +counts: + google_container_node_pool: 1 + google_service_account: 1 diff --git a/tests/modules/gke_nodepool/examples/create-sa.yaml b/tests/modules/gke_nodepool/examples/create-sa.yaml new file mode 100644 index 000000000..df1f2f708 --- /dev/null +++ b/tests/modules/gke_nodepool/examples/create-sa.yaml @@ -0,0 +1,52 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.cluster-1-nodepool-1.google_container_node_pool.nodepool: + cluster: cluster-1 + location: europe-west1-b + name: nodepool-1 + node_config: + - boot_disk_kms_key: null + disk_type: pd-balanced + ephemeral_storage_config: [] + gcfs_config: [] + gvnic: [] + kubelet_config: [] + linux_node_config: [] + logging_variant: DEFAULT + node_group: null + oauth_scopes: + - https://www.googleapis.com/auth/cloud-platform + preemptible: false + reservation_affinity: [] + resource_labels: null + sandbox_config: [] + spot: false + tags: null + taint: [] + placement_policy: [] + project: myproject + timeouts: null + module.cluster-1-nodepool-1.google_service_account.service_account[0]: + account_id: spam-eggs + description: null + disabled: false + display_name: Terraform GKE cluster-1 nodepool-1. + project: myproject + timeouts: null + +counts: + google_container_node_pool: 1 + google_service_account: 1 diff --git a/tests/modules/gke_nodepool/examples/external-sa.yaml b/tests/modules/gke_nodepool/examples/external-sa.yaml new file mode 100644 index 000000000..059593215 --- /dev/null +++ b/tests/modules/gke_nodepool/examples/external-sa.yaml @@ -0,0 +1,43 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.cluster-1-nodepool-1.google_container_node_pool.nodepool: + cluster: cluster-1 + location: europe-west1-b + name: nodepool-1 + node_config: + - boot_disk_kms_key: null + disk_type: pd-balanced + ephemeral_storage_config: [] + gcfs_config: [] + gvnic: [] + kubelet_config: [] + linux_node_config: [] + logging_variant: DEFAULT + node_group: null + oauth_scopes: + - https://www.googleapis.com/auth/cloud-platform + preemptible: false + reservation_affinity: [] + resource_labels: null + sandbox_config: [] + service_account: foo-bar@myproject.iam.gserviceaccount.com + spot: false + tags: null + taint: [] + project: myproject + +counts: + google_container_node_pool: 1 diff --git a/tests/modules/gke_nodepool/fixture/main.tf b/tests/modules/gke_nodepool/fixture/main.tf deleted file mode 100644 index 4ee274828..000000000 --- a/tests/modules/gke_nodepool/fixture/main.tf +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -resource "google_service_account" "test" { - project = "my-project" - account_id = "gke-nodepool-test" - display_name = "Test Service Account" -} - -module "test" { - source = "../../../../modules/gke-nodepool" - project_id = "my-project" - cluster_name = "cluster-1" - location = "europe-west1-b" - name = "nodepool-1" - gke_version = var.gke_version - labels = var.labels - max_pods_per_node = var.max_pods_per_node - node_config = var.node_config - node_count = var.node_count - node_locations = var.node_locations - nodepool_config = var.nodepool_config - pod_range = var.pod_range - reservation_affinity = var.reservation_affinity - service_account = { - create = var.service_account_create - email = google_service_account.test.email - } - sole_tenant_nodegroup = var.sole_tenant_nodegroup - tags = var.tags - taints = var.taints -} diff --git a/tests/modules/gke_nodepool/fixture/variables.tf b/tests/modules/gke_nodepool/fixture/variables.tf deleted file mode 100644 index 18376ec53..000000000 --- a/tests/modules/gke_nodepool/fixture/variables.tf +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -variable "gke_version" { - type = string - default = null -} - -variable "labels" { - type = map(string) - default = {} - nullable = false -} - -variable "max_pods_per_node" { - type = number - default = null -} - -variable "node_config" { - type = any - default = { - disk_type = "pd-balanced" - } -} - -variable "node_count" { - type = any - default = { - initial = 1 - } - nullable = false -} - -variable "node_locations" { - type = list(string) - default = null -} - -variable "nodepool_config" { - type = any - default = null -} - -variable "pod_range" { - type = any - default = null -} - -variable "reservation_affinity" { - type = any - default = null -} - -variable "service_account_create" { - type = bool - default = false -} - -variable "sole_tenant_nodegroup" { - type = string - default = null -} - -variable "tags" { - type = list(string) - default = null -} - -variable "taints" { - type = any - default = null -} diff --git a/tests/modules/gke_nodepool/test_plan.py b/tests/modules/gke_nodepool/test_plan.py deleted file mode 100644 index 75d1cc14b..000000000 --- a/tests/modules/gke_nodepool/test_plan.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -def test_defaults(plan_runner): - "Test resources created with variable defaults." - _, resources = plan_runner() - assert len(resources) == 1 - assert resources[0]['values']['autoscaling'] == [] - - -def test_service_account(plan_runner): - _, resources = plan_runner() - assert len(resources) == 1 - _, resources = plan_runner(service_account_create='true') - assert len(resources) == 2 - assert 'google_service_account' in [r['type'] for r in resources] - - -def test_nodepool_config(plan_runner): - nodepool_config = '''{ - autoscaling = { use_total_nodes = true, max_node_count = 3} - management = {} - upgrade_settings = { max_surge = 3, max_unavailable = 3 } - }''' - _, resources = plan_runner(nodepool_config=nodepool_config) - assert resources[0]['values']['autoscaling'] == [{ - 'location_policy': None, - 'max_node_count': None, - 'min_node_count': None, - 'total_max_node_count': 3, - 'total_min_node_count': None - }] - nodepool_config = '{ autoscaling = { max_node_count = 3} }' - _, resources = plan_runner(nodepool_config=nodepool_config) - assert resources[0]['values']['autoscaling'] == [{ - 'location_policy': None, - 'max_node_count': 3, - 'min_node_count': None, - 'total_max_node_count': None, - 'total_min_node_count': None - }] - - -def test_node_config(plan_runner): - node_config = '''{ - gcfs = true - metadata = { foo = "bar" } - }''' - _, resources = plan_runner(node_config=node_config) - values = resources[0]['values']['node_config'][0] - assert values['gcfs_config'] == [{'enabled': True}] - assert values['metadata'] == { - 'disable-legacy-endpoints': 'true', - 'foo': 'bar' - } diff --git a/tests/modules/iam_service_account/examples/basic.yaml b/tests/modules/iam_service_account/examples/basic.yaml new file mode 100644 index 000000000..4acc58519 --- /dev/null +++ b/tests/modules/iam_service_account/examples/basic.yaml @@ -0,0 +1,39 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +values: + module.myproject-default-service-accounts.google_project_iam_member.project-roles["myproject-roles/logging.logWriter"]: + condition: [] + project: myproject + role: roles/logging.logWriter + module.myproject-default-service-accounts.google_project_iam_member.project-roles["myproject-roles/monitoring.metricWriter"]: + condition: [] + project: myproject + role: roles/monitoring.metricWriter + module.myproject-default-service-accounts.google_service_account.service_account[0]: + account_id: vm-default + description: null + disabled: false + display_name: Terraform-managed. + project: myproject + timeouts: null + module.myproject-default-service-accounts.google_service_account_iam_binding.roles["roles/iam.serviceAccountUser"]: + condition: [] + members: + - user:foo@example.com + role: roles/iam.serviceAccountUser + +counts: + google_project_iam_member: 2 + google_service_account: 1 + google_service_account_iam_binding: 1 diff --git a/tests/modules/iam_service_account/fixture/main.tf b/tests/modules/iam_service_account/fixture/main.tf deleted file mode 100644 index 535139836..000000000 --- a/tests/modules/iam_service_account/fixture/main.tf +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module "test" { - source = "../../../../modules/iam-service-account" - project_id = var.project_id - name = "sa-one" - prefix = var.prefix - generate_key = var.generate_key - iam = var.iam - iam_billing_roles = var.iam_billing_roles - iam_folder_roles = var.iam_folder_roles - iam_organization_roles = var.iam_organization_roles - iam_project_roles = var.iam_project_roles - iam_storage_roles = var.iam_storage_roles -} diff --git a/tests/modules/iam_service_account/fixture/variables.tf b/tests/modules/iam_service_account/fixture/variables.tf deleted file mode 100644 index 0a4781e07..000000000 --- a/tests/modules/iam_service_account/fixture/variables.tf +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -variable "generate_key" { - type = bool - default = false -} - -variable "iam" { - type = map(list(string)) - default = {} -} - -variable "iam_billing_roles" { - type = map(list(string)) - default = {} -} - -variable "iam_folder_roles" { - type = map(list(string)) - default = {} -} - -variable "iam_organization_roles" { - type = map(list(string)) - default = {} -} - -variable "iam_project_roles" { - type = map(list(string)) - default = {} -} - -variable "iam_storage_roles" { - type = map(list(string)) - default = {} -} - -variable "prefix" { - type = string - default = null -} - -variable "project_id" { - type = string - default = "my-project" -} diff --git a/tests/modules/iam_service_account/test_plan.py b/tests/modules/iam_service_account/test_plan.py deleted file mode 100644 index ff7865b5b..000000000 --- a/tests/modules/iam_service_account/test_plan.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -def test_resources(plan_runner): - "Test service account resource." - _, resources = plan_runner() - assert len(resources) == 1 - resource = resources[0] - assert resource['type'] == 'google_service_account' - assert resource['values']['account_id'] == 'sa-one' - - _, resources = plan_runner(prefix='foo') - assert len(resources) == 1 - resource = resources[0] - assert resource['values']['account_id'] == 'foo-sa-one' - - -def test_iam_roles(plan_runner): - "Test iam roles with one member." - iam = ('{"roles/iam.serviceAccountUser" = ["user:a@b.com"]}') - _, resources = plan_runner(iam=iam) - assert len(resources) == 2 - iam_resources = [r for r in resources - if r['type'] != 'google_service_account'] - assert len(iam_resources) == 1 - - iam_resource = iam_resources[0] - assert iam_resource['type'] == 'google_service_account_iam_binding' - assert iam_resource['index'] == 'roles/iam.serviceAccountUser' - assert iam_resource['values']['role'] == 'roles/iam.serviceAccountUser' - assert iam_resource['values']['members'] == ["user:a@b.com"] diff --git a/tests/modules/logging_bucket/test_plan.py b/tests/modules/logging_bucket/test_plan.py index 6c309e340..8ec685add 100644 --- a/tests/modules/logging_bucket/test_plan.py +++ b/tests/modules/logging_bucket/test_plan.py @@ -12,16 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. + def test_project_logging_bucket(plan_runner): "Test project logging bucket." - _, resources = plan_runner(parent_type="project", - parent="myproject") + _, resources = plan_runner(parent_type="project", parent="myproject") assert len(resources) == 1 resource = resources[0] assert resource["type"] == "google_logging_project_bucket_config" assert resource["values"] == { "bucket_id": "mybucket", + "cmek_settings": [], "project": "myproject", "location": "global", "retention_days": 30, @@ -30,15 +31,14 @@ def test_project_logging_bucket(plan_runner): def test_folder_logging_bucket(plan_runner): "Test project logging bucket." - _, resources = plan_runner( - parent_type="folder", parent="folders/0123456789" - ) + _, resources = plan_runner(parent_type="folder", parent="folders/0123456789") assert len(resources) == 1 resource = resources[0] assert resource["type"] == "google_logging_folder_bucket_config" assert resource["values"] == { "bucket_id": "mybucket", + "cmek_settings": [], "folder": "folders/0123456789", "location": "global", "retention_days": 30, @@ -47,15 +47,15 @@ def test_folder_logging_bucket(plan_runner): def test_organization_logging_bucket(plan_runner): "Test project logging bucket." - _, resources = plan_runner( - parent_type="organization", parent="organizations/0123456789" - ) + _, resources = plan_runner(parent_type="organization", + parent="organizations/0123456789") assert len(resources) == 1 resource = resources[0] assert resource["type"] == "google_logging_organization_bucket_config" assert resource["values"] == { "bucket_id": "mybucket", + "cmek_settings": [], "organization": "organizations/0123456789", "location": "global", "retention_days": 30, @@ -64,15 +64,14 @@ def test_organization_logging_bucket(plan_runner): def test_billing_account_logging_bucket(plan_runner): "Test project logging bucket." - _, resources = plan_runner( - parent_type="billing_account", parent="0123456789" - ) + _, resources = plan_runner(parent_type="billing_account", parent="0123456789") assert len(resources) == 1 resource = resources[0] assert resource["type"] == "google_logging_billing_account_bucket_config" assert resource["values"] == { "bucket_id": "mybucket", + "cmek_settings": [], "billing_account": "0123456789", "location": "global", "retention_days": 30, diff --git a/tests/modules/net_glb/examples/https-sneg.yaml b/tests/modules/net_glb/examples/https-sneg.yaml new file mode 100644 index 000000000..fa0823cbf --- /dev/null +++ b/tests/modules/net_glb/examples/https-sneg.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: + module.glb-0.google_compute_backend_service.default["default"]: + port_name: http + protocol: HTTPS + module.glb-0.google_compute_global_forwarding_rule.default: + load_balancing_scheme: EXTERNAL + port_range: '443' + module.glb-0.google_compute_region_network_endpoint_group.serverless["neg-0"]: + cloud_run: + - service: hello + tag: null + url_mask: null + +counts: + google_compute_backend_service: 1 + google_compute_global_forwarding_rule: 1 + google_compute_managed_ssl_certificate: 1 + google_compute_region_network_endpoint_group: 1 + google_compute_target_https_proxy: 1 + google_compute_url_map: 1 + diff --git a/tests/modules/net_glb/test-plan.tfvars b/tests/modules/net_glb/test-plan.tfvars index f10667f17..94cc5ab2a 100644 --- a/tests/modules/net_glb/test-plan.tfvars +++ b/tests/modules/net_glb/test-plan.tfvars @@ -62,30 +62,36 @@ neg_configs = { network = "projects/my-project/global/networks/shared-vpc" subnetwork = "projects/my-project/regions/europe-west8/subnetworks/gce" zone = "europe-west8-b" - endpoints = [{ - instance = "nginx-ew8-b" - ip_address = "10.24.32.25" - port = 80 - }] + endpoints = { + e-0 = { + instance = "nginx-ew8-b" + ip_address = "10.24.32.25" + port = 80 + } + } } } neg-hybrid = { hybrid = { network = "projects/my-project/global/networks/shared-vpc" zone = "europe-west8-b" - endpoints = [{ - ip_address = "192.168.0.3" - port = 80 - }] + endpoints = { + e-0 = { + ip_address = "192.168.0.3" + port = 80 + } + } } } neg-internet = { internet = { use_fqdn = true - endpoints = [{ - destination = "hello.example.org" - port = 80 - }] + endpoints = { + e-0 = { + destination = "hello.example.org" + port = 80 + } + } } } } diff --git a/tests/modules/net_ilb_l7/fixture/test.negs.tfvars b/tests/modules/net_ilb_l7/fixture/test.negs.tfvars index 2f7f48d57..f6141a7ef 100644 --- a/tests/modules/net_ilb_l7/fixture/test.negs.tfvars +++ b/tests/modules/net_ilb_l7/fixture/test.negs.tfvars @@ -9,11 +9,13 @@ neg_configs = { custom = { gce = { zone = "europe-west1-b" - endpoints = [{ - ip_address = "10.0.0.10" - instance = "test-1" - port = 80 - }] + endpoints = { + e-0 = { + ip_address = "10.0.0.10" + instance = "test-1" + port = 80 + } + } } } } diff --git a/tests/modules/net_vpc/data/factory-subnet.yaml b/tests/modules/net_vpc/data/factory-subnet.yaml deleted file mode 100644 index d0f4bd8f1..000000000 --- a/tests/modules/net_vpc/data/factory-subnet.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -region: europe-west1 -description: Sample description -ip_cidr_range: 10.128.0.0/24 -enable_private_access: false -iam_users: ["foobar@example.com"] -iam_groups: ["lorem@example.com"] -iam_service_accounts: ["foobar@project-id.iam.gserviceaccount.com"] -secondary_ip_ranges: - secondary-range-a: 192.168.128.0/24 diff --git a/tests/modules/net_vpc/examples/dns-policies.yaml b/tests/modules/net_vpc/examples/dns-policies.yaml new file mode 100644 index 000000000..a30d6408a --- /dev/null +++ b/tests/modules/net_vpc/examples/dns-policies.yaml @@ -0,0 +1,42 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.vpc.google_compute_network.network[0]: + name: my-network + project: my-project + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/production"]: {} + module.vpc.google_dns_policy.default[0]: + alternative_name_server_config: + - target_name_servers: + - forwarding_path: '' + ipv4_address: '8.8.8.8' + - forwarding_path: private + ipv4_address: '10.0.0.1' + description: Managed by Terraform + enable_inbound_forwarding: true + enable_logging: null + name: my-network + networks: + - {} + project: my-project + +counts: + google_compute_network: 1 + google_compute_subnetwork: 1 + google_dns_policy: 1 + modules: 1 + resources: 3 + +outputs: {} diff --git a/tests/modules/net_vpc/examples/factory.yaml b/tests/modules/net_vpc/examples/factory.yaml new file mode 100644 index 000000000..48671c292 --- /dev/null +++ b/tests/modules/net_vpc/examples/factory.yaml @@ -0,0 +1,50 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.vpc.google_compute_network.network[0]: + name: my-network + project: my-project + routing_mode: GLOBAL + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/subnet-detailed"]: + description: Sample description + ip_cidr_range: 10.0.0.0/24 + log_config: + - aggregation_interval: INTERVAL_5_SEC + filter_expr: 'true' + flow_sampling: 0.5 + metadata: INCLUDE_ALL_METADATA + metadata_fields: null + name: subnet-detailed + private_ip_google_access: false + project: my-project + region: europe-west1 + role: null + secondary_ip_range: + - ip_cidr_range: 192.168.0.0/24 + range_name: secondary-range-a + module.vpc.google_compute_subnetwork.subnetwork["europe-west4/subnet-simple"]: + description: Terraform-managed. + ip_cidr_range: 10.0.1.0/24 + log_config: [] + name: subnet-simple + private_ip_google_access: true + project: my-project + region: europe-west4 + role: null + secondary_ip_range: [] + +counts: + google_compute_network: 1 + google_compute_subnetwork: 2 diff --git a/tests/modules/net_vpc/peering.yaml b/tests/modules/net_vpc/examples/peering.yaml similarity index 50% rename from tests/modules/net_vpc/peering.yaml rename to tests/modules/net_vpc/examples/peering.yaml index 8d0bbed71..937ce1445 100644 --- a/tests/modules/net_vpc/peering.yaml +++ b/tests/modules/net_vpc/examples/peering.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,35 +13,22 @@ # limitations under the License. values: - google_compute_network.network[0]: - auto_create_subnetworks: false - delete_default_routes_on_create: false - description: Terraform-managed. - name: test - project: test-project - routing_mode: GLOBAL - google_compute_network_peering.local[0]: - export_custom_routes: true - import_custom_routes: false - name: test-peer - peer_network: projects/my-project/global/networks/peer - google_compute_network_peering.remote[0]: + module.vpc-hub.google_compute_network.network[0]: {} + module.vpc-spoke-1.google_compute_network.network[0]: {} + module.vpc-hub.google_compute_subnetwork.subnetwork["europe-west1/subnet-1"]: {} + module.vpc-spoke-1.google_compute_subnetwork.subnetwork["europe-west1/subnet-2"]: {} + module.vpc-spoke-1.google_compute_network_peering.local[0]: export_custom_routes: false + export_subnet_routes_with_public_ip: true import_custom_routes: true - name: peer-test - network: projects/my-project/global/networks/peer + import_subnet_routes_with_public_ip: null + module.vpc-spoke-1.google_compute_network_peering.remote[0]: + export_custom_routes: true + export_subnet_routes_with_public_ip: true + import_custom_routes: false + import_subnet_routes_with_public_ip: null counts: - google_compute_network: 1 + google_compute_network: 2 google_compute_network_peering: 2 - -outputs: - bindings: {} - project_id: test-project - subnet_ips: {} - subnet_regions: {} - subnet_secondary_ranges: {} - subnet_self_links: {} - subnets: {} - subnets_proxy_only: {} - subnets_psc: {} + google_compute_subnetwork: 2 diff --git a/tests/modules/net_vpc/examples/proxy-only-subnets.yaml b/tests/modules/net_vpc/examples/proxy-only-subnets.yaml new file mode 100644 index 000000000..6e2069aaa --- /dev/null +++ b/tests/modules/net_vpc/examples/proxy-only-subnets.yaml @@ -0,0 +1,40 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.vpc.google_compute_network.network[0]: + name: my-network + project: my-project + module.vpc.google_compute_subnetwork.proxy_only["europe-west1/regional-proxy"]: + description: Terraform-managed proxy-only subnet for Regional HTTPS or Internal HTTPS LB. + ip_cidr_range: 10.0.1.0/24 + log_config: [] + name: regional-proxy + project: my-project + purpose: REGIONAL_MANAGED_PROXY + region: europe-west1 + role: ACTIVE + module.vpc.google_compute_subnetwork.psc["europe-west1/psc"]: + description: Terraform-managed subnet for Private Service Connect (PSC NAT). + ip_cidr_range: 10.0.3.0/24 + log_config: [] + name: psc + project: my-project + purpose: PRIVATE_SERVICE_CONNECT + region: europe-west1 + role: null + +counts: + google_compute_network: 1 + google_compute_subnetwork: 2 diff --git a/tests/modules/net_vpc/examples/psc-routes.yaml b/tests/modules/net_vpc/examples/psc-routes.yaml new file mode 100644 index 000000000..6f459f4b7 --- /dev/null +++ b/tests/modules/net_vpc/examples/psc-routes.yaml @@ -0,0 +1,47 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.vpc.google_compute_global_address.psa_ranges["myrange"]: + address: 10.0.1.0 + address_type: INTERNAL + description: null + ip_version: null + name: myrange + prefix_length: 24 + project: my-project + purpose: VPC_PEERING + module.vpc.google_compute_network.network[0]: + name: my-network + project: my-project + routing_mode: GLOBAL + module.vpc.google_compute_network_peering_routes_config.psa_routes["1"]: + export_custom_routes: true + import_custom_routes: true + project: my-project + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/production"]: + ip_cidr_range: 10.0.0.0/24 + name: production + project: my-project + module.vpc.google_service_networking_connection.psa_connection["1"]: + reserved_peering_ranges: + - myrange + service: servicenetworking.googleapis.com + +counts: + google_compute_global_address: 1 + google_compute_network: 1 + google_compute_network_peering_routes_config: 1 + google_compute_subnetwork: 1 + google_service_networking_connection: 1 diff --git a/tests/modules/net_vpc/examples/psc.yaml b/tests/modules/net_vpc/examples/psc.yaml new file mode 100644 index 000000000..c08fcb453 --- /dev/null +++ b/tests/modules/net_vpc/examples/psc.yaml @@ -0,0 +1,46 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.vpc.google_compute_global_address.psa_ranges["myrange"]: + address: 10.0.1.0 + address_type: INTERNAL + name: myrange + prefix_length: 24 + project: my-project + purpose: VPC_PEERING + module.vpc.google_compute_network.network[0]: + name: my-network + project: my-project + module.vpc.google_compute_network_peering_routes_config.psa_routes["1"]: + export_custom_routes: false + import_custom_routes: false + project: my-project + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/production"]: + ip_cidr_range: 10.0.0.0/24 + name: production + project: my-project + module.vpc.google_service_networking_connection.psa_connection["1"]: + reserved_peering_ranges: + - myrange + service: servicenetworking.googleapis.com + +counts: + google_compute_global_address: 1 + google_compute_network: 1 + google_compute_network_peering_routes_config: 1 + google_compute_subnetwork: 1 + google_service_networking_connection: 1 + +outputs: {} diff --git a/tests/modules/net_vpc/examples/routes.yaml b/tests/modules/net_vpc/examples/routes.yaml new file mode 100644 index 000000000..205197c82 --- /dev/null +++ b/tests/modules/net_vpc/examples/routes.yaml @@ -0,0 +1,146 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.vpc["gateway"].google_compute_network.network[0]: + name: my-network-with-route-gateway + project: my-project + routing_mode: GLOBAL + module.vpc["gateway"].google_compute_route.gateway["gateway"]: + dest_range: 0.0.0.0/0 + name: my-network-with-route-gateway-gateway + next_hop_gateway: global/gateways/default-internet-gateway + next_hop_ilb: null + next_hop_instance: null + next_hop_vpn_tunnel: null + priority: 100 + project: my-project + tags: + - tag-a + module.vpc["gateway"].google_compute_route.gateway["next-hop"]: + dest_range: 192.168.128.0/24 + name: my-network-with-route-gateway-next-hop + next_hop_gateway: global/gateways/default-internet-gateway + next_hop_ilb: null + next_hop_instance: null + next_hop_vpn_tunnel: null + priority: 1000 + project: my-project + tags: null + module.vpc["ilb"].google_compute_network.network[0]: + name: my-network-with-route-ilb + project: my-project + routing_mode: GLOBAL + module.vpc["ilb"].google_compute_route.gateway["gateway"]: + dest_range: 0.0.0.0/0 + name: my-network-with-route-ilb-gateway + next_hop_gateway: global/gateways/default-internet-gateway + next_hop_ilb: null + next_hop_instance: null + next_hop_vpn_tunnel: null + priority: 100 + project: my-project + tags: + - tag-a + module.vpc["ilb"].google_compute_route.ilb["next-hop"]: + dest_range: 192.168.128.0/24 + name: my-network-with-route-ilb-next-hop + next_hop_gateway: null + next_hop_ilb: regions/europe-west1/forwardingRules/test + next_hop_instance: null + next_hop_vpn_tunnel: null + priority: 1000 + project: my-project + tags: null + module.vpc["instance"].google_compute_network.network[0]: + name: my-network-with-route-instance + project: my-project + routing_mode: GLOBAL + module.vpc["instance"].google_compute_route.gateway["gateway"]: + dest_range: 0.0.0.0/0 + name: my-network-with-route-instance-gateway + next_hop_gateway: global/gateways/default-internet-gateway + next_hop_ilb: null + next_hop_instance: null + next_hop_vpn_tunnel: null + priority: 100 + project: my-project + tags: + - tag-a + module.vpc["instance"].google_compute_route.instance["next-hop"]: + dest_range: 192.168.128.0/24 + name: my-network-with-route-instance-next-hop + next_hop_gateway: null + next_hop_ilb: null + next_hop_instance: zones/europe-west1-b/test + next_hop_instance_zone: europe-west1-b + next_hop_vpn_tunnel: null + priority: 1000 + project: my-project + tags: null + module.vpc["ip"].google_compute_network.network[0]: + name: my-network-with-route-ip + project: my-project + routing_mode: GLOBAL + module.vpc["ip"].google_compute_route.gateway["gateway"]: + dest_range: 0.0.0.0/0 + name: my-network-with-route-ip-gateway + next_hop_gateway: global/gateways/default-internet-gateway + next_hop_ilb: null + next_hop_instance: null + next_hop_vpn_tunnel: null + priority: 100 + project: my-project + tags: + - tag-a + module.vpc["ip"].google_compute_route.ip["next-hop"]: + dest_range: 192.168.128.0/24 + name: my-network-with-route-ip-next-hop + next_hop_gateway: null + next_hop_ilb: null + next_hop_instance: null + next_hop_ip: 192.168.0.128 + next_hop_vpn_tunnel: null + priority: 1000 + project: my-project + tags: null + module.vpc["vpn_tunnel"].google_compute_network.network[0]: + name: my-network-with-route-vpn-tunnel + project: my-project + routing_mode: GLOBAL + module.vpc["vpn_tunnel"].google_compute_route.gateway["gateway"]: + dest_range: 0.0.0.0/0 + name: my-network-with-route-vpn-tunnel-gateway + next_hop_gateway: global/gateways/default-internet-gateway + next_hop_ilb: null + next_hop_instance: null + next_hop_vpn_tunnel: null + priority: 100 + project: my-project + tags: + - tag-a + module.vpc["vpn_tunnel"].google_compute_route.vpn_tunnel["next-hop"]: + dest_range: 192.168.128.0/24 + name: my-network-with-route-vpn-tunnel-next-hop + next_hop_gateway: null + next_hop_ilb: null + next_hop_instance: null + next_hop_vpn_tunnel: regions/europe-west1/vpnTunnels/foo + priority: 1000 + project: my-project + tags: null + +counts: + google_compute_network: 5 + google_compute_route: 10 diff --git a/tests/modules/net_vpc/examples/shared-vpc.yaml b/tests/modules/net_vpc/examples/shared-vpc.yaml new file mode 100644 index 000000000..b004e3151 --- /dev/null +++ b/tests/modules/net_vpc/examples/shared-vpc.yaml @@ -0,0 +1,51 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.vpc-host.google_compute_network.network[0]: + name: my-host-network + project: my-project + module.vpc-host.google_compute_shared_vpc_host_project.shared_vpc_host[0]: + project: my-project + module.vpc-host.google_compute_shared_vpc_service_project.service_projects["project1"]: + host_project: my-project + service_project: project1 + module.vpc-host.google_compute_shared_vpc_service_project.service_projects["project2"]: + host_project: my-project + service_project: project2 + module.vpc-host.google_compute_subnetwork.subnetwork["europe-west1/subnet-1"]: {} + module.vpc-host.google_compute_subnetwork_iam_binding.binding["europe-west1/subnet-1.roles/compute.networkUser"]: + condition: [] + members: + - serviceAccount:cloudsvc + - serviceAccount:gke + project: my-project + region: europe-west1 + role: roles/compute.networkUser + subnetwork: subnet-1 + module.vpc-host.google_compute_subnetwork_iam_binding.binding["europe-west1/subnet-1.roles/compute.securityAdmin"]: + condition: [] + members: + - serviceAccount:gke + project: my-project + region: europe-west1 + role: roles/compute.securityAdmin + subnetwork: subnet-1 + +counts: + google_compute_network: 1 + google_compute_shared_vpc_host_project: 1 + google_compute_shared_vpc_service_project: 2 + google_compute_subnetwork: 1 + google_compute_subnetwork_iam_binding: 2 diff --git a/tests/modules/net_vpc/examples/simple.yaml b/tests/modules/net_vpc/examples/simple.yaml new file mode 100644 index 000000000..799852c02 --- /dev/null +++ b/tests/modules/net_vpc/examples/simple.yaml @@ -0,0 +1,50 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.vpc.google_compute_network.network[0]: + auto_create_subnetworks: false + delete_default_routes_on_create: false + description: Terraform-managed. + name: my-network + project: my-project + routing_mode: GLOBAL + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/production"]: + description: Terraform-managed. + ip_cidr_range: 10.0.0.0/24 + log_config: [] + name: production + private_ip_google_access: true + project: my-project + region: europe-west1 + role: null + secondary_ip_range: + - ip_cidr_range: 172.16.0.0/20 + range_name: pods + - ip_cidr_range: 192.168.0.0/24 + range_name: services + module.vpc.google_compute_subnetwork.subnetwork["europe-west2/production"]: + description: Terraform-managed. + ip_cidr_range: 10.0.16.0/24 + log_config: [] + name: production + private_ip_google_access: true + project: my-project + region: europe-west2 + role: null + secondary_ip_range: [] + +counts: + google_compute_network: 1 + google_compute_subnetwork: 2 diff --git a/tests/modules/net_vpc/examples/subnet-iam.yaml b/tests/modules/net_vpc/examples/subnet-iam.yaml new file mode 100644 index 000000000..cb53ecd80 --- /dev/null +++ b/tests/modules/net_vpc/examples/subnet-iam.yaml @@ -0,0 +1,54 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.vpc.google_compute_network.network[0]: + name: my-network + project: my-project + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/subnet-1"]: + name: subnet-1 + project: my-project + region: europe-west1 + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/subnet-2"]: + name: subnet-2 + private_ip_google_access: true + project: my-project + region: europe-west1 + module.vpc.google_compute_subnetwork_iam_binding.binding["europe-west1/subnet-1.roles/compute.networkUser"]: + condition: [] + members: + - group:group1@example.com + - user:user1@example.com + project: my-project + region: europe-west1 + role: roles/compute.networkUser + subnetwork: subnet-1 + module.vpc.google_compute_subnetwork_iam_binding.binding["europe-west1/subnet-2.roles/compute.networkUser"]: + condition: [] + members: + - group:group2@example.com + - user:user2@example.com + project: my-project + region: europe-west1 + role: roles/compute.networkUser + subnetwork: subnet-2 + +counts: + google_compute_network: 1 + google_compute_subnetwork: 2 + google_compute_subnetwork_iam_binding: 2 + modules: 1 + resources: 5 + +outputs: {} diff --git a/tests/modules/net_vpc/examples/subnet-options.yaml b/tests/modules/net_vpc/examples/subnet-options.yaml new file mode 100644 index 000000000..e3cea5ca6 --- /dev/null +++ b/tests/modules/net_vpc/examples/subnet-options.yaml @@ -0,0 +1,70 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.vpc.google_compute_network.network[0]: + name: my-network + project: my-project + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/no-pga"]: + description: Subnet b + ip_cidr_range: 10.0.1.0/24 + log_config: [] + name: no-pga + private_ip_google_access: false + project: my-project + region: europe-west1 + secondary_ip_range: [] + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/simple"]: + description: Terraform-managed. + ip_cidr_range: 10.0.0.0/24 + log_config: [] + name: simple + private_ip_google_access: true + project: my-project + region: europe-west1 + secondary_ip_range: [] + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/with-flow-logs"]: + description: Terraform-managed. + ip_cidr_range: 10.0.3.0/24 + ipv6_access_type: null + log_config: + - aggregation_interval: INTERVAL_10_MIN + filter_expr: 'true' + flow_sampling: 0.5 + metadata: INCLUDE_ALL_METADATA + metadata_fields: null + name: with-flow-logs + private_ip_google_access: true + project: my-project + region: europe-west1 + role: null + secondary_ip_range: [] + module.vpc.google_compute_subnetwork.subnetwork["europe-west1/with-secondary-ranges"]: + description: Terraform-managed. + ip_cidr_range: 10.0.2.0/24 + log_config: [] + name: with-secondary-ranges + private_ip_google_access: true + project: my-project + region: europe-west1 + role: null + secondary_ip_range: + - ip_cidr_range: 192.168.0.0/24 + range_name: a + - ip_cidr_range: 192.168.1.0/24 + range_name: b + +counts: + google_compute_network: 1 + google_compute_subnetwork: 4 diff --git a/tests/modules/net_vpc/factory.tfvars b/tests/modules/net_vpc/factory.tfvars deleted file mode 100644 index 8c4d4a28c..000000000 --- a/tests/modules/net_vpc/factory.tfvars +++ /dev/null @@ -1 +0,0 @@ -data_folder = "../../tests/modules/net_vpc/data" diff --git a/tests/modules/net_vpc/factory.yaml b/tests/modules/net_vpc/factory.yaml deleted file mode 100644 index 9cf628d09..000000000 --- a/tests/modules/net_vpc/factory.yaml +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -values: - google_compute_subnetwork.subnetwork["europe-west1/factory-subnet"]: - description: 'Sample description' - ip_cidr_range: '10.128.0.0/24' - ipv6_access_type: null - log_config: [] - name: 'factory-subnet' - private_ip_google_access: false - project: 'test-project' - region: 'europe-west1' - role: null - secondary_ip_range: - - ip_cidr_range: '192.168.128.0/24' - range_name: 'secondary-range-a' - google_compute_subnetwork.subnetwork["europe-west4/factory-subnet2"]: - description: 'Sample description' - ip_cidr_range: '10.129.0.0/24' - log_config: [] - name: 'factory-subnet2' - private_ip_google_access: true - project: 'test-project' - region: 'europe-west4' - role: null - secondary_ip_range: [] - - # FIXME: should we have some bindings here? - -counts: - google_compute_network: 1 - google_compute_subnetwork: 2 diff --git a/tests/modules/net_vpc/fixture/main.tf b/tests/modules/net_vpc/fixture/main.tf deleted file mode 100644 index f0e4696e0..000000000 --- a/tests/modules/net_vpc/fixture/main.tf +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module "test" { - source = "../../../../modules/net-vpc" - project_id = "test-project" - name = "test" - peering_config = var.peering_config - routes = var.routes - shared_vpc_host = var.shared_vpc_host - shared_vpc_service_projects = var.shared_vpc_service_projects - subnet_iam = var.subnet_iam - subnets = var.subnets - auto_create_subnetworks = var.auto_create_subnetworks - psa_config = var.psa_config - data_folder = var.data_folder -} diff --git a/tests/modules/net_vpc/fixture/test.subnets.tfvars b/tests/modules/net_vpc/fixture/test.subnets.tfvars deleted file mode 100644 index 499e498f4..000000000 --- a/tests/modules/net_vpc/fixture/test.subnets.tfvars +++ /dev/null @@ -1,44 +0,0 @@ -subnet_iam = { - "europe-west1/a" = { - "roles/compute.networkUser" = [ - "user:a@example.com", "group:g-a@example.com" - ] - } - "europe-west1/c" = { - "roles/compute.networkUser" = [ - "user:c@example.com", "group:g-c@example.com" - ] - } -} -subnets = [ - { - name = "a" - region = "europe-west1" - ip_cidr_range = "10.0.0.0/24" - }, - { - name = "b" - region = "europe-west1" - ip_cidr_range = "10.0.1.0/24", - description = "Subnet b" - enable_private_access = false - }, - { - name = "c" - region = "europe-west1" - ip_cidr_range = "10.0.2.0/24" - secondary_ip_ranges = { - a = "192.168.0.0/24" - b = "192.168.1.0/24" - } - }, - { - name = "d" - region = "europe-west1" - ip_cidr_range = "10.0.3.0/24" - flow_logs_config = { - flow_sampling = 0.5 - aggregation_interval = "INTERVAL_10_MIN" - } - } -] diff --git a/tests/modules/net_vpc/fixture/variables.tf b/tests/modules/net_vpc/fixture/variables.tf deleted file mode 100644 index 868966c8b..000000000 --- a/tests/modules/net_vpc/fixture/variables.tf +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -variable "auto_create_subnetworks" { - type = bool - default = false -} - -variable "data_folder" { - type = string - default = null -} - -variable "delete_default_routes_on_create" { - type = bool - default = false -} - -variable "description" { - type = string - default = "Terraform-managed." -} - -variable "dns_policy" { - type = any - default = null -} - -variable "mtu" { - type = number - default = null -} - -variable "peering_config" { - type = any - default = null -} - -variable "psa_config" { - type = any - default = null -} - -variable "routes" { - type = any - default = {} - nullable = false -} - -variable "routing_mode" { - type = string - default = "GLOBAL" -} - -variable "shared_vpc_host" { - type = bool - default = false -} - -variable "shared_vpc_service_projects" { - type = list(string) - default = [] -} - -variable "subnets" { - type = any - default = [] -} - -variable "subnet_iam" { - type = map(map(list(string))) - default = {} -} - -variable "subnets_proxy_only" { - type = any - default = [] -} - -variable "subnets_psc" { - type = any - default = [] -} - -variable "vpc_create" { - type = bool - default = true -} diff --git a/tests/modules/net_vpc/peering.tfvars b/tests/modules/net_vpc/peering.tfvars deleted file mode 100644 index eccd7ae71..000000000 --- a/tests/modules/net_vpc/peering.tfvars +++ /dev/null @@ -1,5 +0,0 @@ -peering_config = { - peer_vpc_self_link = "projects/my-project/global/networks/peer" - export_routes = true - import_routes = null -} diff --git a/tests/modules/net_vpc/psa_simple.tfvars b/tests/modules/net_vpc/psa_simple.tfvars deleted file mode 100644 index 51289fe04..000000000 --- a/tests/modules/net_vpc/psa_simple.tfvars +++ /dev/null @@ -1,7 +0,0 @@ -psa_config = { - ranges = { - bar = "172.16.100.0/24" - foo = "172.16.101.0/24" - } - routes = null -} diff --git a/tests/modules/net_vpc/psa_simple.yaml b/tests/modules/net_vpc/psa_simple.yaml deleted file mode 100644 index 019b443fa..000000000 --- a/tests/modules/net_vpc/psa_simple.yaml +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -values: - google_compute_global_address.psa_ranges["bar"]: - address: 172.16.100.0 - address_type: INTERNAL - description: null - ip_version: null - name: bar - prefix_length: 24 - project: test-project - purpose: VPC_PEERING - google_compute_global_address.psa_ranges["foo"]: - address: 172.16.101.0 - address_type: INTERNAL - description: null - ip_version: null - name: foo - prefix_length: 24 - project: test-project - purpose: VPC_PEERING - google_compute_network.network[0]: - auto_create_subnetworks: false - delete_default_routes_on_create: false - description: Terraform-managed. - enable_ula_internal_ipv6: null - name: test - project: test-project - routing_mode: GLOBAL - google_compute_network_peering_routes_config.psa_routes["1"]: - export_custom_routes: false - import_custom_routes: false - project: test-project - google_service_networking_connection.psa_connection["1"]: - reserved_peering_ranges: - - bar - - foo - service: servicenetworking.googleapis.com - -counts: - google_compute_global_address: 2 - google_compute_network: 1 - google_compute_network_peering_routes_config: 1 - google_service_networking_connection: 1 - -outputs: - bindings: {} - name: __missing__ - network: __missing__ - project_id: test-project - self_link: __missing__ - subnet_ips: {} - subnet_regions: {} - subnet_secondary_ranges: {} - subnet_self_links: {} - subnets: {} - subnets_proxy_only: {} - subnets_psc: {} diff --git a/tests/modules/net_vpc/simple.tfvars b/tests/modules/net_vpc/simple.tfvars deleted file mode 100644 index 6f848aa99..000000000 --- a/tests/modules/net_vpc/simple.tfvars +++ /dev/null @@ -1 +0,0 @@ -# skip boilerplate check diff --git a/tests/modules/net_vpc/subnets.tfvars b/tests/modules/net_vpc/subnets.tfvars deleted file mode 100644 index 499e498f4..000000000 --- a/tests/modules/net_vpc/subnets.tfvars +++ /dev/null @@ -1,44 +0,0 @@ -subnet_iam = { - "europe-west1/a" = { - "roles/compute.networkUser" = [ - "user:a@example.com", "group:g-a@example.com" - ] - } - "europe-west1/c" = { - "roles/compute.networkUser" = [ - "user:c@example.com", "group:g-c@example.com" - ] - } -} -subnets = [ - { - name = "a" - region = "europe-west1" - ip_cidr_range = "10.0.0.0/24" - }, - { - name = "b" - region = "europe-west1" - ip_cidr_range = "10.0.1.0/24", - description = "Subnet b" - enable_private_access = false - }, - { - name = "c" - region = "europe-west1" - ip_cidr_range = "10.0.2.0/24" - secondary_ip_ranges = { - a = "192.168.0.0/24" - b = "192.168.1.0/24" - } - }, - { - name = "d" - region = "europe-west1" - ip_cidr_range = "10.0.3.0/24" - flow_logs_config = { - flow_sampling = 0.5 - aggregation_interval = "INTERVAL_10_MIN" - } - } -] diff --git a/tests/modules/net_vpc/subnets.yaml b/tests/modules/net_vpc/subnets.yaml deleted file mode 100644 index 9ccf31e60..000000000 --- a/tests/modules/net_vpc/subnets.yaml +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -values: - google_compute_network.network[0]: - auto_create_subnetworks: false - delete_default_routes_on_create: false - description: Terraform-managed. - name: test - project: test-project - routing_mode: GLOBAL - google_compute_subnetwork.subnetwork["europe-west1/a"]: - description: Terraform-managed. - ip_cidr_range: 10.0.0.0/24 - log_config: [] - name: a - private_ip_google_access: true - project: test-project - region: europe-west1 - role: null - secondary_ip_range: [] - google_compute_subnetwork.subnetwork["europe-west1/b"]: - description: Subnet b - ip_cidr_range: 10.0.1.0/24 - log_config: [] - name: b - private_ip_google_access: false - project: test-project - region: europe-west1 - role: null - secondary_ip_range: [] - google_compute_subnetwork.subnetwork["europe-west1/c"]: - description: Terraform-managed. - ip_cidr_range: 10.0.2.0/24 - ipv6_access_type: null - log_config: [] - name: c - private_ip_google_access: true - project: test-project - region: europe-west1 - role: null - secondary_ip_range: - - ip_cidr_range: 192.168.0.0/24 - range_name: a - - ip_cidr_range: 192.168.1.0/24 - range_name: b - google_compute_subnetwork.subnetwork["europe-west1/d"]: - description: Terraform-managed. - ip_cidr_range: 10.0.3.0/24 - log_config: - - aggregation_interval: INTERVAL_10_MIN - filter_expr: 'true' - flow_sampling: 0.5 - metadata: INCLUDE_ALL_METADATA - metadata_fields: null - name: d - private_ip_google_access: true - project: test-project - region: europe-west1 - role: null - secondary_ip_range: [] - google_compute_subnetwork_iam_binding.binding["europe-west1/a.roles/compute.networkUser"]: - condition: [] - members: - - group:g-a@example.com - - user:a@example.com - project: test-project - region: europe-west1 - role: roles/compute.networkUser - subnetwork: a - google_compute_subnetwork_iam_binding.binding["europe-west1/c.roles/compute.networkUser"]: - condition: [] - members: - - group:g-c@example.com - - user:c@example.com - project: test-project - region: europe-west1 - role: roles/compute.networkUser - subnetwork: c - -counts: - google_compute_network: 1 - google_compute_subnetwork: 4 - google_compute_subnetwork_iam_binding: 2 - -outputs: - bindings: __missing__ - project_id: test-project - subnet_ips: - europe-west1/a: 10.0.0.0/24 - europe-west1/b: 10.0.1.0/24 - europe-west1/c: 10.0.2.0/24 - europe-west1/d: 10.0.3.0/24 - subnet_regions: - europe-west1/a: europe-west1 - europe-west1/b: europe-west1 - europe-west1/c: europe-west1 - europe-west1/d: europe-west1 - subnet_secondary_ranges: - europe-west1/a: {} - europe-west1/b: {} - europe-west1/c: - a: 192.168.0.0/24 - b: 192.168.1.0/24 - europe-west1/d: {} - subnet_self_links: __missing__ - subnets: __missing__ - subnets_proxy_only: {} - subnets_psc: {} diff --git a/tests/modules/net_vpc/test_routes.py b/tests/modules/net_vpc/test_routes.py deleted file mode 100644 index 01d9673dd..000000000 --- a/tests/modules/net_vpc/test_routes.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - -_route_parameters = [('gateway', 'global/gateways/default-internet-gateway'), - ('instance', 'zones/europe-west1-b/test'), - ('ip', '192.168.0.128'), - ('ilb', 'regions/europe-west1/forwardingRules/test'), - ('vpn_tunnel', 'regions/europe-west1/vpnTunnels/foo')] - - -@pytest.mark.parametrize('next_hop_type,next_hop', _route_parameters) -def test_vpc_routes(plan_summary, next_hop_type, next_hop): - 'Test vpc routes.' - - var_routes = '''{ - next-hop = { - dest_range = "192.168.128.0/24" - tags = null - next_hop_type = "%s" - next_hop = "%s" - } - gateway = { - dest_range = "0.0.0.0/0", - priority = 100 - tags = ["tag-a"] - next_hop_type = "gateway", - next_hop = "global/gateways/default-internet-gateway" - } - }''' % (next_hop_type, next_hop) - summary = plan_summary('modules/net-vpc', tf_var_files=['common.tfvars'], - routes=var_routes) - assert len(summary.values) == 3 - route = summary.values[f'google_compute_route.{next_hop_type}["next-hop"]'] - assert route[f'next_hop_{next_hop_type}'] == next_hop diff --git a/tests/modules/net_vpc/tftest.yaml b/tests/modules/net_vpc/tftest.yaml index b2b09798b..5e9668ea4 100644 --- a/tests/modules/net_vpc/tftest.yaml +++ b/tests/modules/net_vpc/tftest.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,12 +17,7 @@ common_tfvars: - common.tfvars tests: - simple: - subnets: - peering: shared_vpc: - factory: - psa_simple: psa_routes_export: psa_routes_import: psa_routes_import_export: diff --git a/tests/modules/net_vpc_firewall/auto-rules.tfvars b/tests/modules/net_vpc_firewall/auto-rules.tfvars new file mode 100644 index 000000000..6b991da79 --- /dev/null +++ b/tests/modules/net_vpc_firewall/auto-rules.tfvars @@ -0,0 +1,4 @@ +default_rules_config = { + admin_ranges = ["10.0.0.0/8"] + https_ranges = [] +} diff --git a/tests/modules/net_vpc_firewall/auto-rules.yaml b/tests/modules/net_vpc_firewall/auto-rules.yaml new file mode 100644 index 000000000..ed3c84f23 --- /dev/null +++ b/tests/modules/net_vpc_firewall/auto-rules.yaml @@ -0,0 +1,44 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + google_compute_firewall.allow-admins[0]: + source_ranges: + - 10.0.0.0/8 + google_compute_firewall.allow-tag-http[0]: + allow: + - ports: + - "80" + protocol: tcp + source_ranges: + - 130.211.0.0/22 + - 209.85.152.0/22 + - 209.85.204.0/22 + - 35.191.0.0/16 + google_compute_firewall.allow-tag-ssh[0]: + allow: + - ports: + - "22" + protocol: tcp + source_ranges: + - 35.235.240.0/20 + +counts: + google_compute_firewall: 3 + modules: 0 + resources: 3 + +outputs: + default_rules: __missing__ + rules: {} diff --git a/tests/modules/net_vpc_firewall/common.tfvars b/tests/modules/net_vpc_firewall/common.tfvars new file mode 100644 index 000000000..fda6ab8f4 --- /dev/null +++ b/tests/modules/net_vpc_firewall/common.tfvars @@ -0,0 +1,2 @@ +project_id = "test-project" +network = "test-network" diff --git a/tests/modules/net_vpc_firewall/custom-rules.tfvars b/tests/modules/net_vpc_firewall/custom-rules.tfvars new file mode 100644 index 000000000..181a8248c --- /dev/null +++ b/tests/modules/net_vpc_firewall/custom-rules.tfvars @@ -0,0 +1,33 @@ +default_rules_config = { + disabled = true +} +egress_rules = { + allow-egress-rfc1918 = { + deny = false + description = "Allow egress to RFC 1918 ranges." + destination_ranges = [ + "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" + ] + } + allow-egress-tag = { + deny = false + description = "Allow egress from a specific tag to 0/0." + targets = ["target-tag"] + } + deny-egress-all = { + description = "Block egress." + } +} +ingress_rules = { + allow-ingress-ntp = { + description = "Allow NTP service based on tag." + targets = ["ntp-svc"] + rules = [{ protocol = "udp", ports = [123] }] + } + allow-ingress-tag = { + description = "Allow ingress from a specific tag." + source_ranges = [] + sources = ["client-tag"] + targets = ["target-tag"] + } +} diff --git a/tests/modules/net_vpc_firewall/custom-rules.yaml b/tests/modules/net_vpc_firewall/custom-rules.yaml new file mode 100644 index 000000000..652048975 --- /dev/null +++ b/tests/modules/net_vpc_firewall/custom-rules.yaml @@ -0,0 +1,83 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + google_compute_firewall.custom-rules["allow-egress-rfc1918"]: + allow: + - ports: [] + protocol: all + deny: [] + description: Allow egress to RFC 1918 ranges. + destination_ranges: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + direction: EGRESS + google_compute_firewall.custom-rules["allow-egress-tag"]: + allow: + - ports: [] + protocol: all + deny: [] + description: Allow egress from a specific tag to 0/0. + destination_ranges: + - 0.0.0.0/0 + direction: EGRESS + target_tags: + - target-tag + google_compute_firewall.custom-rules["allow-ingress-ntp"]: + allow: + - ports: + - "123" + protocol: udp + deny: [] + description: Allow NTP service based on tag. + direction: INGRESS + source_ranges: + - 0.0.0.0/0 + source_service_accounts: null + source_tags: null + target_tags: + - ntp-svc + google_compute_firewall.custom-rules["allow-ingress-tag"]: + allow: + - ports: [] + protocol: all + deny: [] + description: Allow ingress from a specific tag. + direction: INGRESS + source_ranges: null + source_tags: + - client-tag + target_tags: + - target-tag + google_compute_firewall.custom-rules["deny-egress-all"]: + allow: [] + deny: + - ports: [] + protocol: all + description: Block egress. + direction: EGRESS + +counts: + google_compute_firewall: 5 + modules: 0 + resources: 5 + +outputs: + default_rules: + admin: [] + http: [] + https: [] + ssh: [] + rules: __missing__ diff --git a/tests/modules/net_vpc_firewall/fixture/config/cidr_template.yaml b/tests/modules/net_vpc_firewall/data/cidr_template.yaml similarity index 100% rename from tests/modules/net_vpc_firewall/fixture/config/cidr_template.yaml rename to tests/modules/net_vpc_firewall/data/cidr_template.yaml diff --git a/tests/modules/net_vpc_firewall/fixture/config/firewall/load_balancers.yaml b/tests/modules/net_vpc_firewall/data/firewall/load_balancers.yaml similarity index 100% rename from tests/modules/net_vpc_firewall/fixture/config/firewall/load_balancers.yaml rename to tests/modules/net_vpc_firewall/data/firewall/load_balancers.yaml diff --git a/tests/modules/net_vpc_firewall/factory.tfvars b/tests/modules/net_vpc_firewall/factory.tfvars new file mode 100644 index 000000000..5d2e1ab71 --- /dev/null +++ b/tests/modules/net_vpc_firewall/factory.tfvars @@ -0,0 +1,7 @@ +default_rules_config = { + disabled = true +} +factories_config = { + cidr_tpl_file = "../../tests/modules/net_vpc_firewall/data/cidr_template.yaml" + rules_folder = "../../tests/modules/net_vpc_firewall/data/firewall" +} diff --git a/tests/modules/net_vpc_firewall/factory.yaml b/tests/modules/net_vpc_firewall/factory.yaml new file mode 100644 index 000000000..26f90bd5b --- /dev/null +++ b/tests/modules/net_vpc_firewall/factory.yaml @@ -0,0 +1,54 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + google_compute_firewall.custom-rules["allow-healthchecks"]: + allow: + - ports: + - "80" + - "443" + protocol: tcp + deny: [] + description: Allow ingress from healthchecks. + direction: INGRESS + disabled: false + log_config: [] + name: allow-healthchecks + network: test-network + priority: 1000 + project: test-project + source_ranges: + - 130.211.0.0/22 + - 209.85.152.0/22 + - 209.85.204.0/22 + - 35.191.0.0/16 + source_service_accounts: null + source_tags: null + target_service_accounts: null + target_tags: + - lb-backends + timeouts: null + +counts: + google_compute_firewall: 1 + modules: 0 + resources: 1 + +outputs: + default_rules: + admin: [] + http: [] + https: [] + ssh: [] + rules: __missing__ diff --git a/tests/modules/net_vpc_firewall/fixture/main.tf b/tests/modules/net_vpc_firewall/fixture/main.tf deleted file mode 100644 index e69aeff10..000000000 --- a/tests/modules/net_vpc_firewall/fixture/main.tf +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module "firewall" { - source = "../../../../modules/net-vpc-firewall" - project_id = "test-project" - network = "test-vpc" - default_rules_config = var.default_rules_config - egress_rules = var.egress_rules - ingress_rules = var.ingress_rules - factories_config = var.factories_config -} diff --git a/tests/modules/net_vpc_firewall/fixture/test.rules.tfvars b/tests/modules/net_vpc_firewall/fixture/test.rules.tfvars deleted file mode 100644 index 36944bea4..000000000 --- a/tests/modules/net_vpc_firewall/fixture/test.rules.tfvars +++ /dev/null @@ -1,22 +0,0 @@ -egress_rules = { - allow-egress-rfc1918 = { - description = "Allow egress to RFC 1918 ranges." - is_egress = true - destination_ranges = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] - } - deny-egress-all = { - description = "Block egress." - is_deny = true - is_egress = true - } -} -ingress_rules = { - allow-ingress-ntp = { - description = "Allow NTP service based on tag." - targets = ["ntp-svc"] - rules = [{ protocol = "udp", ports = [123] }] - } -} -default_rules_config = { - disabled = true -} diff --git a/tests/modules/net_vpc_firewall/fixture/variables.tf b/tests/modules/net_vpc_firewall/fixture/variables.tf deleted file mode 100644 index fd71e93b8..000000000 --- a/tests/modules/net_vpc_firewall/fixture/variables.tf +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -variable "default_rules_config" { - type = any - default = {} -} - -variable "egress_rules" { - type = any - default = {} -} - -variable "factories_config" { - type = any - default = null -} - -variable "ingress_rules" { - type = any - default = {} -} diff --git a/tests/modules/net_vpc_firewall/test_plan.py b/tests/modules/net_vpc_firewall/test_plan.py_ similarity index 100% rename from tests/modules/net_vpc_firewall/test_plan.py rename to tests/modules/net_vpc_firewall/test_plan.py_ diff --git a/tests/blueprints/factories/bigquery_factory/fixture/tables/table_a.yaml b/tests/modules/net_vpc_firewall/tftest.yaml similarity index 83% rename from tests/blueprints/factories/bigquery_factory/fixture/tables/table_a.yaml rename to tests/modules/net_vpc_firewall/tftest.yaml index 05adbcb02..e11810c45 100644 --- a/tests/blueprints/factories/bigquery_factory/fixture/tables/table_a.yaml +++ b/tests/modules/net_vpc_firewall/tftest.yaml @@ -12,6 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -dataset: dataset_a -table: table_a -schema: [{name: "test", type: "STRING"},{name: "test2", type: "INT64"}] +module: modules/net-vpc-firewall +common_tfvars: + - common.tfvars +tests: + auto-rules: + custom-rules: + factory: diff --git a/tests/modules/organization/examples/basic.yaml b/tests/modules/organization/examples/basic.yaml new file mode 100644 index 000000000..f7b63a1d4 --- /dev/null +++ b/tests/modules/organization/examples/basic.yaml @@ -0,0 +1,146 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.org.google_org_policy_policy.default["compute.disableGuestAttributesAccess"]: + name: organizations/1234567890/policies/compute.disableGuestAttributesAccess + parent: organizations/1234567890 + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: 'TRUE' + values: [] + module.org.google_org_policy_policy.default["constraints/compute.skipDefaultNetworkCreation"]: + name: organizations/1234567890/policies/constraints/compute.skipDefaultNetworkCreation + parent: organizations/1234567890 + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: 'TRUE' + values: [] + module.org.google_org_policy_policy.default["constraints/compute.trustedImageProjects"]: + name: organizations/1234567890/policies/constraints/compute.trustedImageProjects + parent: organizations/1234567890 + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: null + values: + - allowed_values: + - projects/my-project + denied_values: null + module.org.google_org_policy_policy.default["constraints/compute.vmExternalIpAccess"]: + name: organizations/1234567890/policies/constraints/compute.vmExternalIpAccess + parent: organizations/1234567890 + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: 'TRUE' + enforce: null + values: [] + module.org.google_org_policy_policy.default["constraints/iam.allowedPolicyMemberDomains"]: + name: organizations/1234567890/policies/constraints/iam.allowedPolicyMemberDomains + parent: organizations/1234567890 + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: null + values: + - allowed_values: + - C0xxxxxxx + - C0yyyyyyy + denied_values: null + module.org.google_org_policy_policy.default["iam.disableServiceAccountKeyCreation"]: + name: organizations/1234567890/policies/iam.disableServiceAccountKeyCreation + parent: organizations/1234567890 + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: 'TRUE' + values: [] + module.org.google_org_policy_policy.default["iam.disableServiceAccountKeyUpload"]: + name: organizations/1234567890/policies/iam.disableServiceAccountKeyUpload + parent: organizations/1234567890 + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: + - description: test condition + expression: resource.matchTagId("tagKeys/1234", "tagValues/1234") + location: somewhere + title: condition + deny_all: null + enforce: 'TRUE' + values: [] + - allow_all: null + condition: [] + deny_all: null + enforce: 'FALSE' + values: [] + module.org.google_organization_iam_binding.authoritative["roles/owner"]: + condition: [] + members: + - group:cloud-owners@example.org + org_id: '1234567890' + role: roles/owner + module.org.google_organization_iam_binding.authoritative["roles/projectCreator"]: + condition: [] + members: + - group:cloud-owners@example.org + org_id: '1234567890' + role: roles/projectCreator + module.org.google_organization_iam_binding.authoritative["roles/resourcemanager.projectCreator"]: + condition: [] + members: + - group:cloud-admins@example.org + org_id: '1234567890' + role: roles/resourcemanager.projectCreator + module.org.google_organization_iam_member.additive["roles/compute.admin-user:compute@example.org"]: + condition: [] + member: user:compute@example.org + org_id: '1234567890' + role: roles/compute.admin + module.org.google_organization_iam_member.additive["roles/container.viewer-user:compute@example.org"]: + condition: [] + member: user:compute@example.org + org_id: '1234567890' + role: roles/container.viewer +counts: + google_org_policy_policy: 8 + google_organization_iam_binding: 3 diff --git a/tests/modules/organization/examples/custom-constraints.yaml b/tests/modules/organization/examples/custom-constraints.yaml new file mode 100644 index 000000000..db3023985 --- /dev/null +++ b/tests/modules/organization/examples/custom-constraints.yaml @@ -0,0 +1,39 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.org.google_org_policy_custom_constraint.constraint["custom.gkeEnableAutoUpgrade"]: + action_type: ALLOW + condition: resource.management.autoUpgrade == true + description: All node pools must have node auto-upgrade enabled. + display_name: Enable node auto-upgrade + method_types: + - CREATE + name: custom.gkeEnableAutoUpgrade + parent: organizations/1122334455 + resource_types: + - container.googleapis.com/NodePool + + module.org.google_org_policy_policy.default["custom.gkeEnableAutoUpgrade"]: + name: organizations/1122334455/policies/custom.gkeEnableAutoUpgrade + parent: organizations/1122334455 + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: 'TRUE' + values: [] diff --git a/tests/modules/organization/firewall_policies_factory.yaml b/tests/modules/organization/examples/hfw.yaml similarity index 67% rename from tests/modules/organization/firewall_policies_factory.yaml rename to tests/modules/organization/examples/hfw.yaml index 85e565fdd..91ced6db4 100644 --- a/tests/modules/organization/firewall_policies_factory.yaml +++ b/tests/modules/organization/examples/hfw.yaml @@ -13,17 +13,18 @@ # limitations under the License. values: - google_compute_firewall_policy.policy["factory-1"]: - description: null - parent: organizations/1234567890 - short_name: factory-1 - timeouts: null - google_compute_firewall_policy_rule.rule["factory-1-allow-admins"]: + module.org.google_compute_firewall_policy.policy["iap-policy"]: + parent: organizations/1122334455 + short_name: iap-policy + module.org.google_compute_firewall_policy_association.association["iap_policy"]: + attachment_target: organizations/1122334455 + name: organizations-1122334455 + module.org.google_compute_firewall_policy_rule.rule["iap-policy-allow-admins"]: action: allow description: Access from the admin subnet to all subnets direction: INGRESS disabled: null - enable_logging: null + enable_logging: false match: - dest_ip_ranges: null layer4_configs: @@ -31,18 +32,17 @@ values: ports: [] src_ip_ranges: - 10.0.0.0/8 - - 172.168.0.0/12 + - 172.16.0.0/12 - 192.168.0.0/16 priority: 1000 target_resources: null target_service_accounts: null - timeouts: null - google_compute_firewall_policy_rule.rule["factory-1-allow-ssh-from-iap"]: + module.org.google_compute_firewall_policy_rule.rule["iap-policy-allow-iap-ssh"]: action: allow - description: Enable SSH from IAP + description: Always allow ssh from IAP. direction: INGRESS disabled: null - enable_logging: null + enable_logging: false match: - dest_ip_ranges: null layer4_configs: @@ -51,11 +51,12 @@ values: - '22' src_ip_ranges: - 35.235.240.0/20 - priority: 1002 + priority: 100 target_resources: null target_service_accounts: null timeouts: null counts: google_compute_firewall_policy: 1 + google_compute_firewall_policy_association: 1 google_compute_firewall_policy_rule: 2 diff --git a/tests/modules/organization/examples/logging.yaml b/tests/modules/organization/examples/logging.yaml new file mode 100644 index 000000000..68df72bc5 --- /dev/null +++ b/tests/modules/organization/examples/logging.yaml @@ -0,0 +1,70 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.org.google_bigquery_dataset_iam_member.bq-sinks-binding["info"]: + condition: [] + role: roles/bigquery.dataEditor + module.org.google_logging_organization_exclusion.logging-exclusion["no-gce-instances"]: + disabled: null + filter: resource.type=gce_instance + name: no-gce-instances + org_id: '1122334455' + module.org.google_logging_organization_sink.sink["debug"]: + disabled: false + exclusions: + - description: null + disabled: false + filter: logName:compute + name: no-compute + filter: severity=DEBUG + include_children: true + name: debug + org_id: '1122334455' + module.org.google_logging_organization_sink.sink["info"]: + disabled: false + exclusions: [] + filter: severity=INFO + include_children: true + name: info + org_id: '1122334455' + module.org.google_logging_organization_sink.sink["notice"]: + disabled: false + exclusions: [] + filter: severity=NOTICE + include_children: true + name: notice + org_id: '1122334455' + module.org.google_logging_organization_sink.sink["warnings"]: + destination: storage.googleapis.com/gcs_sink + disabled: false + exclusions: [] + filter: severity=WARNING + include_children: true + name: warnings + org_id: '1122334455' + module.pubsub.google_pubsub_topic.default: + kms_key_name: null + labels: null + message_retention_duration: null + name: pubsub_sink + project: project-id + +counts: + google_bigquery_dataset_iam_member: 1 + google_logging_organization_exclusion: 1 + google_logging_organization_sink: 4 + google_project_iam_member: 1 + google_pubsub_topic_iam_member: 1 + google_storage_bucket_iam_member: 1 diff --git a/tests/modules/organization/examples/network-tags.yaml b/tests/modules/organization/examples/network-tags.yaml new file mode 100644 index 000000000..9cacffb61 --- /dev/null +++ b/tests/modules/organization/examples/network-tags.yaml @@ -0,0 +1,47 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.org.google_tags_tag_key.default["net-environment"]: + description: This is a network tag. + parent: organizations/1122334455 + purpose: GCE_FIREWALL + purpose_data: + network: my_project/my_vpc + short_name: net-environment + timeouts: null + module.org.google_tags_tag_key_iam_binding.default["net-environment:roles/resourcemanager.tagAdmin"]: + condition: [] + members: + - group:admins@example.com + role: roles/resourcemanager.tagAdmin + module.org.google_tags_tag_value.default["net-environment/dev"]: + description: Managed by the Terraform organization module. + short_name: dev + timeouts: null + module.org.google_tags_tag_value.default["net-environment/prod"]: + description: 'Environment: production.' + short_name: prod + timeouts: null + module.org.google_tags_tag_value_iam_binding.default["net-environment/prod:roles/resourcemanager.tagUser"]: + condition: [] + members: + - user:user1@example.com + role: roles/resourcemanager.tagUser + +counts: + google_tags_tag_key: 1 + google_tags_tag_key_iam_binding: 1 + google_tags_tag_value: 2 + google_tags_tag_value_iam_binding: 1 diff --git a/tests/modules/organization/examples/roles.yaml b/tests/modules/organization/examples/roles.yaml new file mode 100644 index 000000000..4705d1958 --- /dev/null +++ b/tests/modules/organization/examples/roles.yaml @@ -0,0 +1,33 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.org.google_organization_iam_binding.authoritative["organizations/1122334455/roles/myRole"]: + condition: [] + members: + - user:me@example.com + org_id: '1122334455' + role: organizations/1122334455/roles/myRole + module.org.google_organization_iam_custom_role.roles["myRole"]: + description: Terraform-managed. + org_id: '1122334455' + permissions: + - compute.instances.list + role_id: myRole + stage: GA + title: Custom role myRole + +counts: + google_organization_iam_binding: 1 + google_organization_iam_custom_role: 1 diff --git a/tests/modules/organization/examples/tags.yaml b/tests/modules/organization/examples/tags.yaml new file mode 100644 index 000000000..afbb7f8ff --- /dev/null +++ b/tests/modules/organization/examples/tags.yaml @@ -0,0 +1,53 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.org.google_tags_tag_binding.binding["env-prod"]: + parent: //cloudresourcemanager.googleapis.com/organizations/1122334455 + timeouts: null + module.org.google_tags_tag_binding.binding["foo"]: + parent: //cloudresourcemanager.googleapis.com/organizations/1122334455 + tag_value: tagValues/12345678 + timeouts: null + module.org.google_tags_tag_key.default["environment"]: + description: Environment specification. + parent: organizations/1122334455 + purpose: null + purpose_data: null + short_name: environment + timeouts: null + module.org.google_tags_tag_key_iam_binding.default["environment:roles/resourcemanager.tagAdmin"]: + condition: [] + members: + - group:admins@example.com + role: roles/resourcemanager.tagAdmin + module.org.google_tags_tag_value.default["environment/dev"]: + description: Managed by the Terraform organization module. + short_name: dev + timeouts: null + module.org.google_tags_tag_value.default["environment/prod"]: + description: 'Environment: production.' + short_name: prod + timeouts: null + module.org.google_tags_tag_value_iam_binding.default["environment/prod:roles/resourcemanager.tagViewer"]: + condition: [] + members: + - user:user1@example.com + role: roles/resourcemanager.tagViewer + +counts: + google_tags_tag_binding: 2 + google_tags_tag_key: 1 + google_tags_tag_key_iam_binding: 1 + google_tags_tag_value: 2 diff --git a/tests/modules/organization/firewall_policies.tfvars b/tests/modules/organization/firewall_policies.tfvars deleted file mode 100644 index 603cd3a47..000000000 --- a/tests/modules/organization/firewall_policies.tfvars +++ /dev/null @@ -1,45 +0,0 @@ -firewall_policies = { - policy1 = { - allow-ingress = { - description = "" - direction = "INGRESS" - action = "allow" - priority = 100 - ranges = ["10.0.0.0/8"] - ports = { - tcp = ["22"] - } - target_service_accounts = null - target_resources = null - logging = false - } - deny-egress = { - description = "" - direction = "EGRESS" - action = "deny" - priority = 200 - ranges = ["192.168.0.0/24"] - ports = { - tcp = ["443"] - } - target_service_accounts = null - target_resources = null - logging = false - } - } - policy2 = { - allow-ingress = { - description = "" - direction = "INGRESS" - action = "allow" - priority = 100 - ranges = ["10.0.0.0/8"] - ports = { - tcp = ["22"] - } - target_service_accounts = null - target_resources = null - logging = false - } - } -} diff --git a/tests/modules/organization/firewall_policies_factory.tfvars b/tests/modules/organization/firewall_policies_factory.tfvars deleted file mode 100644 index 3e1cf1813..000000000 --- a/tests/modules/organization/firewall_policies_factory.tfvars +++ /dev/null @@ -1,5 +0,0 @@ -firewall_policy_factory = { - cidr_file = "../../tests/modules/organization/data/firewall-cidrs.yaml" - policy_name = "factory-1" - rules_file = "../../tests/modules/organization/data/firewall-rules.yaml" -} diff --git a/tests/modules/organization/firewall_policies_factory_combined.tfvars b/tests/modules/organization/firewall_policies_factory_combined.tfvars index 6f848aa99..7ea51bb0c 100644 --- a/tests/modules/organization/firewall_policies_factory_combined.tfvars +++ b/tests/modules/organization/firewall_policies_factory_combined.tfvars @@ -1 +1,51 @@ -# skip boilerplate check +firewall_policies = { + policy1 = { + allow-ingress = { + description = "" + direction = "INGRESS" + action = "allow" + priority = 100 + ranges = ["10.0.0.0/8"] + ports = { + tcp = ["22"] + } + target_service_accounts = null + target_resources = null + logging = false + } + deny-egress = { + description = "" + direction = "EGRESS" + action = "deny" + priority = 200 + ranges = ["192.168.0.0/24"] + ports = { + tcp = ["443"] + } + target_service_accounts = null + target_resources = null + logging = false + } + } + policy2 = { + allow-ingress = { + description = "" + direction = "INGRESS" + action = "allow" + priority = 100 + ranges = ["10.0.0.0/8"] + ports = { + tcp = ["22"] + } + target_service_accounts = null + target_resources = null + logging = false + } + } +} + +firewall_policy_factory = { + cidr_file = "../../tests/modules/organization/data/firewall-cidrs.yaml" + policy_name = "factory-1" + rules_file = "../../tests/modules/organization/data/firewall-rules.yaml" +} diff --git a/tests/modules/organization/fixture/main.tf b/tests/modules/organization/fixture/main.tf deleted file mode 100644 index feb49f11e..000000000 --- a/tests/modules/organization/fixture/main.tf +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module "test" { - source = "../../../../modules/organization" - organization_id = "organizations/1234567890" - custom_roles = var.custom_roles - firewall_policies = var.firewall_policies - firewall_policy_association = var.firewall_policy_association - firewall_policy_factory = var.firewall_policy_factory - group_iam = var.group_iam - iam = var.iam - iam_additive = var.iam_additive - iam_additive_members = var.iam_additive_members - iam_audit_config = var.iam_audit_config - logging_sinks = var.logging_sinks - logging_exclusions = var.logging_exclusions - network_tags = var.network_tags - org_policies = var.org_policies - org_policies_data_path = var.org_policies_data_path - org_policy_custom_constraints = var.org_policy_custom_constraints - org_policy_custom_constraints_data_path = var.org_policy_custom_constraints_data_path - tag_bindings = var.tag_bindings - tags = var.tags -} diff --git a/tests/modules/organization/fixture/test.logging-sinks.tfvars b/tests/modules/organization/fixture/test.logging-sinks.tfvars deleted file mode 100644 index 95a272e1f..000000000 --- a/tests/modules/organization/fixture/test.logging-sinks.tfvars +++ /dev/null @@ -1,29 +0,0 @@ -logging_sinks = { - warning = { - destination = "mybucket" - type = "storage" - filter = "severity=WARNING" - } - info = { - destination = "projects/myproject/datasets/mydataset" - type = "bigquery" - filter = "severity=INFO" - disabled = true - } - notice = { - destination = "projects/myproject/topics/mytopic" - type = "pubsub" - filter = "severity=NOTICE" - include_children = false - } - debug = { - destination = "projects/myproject/locations/global/buckets/mybucket" - type = "logging" - filter = "severity=DEBUG" - include_children = false - exclusions = { - no-compute = "logName:compute" - no-container = "logName:container" - } - } -} diff --git a/tests/modules/organization/fixture/variables.tf b/tests/modules/organization/fixture/variables.tf deleted file mode 100644 index 35c777c4e..000000000 --- a/tests/modules/organization/fixture/variables.tf +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -variable "custom_roles" { - type = any - default = {} -} - -variable "group_iam" { - type = any - default = {} -} - -variable "iam" { - type = any - default = {} -} - -variable "iam_additive" { - type = any - default = {} -} - -variable "iam_additive_members" { - type = any - default = {} -} - -variable "iam_audit_config" { - type = any - default = {} -} - -variable "firewall_policies" { - type = any - default = {} -} - -variable "firewall_policy_association" { - type = any - default = {} -} - -variable "firewall_policy_factory" { - type = any - default = null -} - -variable "logging_sinks" { - type = any - default = {} -} - -variable "logging_exclusions" { - type = map(string) - default = {} -} - -variable "network_tags" { - type = any - default = null -} - -variable "org_policies" { - type = any - default = {} -} - -variable "org_policies_data_path" { - type = any - default = null -} - -variable "org_policy_custom_constraints" { - type = any - default = {} -} - -variable "org_policy_custom_constraints_data_path" { - type = any - default = null -} - -variable "tag_bindings" { - type = any - default = null -} - -variable "tags" { - type = any - default = null -} diff --git a/tests/modules/organization/iam.tfvars b/tests/modules/organization/iam.tfvars deleted file mode 100644 index 699631277..000000000 --- a/tests/modules/organization/iam.tfvars +++ /dev/null @@ -1,18 +0,0 @@ -group_iam = { - "owners@example.org" = [ - "roles/owner", - "roles/resourcemanager.folderAdmin" - ], - "viewers@example.org" = [ - "roles/viewer" - ] -} -iam = { - "roles/owner" = [ - "user:one@example.org", - "user:two@example.org" - ], - "roles/browser" = [ - "domain:example.org" - ] -} diff --git a/tests/modules/organization/iam.yaml b/tests/modules/organization/iam.yaml deleted file mode 100644 index 7b1a8cb95..000000000 --- a/tests/modules/organization/iam.yaml +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -values: - google_organization_iam_binding.authoritative["roles/browser"]: - condition: [] - members: - - domain:example.org - org_id: '1234567890' - role: roles/browser - google_organization_iam_binding.authoritative["roles/owner"]: - condition: [] - members: - - group:owners@example.org - - user:one@example.org - - user:two@example.org - org_id: '1234567890' - role: roles/owner - google_organization_iam_binding.authoritative["roles/resourcemanager.folderAdmin"]: - condition: [] - members: - - group:owners@example.org - org_id: '1234567890' - role: roles/resourcemanager.folderAdmin - google_organization_iam_binding.authoritative["roles/viewer"]: - condition: [] - members: - - group:viewers@example.org - org_id: '1234567890' - role: roles/viewer - -counts: - google_organization_iam_binding: 4 diff --git a/tests/modules/organization/iam_additive.tfvars b/tests/modules/organization/iam_additive.tfvars deleted file mode 100644 index 823d70ad3..000000000 --- a/tests/modules/organization/iam_additive.tfvars +++ /dev/null @@ -1,4 +0,0 @@ -iam = { - "user:one@example.org" = ["roles/owner"], - "user:two@example.org" = ["roles/owner", "roles/editor"] -} diff --git a/tests/modules/organization/logging.tfvars b/tests/modules/organization/logging.tfvars deleted file mode 100644 index 95a272e1f..000000000 --- a/tests/modules/organization/logging.tfvars +++ /dev/null @@ -1,29 +0,0 @@ -logging_sinks = { - warning = { - destination = "mybucket" - type = "storage" - filter = "severity=WARNING" - } - info = { - destination = "projects/myproject/datasets/mydataset" - type = "bigquery" - filter = "severity=INFO" - disabled = true - } - notice = { - destination = "projects/myproject/topics/mytopic" - type = "pubsub" - filter = "severity=NOTICE" - include_children = false - } - debug = { - destination = "projects/myproject/locations/global/buckets/mybucket" - type = "logging" - filter = "severity=DEBUG" - include_children = false - exclusions = { - no-compute = "logName:compute" - no-container = "logName:container" - } - } -} diff --git a/tests/modules/organization/logging.yaml b/tests/modules/organization/logging.yaml deleted file mode 100644 index 8038c9ab5..000000000 --- a/tests/modules/organization/logging.yaml +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -values: - google_bigquery_dataset_iam_member.bq-sinks-binding["info"]: - condition: [] - dataset_id: mydataset - project: myproject - role: roles/bigquery.dataEditor - google_logging_organization_sink.sink["debug"]: - description: debug (Terraform-managed). - destination: logging.googleapis.com/projects/myproject/locations/global/buckets/mybucket - disabled: false - exclusions: - - description: null - disabled: false - filter: logName:compute - name: no-compute - - description: null - disabled: false - filter: logName:container - name: no-container - filter: severity=DEBUG - include_children: false - name: debug - org_id: '1234567890' - google_logging_organization_sink.sink["info"]: - description: info (Terraform-managed). - destination: bigquery.googleapis.com/projects/myproject/datasets/mydataset - disabled: true - exclusions: [] - filter: severity=INFO - include_children: true - name: info - org_id: '1234567890' - google_logging_organization_sink.sink["notice"]: - description: notice (Terraform-managed). - destination: pubsub.googleapis.com/projects/myproject/topics/mytopic - disabled: false - exclusions: [] - filter: severity=NOTICE - include_children: false - name: notice - org_id: '1234567890' - google_logging_organization_sink.sink["warning"]: - description: warning (Terraform-managed). - destination: storage.googleapis.com/mybucket - disabled: false - exclusions: [] - filter: severity=WARNING - include_children: true - name: warning - org_id: '1234567890' - google_project_iam_member.bucket-sinks-binding["debug"]: - condition: - - expression: resource.name.endsWith('projects/myproject/locations/global/buckets/mybucket') - title: debug bucket writer - project: myproject - role: roles/logging.bucketWriter - google_pubsub_topic_iam_member.pubsub-sinks-binding["notice"]: - condition: [] - project: myproject - role: roles/pubsub.publisher - topic: mytopic - google_storage_bucket_iam_member.storage-sinks-binding["warning"]: - bucket: mybucket - condition: [] - role: roles/storage.objectCreator - -counts: - google_bigquery_dataset_iam_member: 1 - google_logging_organization_sink: 4 - google_project_iam_member: 1 - google_pubsub_topic_iam_member: 1 - google_storage_bucket_iam_member: 1 diff --git a/tests/modules/organization/logging_exclusions.tfvars b/tests/modules/organization/logging_exclusions.tfvars deleted file mode 100644 index 75c35604e..000000000 --- a/tests/modules/organization/logging_exclusions.tfvars +++ /dev/null @@ -1,4 +0,0 @@ -logging_exclusions = { - exclusion1 = "resource.type=gce_instance" - exclusion2 = "severity=NOTICE" -} diff --git a/tests/modules/organization/org_policies_boolean.yaml b/tests/modules/organization/org_policies_boolean.yaml index 310997a4c..00f98b06c 100644 --- a/tests/modules/organization/org_policies_boolean.yaml +++ b/tests/modules/organization/org_policies_boolean.yaml @@ -33,11 +33,6 @@ values: - inherit_from_parent: null reset: null rules: - - allow_all: null - condition: [] - deny_all: null - enforce: 'FALSE' - values: [] - allow_all: null condition: - description: test condition @@ -47,6 +42,11 @@ values: deny_all: null enforce: 'TRUE' values: [] + - allow_all: null + condition: [] + deny_all: null + enforce: 'FALSE' + values: [] timeouts: null counts: diff --git a/tests/modules/organization/org_policies_list.yaml b/tests/modules/organization/org_policies_list.yaml index 39c3a3896..393eadde4 100644 --- a/tests/modules/organization/org_policies_list.yaml +++ b/tests/modules/organization/org_policies_list.yaml @@ -20,14 +20,6 @@ values: - inherit_from_parent: null reset: null rules: - - allow_all: null - condition: [] - deny_all: null - enforce: null - values: - - allowed_values: null - denied_values: - - in:EXTERNAL - allow_all: null condition: - description: test condition @@ -49,6 +41,14 @@ values: deny_all: null enforce: null values: [] + - allow_all: null + condition: [] + deny_all: null + enforce: null + values: + - allowed_values: null + denied_values: + - in:EXTERNAL timeouts: null google_org_policy_policy.default["compute.vmExternalIpAccess"]: name: organizations/1234567890/policies/compute.vmExternalIpAccess diff --git a/tests/modules/organization/tags.tfvars b/tests/modules/organization/tags.tfvars index 31c6764e3..2a4dcb42f 100644 --- a/tests/modules/organization/tags.tfvars +++ b/tests/modules/organization/tags.tfvars @@ -10,6 +10,13 @@ tags = { iam = null values = null } + baz = { + id = "tagKeys/1234567890" + values = { + one = null + two = null + } + } foobar = { description = "Foobar tag." iam = { @@ -38,6 +45,15 @@ tags = { ] } } + four = { + description = "Foobar 4." + id = "tagValues/1234567890" + iam = { + "roles/resourcemanager.tagViewer" = [ + "user:user4@example.com" + ] + } + } } } } diff --git a/tests/modules/organization/tags.yaml b/tests/modules/organization/tags.yaml index 7da6f77bb..3e5524d47 100644 --- a/tests/modules/organization/tags.yaml +++ b/tests/modules/organization/tags.yaml @@ -38,11 +38,11 @@ values: purpose_data: network: foobar short_name: net_environment - google_tags_tag_key_iam_binding.default["foobar:roles/resourcemanager.tagAdmin"]: - condition: [] + ? google_tags_tag_key_iam_binding.default["foobar:roles/resourcemanager.tagAdmin"] + : condition: [] members: - - user:user1@example.com - - user:user2@example.com + - user:user1@example.com + - user:user2@example.com role: roles/resourcemanager.tagAdmin google_tags_tag_value.default["foobar/one"]: description: Managed by the Terraform organization module. @@ -53,24 +53,24 @@ values: google_tags_tag_value.default["foobar/two"]: description: Foobar 2. short_name: two - google_tags_tag_value_iam_binding.default["foobar/three:roles/resourcemanager.tagAdmin"]: - condition: [] + ? google_tags_tag_value_iam_binding.default["foobar/three:roles/resourcemanager.tagAdmin"] + : condition: [] members: - - user:user4@example.com + - user:user4@example.com role: roles/resourcemanager.tagAdmin - google_tags_tag_value_iam_binding.default["foobar/three:roles/resourcemanager.tagViewer"]: - condition: [] + ? google_tags_tag_value_iam_binding.default["foobar/three:roles/resourcemanager.tagViewer"] + : condition: [] members: - - user:user3@example.com + - user:user3@example.com role: roles/resourcemanager.tagViewer - google_tags_tag_value_iam_binding.default["foobar/two:roles/resourcemanager.tagViewer"]: - condition: [] + ? google_tags_tag_value_iam_binding.default["foobar/two:roles/resourcemanager.tagViewer"] + : condition: [] members: - - user:user3@example.com + - user:user3@example.com role: roles/resourcemanager.tagViewer counts: google_tags_tag_key: 4 google_tags_tag_key_iam_binding: 1 - google_tags_tag_value: 3 - google_tags_tag_value_iam_binding: 3 + google_tags_tag_value: 5 + google_tags_tag_value_iam_binding: 4 diff --git a/tests/modules/organization/test_plan_org_policies.py b/tests/modules/organization/test_plan_org_policies.py index 1e041dbc4..f5002523a 100644 --- a/tests/modules/organization/test_plan_org_policies.py +++ b/tests/modules/organization/test_plan_org_policies.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pathlib - import pytest _params = ['boolean', 'list'] diff --git a/tests/modules/organization/tftest.yaml b/tests/modules/organization/tftest.yaml index c88e05c12..7568b732a 100644 --- a/tests/modules/organization/tftest.yaml +++ b/tests/modules/organization/tftest.yaml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,17 +19,8 @@ common_tfvars: tests: audit_config: - iam: - iam_additive: - logging: - logging_exclusions: org_policies_list: org_policies_boolean: org_policies_custom_constraints: - tags: - firewall_policies: - firewall_policies_factory: firewall_policies_factory_combined: - tfvars: - - firewall_policies.tfvars - - firewall_policies_factory.tfvars + tags: diff --git a/tests/modules/project/common.tfvars b/tests/modules/project/common.tfvars new file mode 100644 index 000000000..9bd31b6fc --- /dev/null +++ b/tests/modules/project/common.tfvars @@ -0,0 +1 @@ +name = "my-project" diff --git a/tests/modules/project/examples/basic.yaml b/tests/modules/project/examples/basic.yaml new file mode 100644 index 000000000..a6ae5af3e --- /dev/null +++ b/tests/modules/project/examples/basic.yaml @@ -0,0 +1,39 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +values: + module.project.google_project.project[0]: + auto_create_network: false + billing_account: 123456-123456-123456 + folder_id: '1234567890' + labels: null + name: foo-myproject + org_id: null + project_id: foo-myproject + skip_delete: false + module.project.google_project_service.project_services["container.googleapis.com"]: + disable_dependent_services: false + disable_on_destroy: false + project: foo-myproject + service: container.googleapis.com + module.project.google_project_service.project_services["stackdriver.googleapis.com"]: + disable_dependent_services: false + disable_on_destroy: false + project: foo-myproject + service: stackdriver.googleapis.com + +counts: + google_project: 1 + google_project_service: 2 diff --git a/tests/modules/project/examples/iam-additive-members.yaml b/tests/modules/project/examples/iam-additive-members.yaml new file mode 100644 index 000000000..6a517a4a1 --- /dev/null +++ b/tests/modules/project/examples/iam-additive-members.yaml @@ -0,0 +1,33 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.project.google_project.project[0]: + project_id: project-example + module.project.google_project_iam_member.additive["roles/editor-user:two@example.org"]: + condition: [] + project: project-example + role: roles/editor + module.project.google_project_iam_member.additive["roles/owner-user:one@example.org"]: + condition: [] + project: project-example + role: roles/owner + module.project.google_project_iam_member.additive["roles/owner-user:two@example.org"]: + condition: [] + project: project-example + role: roles/owner + +counts: + google_project: 1 + google_project_iam_member: 3 diff --git a/tests/modules/project/examples/iam-additive.yaml b/tests/modules/project/examples/iam-additive.yaml new file mode 100644 index 000000000..5bab82232 --- /dev/null +++ b/tests/modules/project/examples/iam-additive.yaml @@ -0,0 +1,36 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.project.google_project.project[0]: {} + module.project.google_project_iam_member.additive["roles/owner-group:three@example.org"]: + condition: [] + project: project-example + role: roles/owner + module.project.google_project_iam_member.additive["roles/storage.objectAdmin-group:two@example.org"]: + condition: [] + project: project-example + role: roles/storage.objectAdmin + module.project.google_project_iam_member.additive["roles/viewer-group:one@example.org"]: + condition: [] + project: project-example + role: roles/viewer + module.project.google_project_iam_member.additive["roles/viewer-group:two@xample.org"]: + condition: [] + project: project-example + role: roles/viewer + +counts: + google_project: 1 + google_project_iam_member: 4 diff --git a/tests/modules/project/examples/iam-authoritative.yaml b/tests/modules/project/examples/iam-authoritative.yaml new file mode 100644 index 000000000..f190a4298 --- /dev/null +++ b/tests/modules/project/examples/iam-authoritative.yaml @@ -0,0 +1,39 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.project.google_project.project[0]: {} + module.project.google_project_iam_binding.authoritative["roles/container.hostServiceAgentUser"]: + condition: [] + members: + - serviceAccount:my_gke_service_account + project: foo-project-example + role: roles/container.hostServiceAgentUser + module.project.google_project_service.project_services["container.googleapis.com"]: + disable_dependent_services: false + disable_on_destroy: false + project: foo-project-example + service: container.googleapis.com + timeouts: null + module.project.google_project_service.project_services["stackdriver.googleapis.com"]: + disable_dependent_services: false + disable_on_destroy: false + project: foo-project-example + service: stackdriver.googleapis.com + timeouts: null + +counts: + google_project: 1 + google_project_iam_binding: 1 + google_project_service: 2 diff --git a/tests/modules/project/examples/iam-group.yaml b/tests/modules/project/examples/iam-group.yaml new file mode 100644 index 000000000..02728d019 --- /dev/null +++ b/tests/modules/project/examples/iam-group.yaml @@ -0,0 +1,44 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.project.google_project.project[0]: {} + module.project.google_project_iam_binding.authoritative["roles/cloudasset.owner"]: + condition: [] + members: + - group:gcp-security-admins@example.com + project: foo-project-example + role: roles/cloudasset.owner + module.project.google_project_iam_binding.authoritative["roles/cloudsupport.techSupportEditor"]: + condition: [] + members: + - group:gcp-security-admins@example.com + project: foo-project-example + role: roles/cloudsupport.techSupportEditor + module.project.google_project_iam_binding.authoritative["roles/iam.securityReviewer"]: + condition: [] + members: + - group:gcp-security-admins@example.com + project: foo-project-example + role: roles/iam.securityReviewer + module.project.google_project_iam_binding.authoritative["roles/logging.admin"]: + condition: [] + members: + - group:gcp-security-admins@example.com + project: foo-project-example + role: roles/logging.admin + +counts: + google_project: 1 + google_project_iam_binding: 4 diff --git a/tests/modules/project/examples/kms.yaml b/tests/modules/project/examples/kms.yaml new file mode 100644 index 000000000..b3981881a --- /dev/null +++ b/tests/modules/project/examples/kms.yaml @@ -0,0 +1,38 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.org.google_tags_tag_key.default["environment"]: + description: Environment specification. + parent: organizations/1122334455 + purpose: null + purpose_data: null + short_name: environment + module.org.google_tags_tag_value.default["environment/dev"]: + description: Managed by the Terraform organization module. + short_name: dev + module.org.google_tags_tag_value.default["environment/prod"]: + description: Managed by the Terraform organization module. + short_name: prod + module.project.google_project.project[0]: + project_id: test-project + module.project.google_tags_tag_binding.binding["env-prod"]: {} + module.project.google_tags_tag_binding.binding["foo"]: + tag_value: tagValues/12345678 + +counts: + google_project: 1 + google_tags_tag_binding: 2 + google_tags_tag_key: 1 + google_tags_tag_value: 2 diff --git a/tests/modules/project/examples/logging.yaml b/tests/modules/project/examples/logging.yaml new file mode 100644 index 000000000..9902c0adc --- /dev/null +++ b/tests/modules/project/examples/logging.yaml @@ -0,0 +1,94 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.project-host.google_bigquery_dataset_iam_member.bq-sinks-binding["info"]: + condition: [] + role: roles/bigquery.dataEditor + module.project-host.google_logging_project_exclusion.logging-exclusion["no-gce-instances"]: + description: no-gce-instances (Terraform-managed). + disabled: null + filter: resource.type=gce_instance + name: no-gce-instances + project: my-project + module.project-host.google_logging_project_sink.sink["debug"]: + description: debug (Terraform-managed). + disabled: false + exclusions: + - description: null + disabled: false + filter: logName:compute + name: no-compute + filter: severity=DEBUG + name: debug + project: my-project + unique_writer_identity: false + module.project-host.google_logging_project_sink.sink["info"]: + description: info (Terraform-managed). + disabled: false + exclusions: [] + filter: severity=INFO + name: info + project: my-project + unique_writer_identity: false + module.project-host.google_logging_project_sink.sink["notice"]: + description: notice (Terraform-managed). + disabled: false + exclusions: [] + filter: severity=NOTICE + name: notice + project: my-project + unique_writer_identity: false + module.project-host.google_logging_project_sink.sink["warnings"]: + description: warnings (Terraform-managed). + destination: storage.googleapis.com/gcs_sink + disabled: false + exclusions: [] + filter: severity=WARNING + name: warnings + project: my-project + unique_writer_identity: false + module.project-host.google_project.project[0]: + auto_create_network: false + billing_account: 123456-123456-123456 + folder_id: '1234567890' + labels: null + name: my-project + org_id: null + project_id: my-project + skip_delete: false + module.project-host.google_project_iam_member.bucket-sinks-binding["debug"]: + condition: + - title: debug bucket writer + role: roles/logging.bucketWriter + module.project-host.google_pubsub_topic_iam_member.pubsub-sinks-binding["notice"]: + condition: [] + role: roles/pubsub.publisher + module.project-host.google_storage_bucket_iam_member.gcs-sinks-binding["warnings"]: + bucket: gcs_sink + condition: [] + role: roles/storage.objectCreator + +counts: + google_bigquery_dataset: 1 + google_bigquery_dataset_iam_member: 1 + google_logging_project_bucket_config: 1 + google_logging_project_exclusion: 1 + google_logging_project_sink: 4 + google_project: 1 + google_project_iam_member: 1 + google_pubsub_topic: 1 + google_pubsub_topic_iam_member: 1 + google_storage_bucket: 1 + google_storage_bucket_iam_member: 1 diff --git a/tests/modules/project/examples/org-policies.yaml b/tests/modules/project/examples/org-policies.yaml new file mode 100644 index 000000000..8841dedee --- /dev/null +++ b/tests/modules/project/examples/org-policies.yaml @@ -0,0 +1,125 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.project.google_org_policy_policy.default["compute.disableGuestAttributesAccess"]: + name: projects/foo-project-example/policies/compute.disableGuestAttributesAccess + parent: projects/foo-project-example + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: 'TRUE' + values: [] + module.project.google_org_policy_policy.default["constraints/compute.skipDefaultNetworkCreation"]: + name: projects/foo-project-example/policies/constraints/compute.skipDefaultNetworkCreation + parent: projects/foo-project-example + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: 'TRUE' + values: [] + module.project.google_org_policy_policy.default["constraints/compute.trustedImageProjects"]: + name: projects/foo-project-example/policies/constraints/compute.trustedImageProjects + parent: projects/foo-project-example + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: null + values: + - allowed_values: + - projects/my-project + denied_values: null + module.project.google_org_policy_policy.default["constraints/compute.vmExternalIpAccess"]: + name: projects/foo-project-example/policies/constraints/compute.vmExternalIpAccess + parent: projects/foo-project-example + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: 'TRUE' + enforce: null + values: [] + module.project.google_org_policy_policy.default["constraints/iam.allowedPolicyMemberDomains"]: + name: projects/foo-project-example/policies/constraints/iam.allowedPolicyMemberDomains + parent: projects/foo-project-example + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: null + values: + - allowed_values: + - C0xxxxxxx + - C0yyyyyyy + denied_values: null + module.project.google_org_policy_policy.default["iam.disableServiceAccountKeyCreation"]: + name: projects/foo-project-example/policies/iam.disableServiceAccountKeyCreation + parent: projects/foo-project-example + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: 'TRUE' + values: [] + module.project.google_org_policy_policy.default["iam.disableServiceAccountKeyUpload"]: + name: projects/foo-project-example/policies/iam.disableServiceAccountKeyUpload + parent: projects/foo-project-example + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: + - description: test condition + expression: resource.matchTagId("tagKeys/1234", "tagValues/1234") + location: somewhere + title: condition + deny_all: null + enforce: 'TRUE' + values: [] + - allow_all: null + condition: [] + deny_all: null + enforce: 'FALSE' + values: [] + module.project.google_project.project[0]: + billing_account: 123456-123456-123456 + folder_id: '1234567890' + name: foo-project-example + org_id: null + project_id: foo-project-example + +counts: + google_org_policy_policy: 7 + google_project: 1 diff --git a/tests/modules/project/examples/outputs.yaml b/tests/modules/project/examples/outputs.yaml new file mode 100644 index 000000000..339896625 --- /dev/null +++ b/tests/modules/project/examples/outputs.yaml @@ -0,0 +1,27 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.project.google_project.project[0]: + project_id: project-example + module.project.google_project_service.project_services["compute.googleapis.com"]: + project: project-example + service: compute.googleapis.com + +counts: + google_project: 1 + google_project_service: 1 + +outputs: + compute_robot: __missing__ diff --git a/tests/modules/project/examples/shared-vpc.yaml b/tests/modules/project/examples/shared-vpc.yaml new file mode 100644 index 000000000..b03f220af --- /dev/null +++ b/tests/modules/project/examples/shared-vpc.yaml @@ -0,0 +1,46 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.host-project.google_compute_shared_vpc_host_project.shared_vpc_host[0]: + project: my-host-project + module.host-project.google_project.project[0]: + project_id: my-host-project + module.service-project.google_compute_shared_vpc_service_project.shared_vpc_service[0]: + host_project: my-host-project + service_project: my-service-project + module.service-project.google_project.project[0]: + project_id: my-service-project + module.service-project.google_project_iam_member.shared_vpc_host_robots["roles/compute.networkUser:cloudservices"]: + condition: [] + project: my-host-project + role: roles/compute.networkUser + module.service-project.google_project_iam_member.shared_vpc_host_robots["roles/compute.networkUser:container-engine"]: + condition: [] + project: my-host-project + role: roles/compute.networkUser + module.service-project.google_project_iam_member.shared_vpc_host_robots["roles/container.hostServiceAgentUser:container-engine"]: + condition: [] + project: my-host-project + role: roles/container.hostServiceAgentUser + module.service-project.google_project_iam_member.shared_vpc_host_robots["roles/vpcaccess.user:cloudrun"]: + condition: [] + project: my-host-project + role: roles/vpcaccess.user + +counts: + google_compute_shared_vpc_host_project: 1 + google_compute_shared_vpc_service_project: 1 + google_project: 2 + google_project_iam_member: 4 diff --git a/tests/modules/project/fixture/main.tf b/tests/modules/project/fixture/main.tf deleted file mode 100644 index 08cf49dc6..000000000 --- a/tests/modules/project/fixture/main.tf +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module "test" { - source = "../../../../modules/project" - name = var.name - billing_account = var.billing_account - auto_create_network = var.auto_create_network - custom_roles = var.custom_roles - iam = var.iam - iam_additive = var.iam_additive - iam_additive_members = var.iam_additive_members - labels = var.labels - lien_reason = var.lien_reason - org_policies = var.org_policies - org_policies_data_path = var.org_policies_data_path - oslogin = var.oslogin - oslogin_admins = var.oslogin_admins - oslogin_users = var.oslogin_users - parent = var.parent - prefix = var.prefix - service_encryption_key_ids = var.service_encryption_key_ids - services = var.services - logging_sinks = var.logging_sinks - logging_exclusions = var.logging_exclusions - shared_vpc_host_config = var.shared_vpc_host_config -} - -module "test-svpc-service" { - source = "../../../../modules/project" - count = var._test_service_project ? 1 : 0 - name = "test-svc" - billing_account = var.billing_account - auto_create_network = false - parent = var.parent - services = var.services - shared_vpc_service_config = { - attach = true - host_project = module.test.project_id - service_identity_iam = { - "roles/compute.networkUser" = [ - "cloudservices", "container-engine" - ] - "roles/vpcaccess.user" = [ - "cloudrun" - ] - "roles/container.hostServiceAgentUser" = [ - "container-engine" - ] - } - } -} diff --git a/tests/modules/project/fixture/test.logging-sinks.tfvars b/tests/modules/project/fixture/test.logging-sinks.tfvars deleted file mode 100644 index 5c79cfb55..000000000 --- a/tests/modules/project/fixture/test.logging-sinks.tfvars +++ /dev/null @@ -1,29 +0,0 @@ -logging_sinks = { - warning = { - destination = "mybucket" - type = "storage" - filter = "severity=WARNING" - } - info = { - destination = "projects/myproject/datasets/mydataset" - type = "bigquery" - filter = "severity=INFO" - disabled = true - } - notice = { - destination = "projects/myproject/topics/mytopic" - type = "pubsub" - filter = "severity=NOTICE" - unique_writer = true - } - debug = { - destination = "projects/myproject/locations/global/buckets/mybucket" - type = "logging" - filter = "severity=DEBUG" - exclusions = { - no-compute = "logName:compute" - no-container = "logName:container" - } - unique_writer = true - } -} diff --git a/tests/modules/project/fixture/variables.tf b/tests/modules/project/fixture/variables.tf deleted file mode 100644 index 4c3474f00..000000000 --- a/tests/modules/project/fixture/variables.tf +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -variable "_test_service_project" { - type = bool - default = false -} - -variable "name" { - type = string - default = "my-project" -} - -variable "billing_account" { - type = string - default = "12345-12345-12345" -} - -variable "auto_create_network" { - type = bool - default = false -} - -variable "custom_roles" { - type = map(list(string)) - default = {} -} - -variable "iam" { - type = map(list(string)) - default = {} -} - -variable "iam_additive" { - type = map(list(string)) - default = {} -} - -variable "iam_additive_members" { - type = map(list(string)) - default = {} -} - -variable "labels" { - type = map(string) - default = {} -} - -variable "lien_reason" { - type = string - default = "" -} - -variable "org_policies" { - type = any - default = {} -} - -variable "org_policies_data_path" { - type = any - default = null -} - -variable "oslogin" { - type = bool - default = false -} - -variable "oslogin_admins" { - type = list(string) - default = [] -} - -variable "oslogin_users" { - type = list(string) - default = [] -} - -variable "parent" { - type = string - default = null -} - -variable "prefix" { - type = string - default = null -} - -variable "service_encryption_key_ids" { - type = map(list(string)) - default = {} -} - -variable "services" { - type = list(string) - default = [] -} - -variable "logging_sinks" { - type = any - default = {} -} - -variable "logging_exclusions" { - type = map(string) - default = {} -} - -variable "shared_vpc_host_config" { - type = object({ - enabled = bool - service_projects = list(string) - }) - default = { - enabled = true - service_projects = [ - "my-service-project-1", - "my-service-project-2" - ] - } -} diff --git a/tests/modules/project/no_parent.tfvars b/tests/modules/project/no_parent.tfvars new file mode 100644 index 000000000..8b954bdef --- /dev/null +++ b/tests/modules/project/no_parent.tfvars @@ -0,0 +1 @@ +parent = null diff --git a/tests/modules/project/no_parent.yaml b/tests/modules/project/no_parent.yaml new file mode 100644 index 000000000..57f2fbd49 --- /dev/null +++ b/tests/modules/project/no_parent.yaml @@ -0,0 +1,22 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + google_project.project[0]: + project_id: my-project + folder_id: null + org_id: null + +counts: + google_project: 1 diff --git a/tests/modules/project/no_prefix.tfvars b/tests/modules/project/no_prefix.tfvars new file mode 100644 index 000000000..3d1b7ab3e --- /dev/null +++ b/tests/modules/project/no_prefix.tfvars @@ -0,0 +1 @@ +prefix = null diff --git a/tests/modules/project/no_prefix.yaml b/tests/modules/project/no_prefix.yaml new file mode 100644 index 000000000..6322ca9c1 --- /dev/null +++ b/tests/modules/project/no_prefix.yaml @@ -0,0 +1,20 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + google_project.project[0]: + project_id: my-project + +counts: + google_project: 1 diff --git a/tests/modules/project/fixture/test.orgpolicies-boolean.tfvars b/tests/modules/project/org_policies_boolean.tfvars similarity index 100% rename from tests/modules/project/fixture/test.orgpolicies-boolean.tfvars rename to tests/modules/project/org_policies_boolean.tfvars diff --git a/tests/modules/project/org_policies_boolean.yaml b/tests/modules/project/org_policies_boolean.yaml new file mode 100644 index 000000000..4f23958fb --- /dev/null +++ b/tests/modules/project/org_policies_boolean.yaml @@ -0,0 +1,53 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + google_org_policy_policy.default["iam.disableServiceAccountKeyCreation"]: + name: projects/my-project/policies/iam.disableServiceAccountKeyCreation + parent: projects/my-project + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: 'TRUE' + values: [] + timeouts: null + google_org_policy_policy.default["iam.disableServiceAccountKeyUpload"]: + name: projects/my-project/policies/iam.disableServiceAccountKeyUpload + parent: projects/my-project + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: + - description: test condition + expression: resource.matchTagId(aa, bb) + location: xxx + title: condition + deny_all: null + enforce: 'TRUE' + values: [] + - allow_all: null + condition: [] + deny_all: null + enforce: 'FALSE' + values: [] + timeouts: null + +counts: + google_org_policy_policy: 2 diff --git a/tests/modules/project/fixture/test.orgpolicies-list.tfvars b/tests/modules/project/org_policies_list.tfvars similarity index 96% rename from tests/modules/project/fixture/test.orgpolicies-list.tfvars rename to tests/modules/project/org_policies_list.tfvars index 738071733..f9de8dbab 100644 --- a/tests/modules/project/fixture/test.orgpolicies-list.tfvars +++ b/tests/modules/project/org_policies_list.tfvars @@ -3,6 +3,7 @@ org_policies = { deny = { all = true } } "iam.allowedPolicyMemberDomains" = { + inherit_from_parent = true allow = { values = ["C0xxxxxxx", "C0yyyyyyy"] } diff --git a/tests/modules/project/org_policies_list.yaml b/tests/modules/project/org_policies_list.yaml new file mode 100644 index 000000000..2f1c64e0b --- /dev/null +++ b/tests/modules/project/org_policies_list.yaml @@ -0,0 +1,85 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + google_org_policy_policy.default["compute.restrictLoadBalancerCreationForTypes"]: + name: projects/my-project/policies/compute.restrictLoadBalancerCreationForTypes + parent: projects/my-project + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: + - description: test condition + expression: resource.matchTagId(aa, bb) + location: xxx + title: condition + deny_all: null + enforce: null + values: + - allowed_values: + - EXTERNAL_1 + denied_values: null + - allow_all: 'TRUE' + condition: + - description: test condition2 + expression: resource.matchTagId(cc, dd) + location: xxx + title: condition2 + deny_all: null + enforce: null + values: [] + - allow_all: null + condition: [] + deny_all: null + enforce: null + values: + - allowed_values: null + denied_values: + - in:EXTERNAL + timeouts: null + google_org_policy_policy.default["compute.vmExternalIpAccess"]: + name: projects/my-project/policies/compute.vmExternalIpAccess + parent: projects/my-project + spec: + - inherit_from_parent: null + reset: null + rules: + - allow_all: null + condition: [] + deny_all: 'TRUE' + enforce: null + values: [] + timeouts: null + google_org_policy_policy.default["iam.allowedPolicyMemberDomains"]: + name: projects/my-project/policies/iam.allowedPolicyMemberDomains + parent: projects/my-project + spec: + - inherit_from_parent: true + reset: null + rules: + - allow_all: null + condition: [] + deny_all: null + enforce: null + values: + - allowed_values: + - C0xxxxxxx + - C0yyyyyyy + denied_values: null + timeouts: null + +counts: + google_org_policy_policy: 3 diff --git a/tests/modules/project/parent_folder.tfvars b/tests/modules/project/parent_folder.tfvars new file mode 100644 index 000000000..e7ce6acda --- /dev/null +++ b/tests/modules/project/parent_folder.tfvars @@ -0,0 +1 @@ +parent = "folders/12345678" diff --git a/tests/modules/api_gateway/test_plan.py b/tests/modules/project/parent_folder.yaml similarity index 80% rename from tests/modules/api_gateway/test_plan.py rename to tests/modules/project/parent_folder.yaml index 18ecdd329..684f94d81 100644 --- a/tests/modules/api_gateway/test_plan.py +++ b/tests/modules/project/parent_folder.yaml @@ -12,8 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +values: + google_project.project[0]: + project_id: my-project + folder_id: '12345678' + org_id: null -def test_resource_count(plan_runner): - "Test number of resources created." - _, resources = plan_runner() - assert len(resources) == 5 +counts: + google_project: 1 diff --git a/tests/modules/project/parent_org.tfvars b/tests/modules/project/parent_org.tfvars new file mode 100644 index 000000000..86ce1eec6 --- /dev/null +++ b/tests/modules/project/parent_org.tfvars @@ -0,0 +1 @@ +parent = "organizations/12345678" diff --git a/tests/modules/project/parent_org.yaml b/tests/modules/project/parent_org.yaml new file mode 100644 index 000000000..ded3f6f30 --- /dev/null +++ b/tests/modules/project/parent_org.yaml @@ -0,0 +1,22 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + google_project.project[0]: + project_id: my-project + folder_id: null + org_id: '12345678' + +counts: + google_project: 1 diff --git a/tests/modules/project/prefix.tfvars b/tests/modules/project/prefix.tfvars new file mode 100644 index 000000000..0031d561d --- /dev/null +++ b/tests/modules/project/prefix.tfvars @@ -0,0 +1 @@ +prefix = "foo" diff --git a/tests/modules/project/prefix.yaml b/tests/modules/project/prefix.yaml new file mode 100644 index 000000000..e5126e200 --- /dev/null +++ b/tests/modules/project/prefix.yaml @@ -0,0 +1,20 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + google_project.project[0]: + project_id: foo-my-project + +counts: + google_project: 1 diff --git a/tests/modules/project/service_encryption_keys.tfvars b/tests/modules/project/service_encryption_keys.tfvars new file mode 100644 index 000000000..f0dedc21a --- /dev/null +++ b/tests/modules/project/service_encryption_keys.tfvars @@ -0,0 +1,4 @@ +service_encryption_key_ids = { + compute = ["key1"], + storage = ["key1", "key2"] +} diff --git a/tests/modules/project/service_encryption_keys.yaml b/tests/modules/project/service_encryption_keys.yaml new file mode 100644 index 000000000..8e2bd8236 --- /dev/null +++ b/tests/modules/project/service_encryption_keys.yaml @@ -0,0 +1,44 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + google_kms_crypto_key_iam_member.service_identity_cmek["compute.key1"]: + condition: [] + crypto_key_id: key1 + role: roles/cloudkms.cryptoKeyEncrypterDecrypter + google_kms_crypto_key_iam_member.service_identity_cmek["storage.key1"]: + condition: [] + crypto_key_id: key1 + role: roles/cloudkms.cryptoKeyEncrypterDecrypter + google_kms_crypto_key_iam_member.service_identity_cmek["storage.key2"]: + condition: [] + crypto_key_id: key2 + role: roles/cloudkms.cryptoKeyEncrypterDecrypter + google_project.project[0]: + auto_create_network: false + billing_account: null + folder_id: null + labels: null + name: my-project + org_id: null + project_id: my-project + skip_delete: false + timeouts: null + +counts: + google_kms_crypto_key_iam_member: 3 + google_project: 1 + +outputs: + name: my-project diff --git a/tests/modules/project/test_iam.py b/tests/modules/project/test_iam.py deleted file mode 100644 index 16581baa9..000000000 --- a/tests/modules/project/test_iam.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -def test_iam(plan_runner): - "Test IAM bindings." - iam = ( - '{"roles/owner" = ["user:one@example.org"],' - '"roles/viewer" = ["user:two@example.org", "user:three@example.org"]}' - ) - _, resources = plan_runner(iam=iam) - roles = dict((r['values']['role'], r['values']['members']) - for r in resources if r['type'] == 'google_project_iam_binding') - assert roles == { - 'roles/owner': ['user:one@example.org'], - 'roles/viewer': ['user:three@example.org', 'user:two@example.org']} - - -def test_iam_additive(plan_runner): - "Test IAM additive bindings." - iam = ( - '{"roles/owner" = ["user:one@example.org"],' - '"roles/viewer" = ["user:two@example.org", "user:three@example.org"]}' - ) - _, resources = plan_runner(iam_additive=iam) - roles = set((r['values']['role'], r['values']['member']) - for r in resources if r['type'] == 'google_project_iam_member') - assert roles == set([ - ('roles/owner', 'user:one@example.org'), - ('roles/viewer', 'user:three@example.org'), - ('roles/viewer', 'user:two@example.org') - ]) - - -def test_iam_additive_members(plan_runner): - "Test IAM additive members." - iam = ( - '{"user:one@example.org" = ["roles/owner"],' - '"user:two@example.org" = ["roles/owner", "roles/editor"]}' - ) - _, resources = plan_runner(iam_additive_members=iam) - roles = set((r['values']['role'], r['values']['member']) - for r in resources if r['type'] == 'google_project_iam_member') - assert roles == set([ - ('roles/owner', 'user:one@example.org'), - ('roles/owner', 'user:two@example.org'), - ('roles/editor', 'user:two@example.org') - ]) diff --git a/tests/modules/project/test_plan.py b/tests/modules/project/test_plan.py index 8d1bd538c..50d50b3c7 100644 --- a/tests/modules/project/test_plan.py +++ b/tests/modules/project/test_plan.py @@ -12,65 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -def test_prefix(plan_runner): - "Test project id prefix." - _, resources = plan_runner() - assert len(resources) == 4 - [project_resource] = [r for r in resources if r['address'] == 'module.test.google_project.project[0]'] - assert project_resource['values']['name'] == 'my-project' - _, resources = plan_runner(prefix='foo') - assert len(resources) == 4 - [project_resource] = [r for r in resources if r['address'] == 'module.test.google_project.project[0]'] - assert project_resource['values']['name'] == 'foo-my-project' - - -def test_parent(plan_runner): - "Test project parent." - _, resources = plan_runner(parent='folders/12345678') - assert len(resources) == 4 - [project_resource] = [r for r in resources if r['address'] == 'module.test.google_project.project[0]'] - assert project_resource['values']['folder_id'] == '12345678' - assert project_resource['values'].get('org_id') == None - _, resources = plan_runner(parent='organizations/12345678') - assert len(resources) == 4 - [project_resource] = [r for r in resources if r['address'] == 'module.test.google_project.project[0]'] - assert project_resource['values']['org_id'] == '12345678' - assert project_resource['values'].get('folder_id') == None - - -def test_no_parent(plan_runner): - "Test null project parent." - _, resources = plan_runner() - assert len(resources) == 4 - [project_resource] = [r for r in resources if r['address'] == 'module.test.google_project.project[0]'] - assert project_resource['values'].get('folder_id') == None - assert project_resource['values'].get('org_id') == None - - -def test_service_encryption_keys(plan_runner): - "Test service encryption keys with no dependencies." - _, resources = plan_runner(service_encryption_key_ids=( - '{compute=["key1"], storage=["key1", "key2"]}' - )) - key_bindings = [ - r['index'] for r in resources - if r['type'] == 'google_kms_crypto_key_iam_member' - ] - assert len(key_bindings), 3 - assert key_bindings == ['compute.key1', 'storage.key1', 'storage.key2'] - - -def test_service_encryption_key_dependencies(plan_runner): - "Test service encryption keys with dependencies." - _, resources = plan_runner(service_encryption_key_ids=( - '{compute=["key1"], dataflow=["key1", "key2"]}' - )) - key_bindings = [ - r['index'] for r in resources - if r['type'] == 'google_kms_crypto_key_iam_member' - ] - assert len(key_bindings), 3 - # compute.key1 cannot repeat or we'll get a duplicate key error in for_each - assert key_bindings == [ - 'compute.key1', 'compute.key2', 'dataflow.key1', 'dataflow.key2' - ] +# def test_service_encryption_key_dependencies(plan_runner): +# "Test service encryption keys with dependencies." +# _, resources = plan_runner(service_encryption_key_ids=( +# '{compute=["key1"], dataflow=["key1", "key2"]}')) +# key_bindings = [ +# r['index'] +# for r in resources +# if r['type'] == 'google_kms_crypto_key_iam_member' +# ] +# assert len(key_bindings), 3 +# # compute.key1 cannot repeat or we'll get a duplicate key error in for_each +# assert key_bindings == [ +# 'compute.key1', 'compute.key2', 'dataflow.key1', 'dataflow.key2' +# ] diff --git a/tests/modules/project/test_plan_logging.py b/tests/modules/project/test_plan_logging.py deleted file mode 100644 index 59c9179bc..000000000 --- a/tests/modules/project/test_plan_logging.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections import Counter - - -def test_sinks(plan_runner): - "Test folder-level sinks." - tfvars = 'test.logging-sinks.tfvars' - _, resources = plan_runner(tf_var_file=tfvars) - assert len(resources) == 12 - - resource_types = Counter([r["type"] for r in resources]) - assert resource_types == { - "google_logging_project_sink": 4, - "google_bigquery_dataset_iam_member": 1, - "google_project": 1, - "google_project_iam_member": 1, - "google_pubsub_topic_iam_member": 1, - "google_storage_bucket_iam_member": 1, - "google_compute_shared_vpc_host_project": 1, - "google_compute_shared_vpc_service_project": 2 - } - - sinks = [r for r in resources if r["type"] == "google_logging_project_sink"] - assert sorted([r["index"] for r in sinks]) == [ - "debug", - "info", - "notice", - "warning", - ] - values = [( - r["index"], - r["values"]["filter"], - r["values"]["destination"], - r["values"]["unique_writer_identity"], - ) for r in sinks] - assert sorted(values) == [ - ( - "debug", - "severity=DEBUG", - "logging.googleapis.com/projects/myproject/locations/global/buckets/mybucket", - True, - ), - ( - "info", - "severity=INFO", - "bigquery.googleapis.com/projects/myproject/datasets/mydataset", - False, - ), - ( - "notice", - "severity=NOTICE", - "pubsub.googleapis.com/projects/myproject/topics/mytopic", - True, - ), - ("warning", "severity=WARNING", "storage.googleapis.com/mybucket", False), - ] - - bindings = [r for r in resources if "member" in r["type"]] - values = [(r["index"], r["type"], r["values"]["role"]) for r in bindings] - assert sorted(values) == [ - ("debug", "google_project_iam_member", "roles/logging.bucketWriter"), - ("info", "google_bigquery_dataset_iam_member", - "roles/bigquery.dataEditor"), - ("notice", "google_pubsub_topic_iam_member", "roles/pubsub.publisher"), - ("warning", "google_storage_bucket_iam_member", - "roles/storage.objectCreator"), - ] - - exclusions = [(r["index"], r["values"]["exclusions"]) for r in sinks] - assert sorted(exclusions) == [ - ( - "debug", - [ - { - "description": None, - "disabled": False, - "filter": "logName:compute", - "name": "no-compute", - }, - { - "description": None, - "disabled": False, - "filter": "logName:container", - "name": "no-container", - }, - ], - ), - ("info", []), - ("notice", []), - ("warning", []), - ] - - -def test_exclusions(plan_runner): - "Test folder-level logging exclusions." - logging_exclusions = ("{" - 'exclusion1 = "resource.type=gce_instance", ' - 'exclusion2 = "severity=NOTICE", ' - "}") - _, resources = plan_runner(logging_exclusions=logging_exclusions) - assert len(resources) == 6 - exclusions = [ - r for r in resources if r["type"] == "google_logging_project_exclusion" - ] - assert sorted([r["index"] for r in exclusions]) == [ - "exclusion1", - "exclusion2", - ] - values = [(r["index"], r["values"]["filter"]) for r in exclusions] - assert sorted(values) == [ - ("exclusion1", "resource.type=gce_instance"), - ("exclusion2", "severity=NOTICE"), - ] diff --git a/tests/modules/project/test_plan_org_policies.py b/tests/modules/project/test_plan_org_policies.py index 8463761e8..fef2a8aaf 100644 --- a/tests/modules/project/test_plan_org_policies.py +++ b/tests/modules/project/test_plan_org_policies.py @@ -12,33 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .validate_policies import validate_policy_boolean, validate_policy_list +import pytest + +_params = ['boolean', 'list'] -def test_policy_boolean(plan_runner): - "Test boolean org policy." - tfvars = 'test.orgpolicies-boolean.tfvars' - _, resources = plan_runner(tf_var_file=tfvars) - validate_policy_boolean(resources) - - -def test_policy_list(plan_runner): - "Test list org policy." - tfvars = 'test.orgpolicies-list.tfvars' - _, resources = plan_runner(tf_var_file=tfvars) - validate_policy_list(resources) - - -def test_factory_policy_boolean(plan_runner, tfvars_to_yaml, tmp_path): +@pytest.mark.parametrize('policy_type', _params) +def test_policy_factory(plan_summary, tfvars_to_yaml, tmp_path, policy_type): dest = tmp_path / 'policies.yaml' - tfvars_to_yaml('fixture/test.orgpolicies-boolean.tfvars', dest, - 'org_policies') - _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') - validate_policy_boolean(resources) - - -def test_factory_policy_list(plan_runner, tfvars_to_yaml, tmp_path): - dest = tmp_path / 'policies.yaml' - tfvars_to_yaml('fixture/test.orgpolicies-list.tfvars', dest, 'org_policies') - _, resources = plan_runner(org_policies_data_path=f'"{tmp_path}"') - validate_policy_list(resources) + tfvars_to_yaml(f'org_policies_{policy_type}.tfvars', dest, 'org_policies') + tfvars_plan = plan_summary( + 'modules/project', + tf_var_files=['common.tfvars', f'org_policies_{policy_type}.tfvars']) + yaml_plan = plan_summary('modules/project', tf_var_files=['common.tfvars'], + org_policies_data_path=f'{tmp_path}') + assert tfvars_plan.values == yaml_plan.values diff --git a/tests/modules/project/test_plan_svpc.py b/tests/modules/project/test_plan_svpc.py deleted file mode 100644 index bd22131d9..000000000 --- a/tests/modules/project/test_plan_svpc.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -def test_svpc(_plan_runner): - "Test Shared VPC service project attachment." - fixture_path = os.path.join(os.path.dirname(__file__), 'fixture') - plan = _plan_runner(fixture_path=fixture_path, _test_service_project='true') - modules = [m for m in plan.root_module['child_modules']] - resources = [r for r in modules[0]['resources'] if r['address'] == 'module.test.google_compute_shared_vpc_host_project.shared_vpc_host[0]'] - assert len(resources) == 1 - print(modules[1]['resources']) - resources = [r for r in modules[1]['resources'] if r['address'] == 'module.test-svpc-service[0].google_compute_shared_vpc_service_project.shared_vpc_service[0]'] - assert len(resources) == 1 diff --git a/tests/blueprints/factories/bigquery_factory/test_plan.py b/tests/modules/project/tftest.yaml similarity index 73% rename from tests/blueprints/factories/bigquery_factory/test_plan.py rename to tests/modules/project/tftest.yaml index 74705e423..2fda31b7c 100644 --- a/tests/blueprints/factories/bigquery_factory/test_plan.py +++ b/tests/modules/project/tftest.yaml @@ -12,8 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -def test_resources(e2e_plan_runner): - "Test that plan works and the numbers of resources is as expected." - modules, resources = e2e_plan_runner() - assert len(modules) > 0 - assert len(resources) > 0 +module: modules/project + +common_tfvars: + - common.tfvars + +tests: + prefix: + no_prefix: + parent_folder: + parent_org: + no_parent: + service_encryption_keys: + org_policies_list: + org_policies_boolean: diff --git a/tests/modules/project/validate_policies.py b/tests/modules/project/validate_policies.py deleted file mode 100644 index 0fd038371..000000000 --- a/tests/modules/project/validate_policies.py +++ /dev/null @@ -1,160 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -def validate_policy_boolean(resources): - assert len(resources) == 6 - policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] - assert len(policies) == 2 - assert all(x['values']['parent'] == 'projects/my-project' for x in policies) - - p1 = [ - r['values']['spec'][0] - for r in policies - if r['index'] == 'iam.disableServiceAccountKeyCreation' - ][0] - - assert p1['inherit_from_parent'] is None - assert p1['reset'] is None - assert p1['rules'] == [{ - 'allow_all': None, - 'condition': [], - 'deny_all': None, - 'enforce': 'TRUE', - 'values': [] - }] - - p2 = [ - r['values']['spec'][0] - for r in policies - if r['index'] == 'iam.disableServiceAccountKeyUpload' - ][0] - - assert p2['inherit_from_parent'] is None - assert p2['reset'] is None - assert len(p2['rules']) == 2 - assert p2['rules'][0] == { - 'allow_all': None, - 'condition': [], - 'deny_all': None, - 'enforce': 'FALSE', - 'values': [] - } - assert p2['rules'][1] == { - 'allow_all': None, - 'condition': [{ - 'description': 'test condition', - 'expression': 'resource.matchTagId(aa, bb)', - 'location': 'xxx', - 'title': 'condition' - }], - 'deny_all': None, - 'enforce': 'TRUE', - 'values': [] - } - - -def validate_policy_list(resources): - assert len(resources) == 7 - - policies = [r for r in resources if r['type'] == 'google_org_policy_policy'] - assert len(policies) == 3 - assert all(x['values']['parent'] == 'projects/my-project' for x in policies) - - p1 = [ - r['values']['spec'][0] - for r in policies - if r['index'] == 'compute.vmExternalIpAccess' - ][0] - assert p1['inherit_from_parent'] is None - assert p1['reset'] is None - assert p1['rules'] == [{ - 'allow_all': None, - 'condition': [], - 'deny_all': 'TRUE', - 'enforce': None, - 'values': [] - }] - - p2 = [ - r['values']['spec'][0] - for r in policies - if r['index'] == 'iam.allowedPolicyMemberDomains' - ][0] - assert p2['inherit_from_parent'] is None - assert p2['reset'] is None - assert p2['rules'] == [{ - 'allow_all': - None, - 'condition': [], - 'deny_all': - None, - 'enforce': - None, - 'values': [{ - 'allowed_values': [ - 'C0xxxxxxx', - 'C0yyyyyyy', - ], - 'denied_values': None - }] - }] - - p3 = [ - r['values']['spec'][0] - for r in policies - if r['index'] == 'compute.restrictLoadBalancerCreationForTypes' - ][0] - assert p3['inherit_from_parent'] is None - assert p3['reset'] is None - assert len(p3['rules']) == 3 - assert p3['rules'][0] == { - 'allow_all': None, - 'condition': [], - 'deny_all': None, - 'enforce': None, - 'values': [{ - 'allowed_values': None, - 'denied_values': ['in:EXTERNAL'] - }] - } - - assert p3['rules'][1] == { - 'allow_all': None, - 'condition': [{ - 'description': 'test condition', - 'expression': 'resource.matchTagId(aa, bb)', - 'location': 'xxx', - 'title': 'condition' - }], - 'deny_all': None, - 'enforce': None, - 'values': [{ - 'allowed_values': ['EXTERNAL_1'], - 'denied_values': None - }] - } - - assert p3['rules'][2] == { - 'allow_all': 'TRUE', - 'condition': [{ - 'description': 'test condition2', - 'expression': 'resource.matchTagId(cc, dd)', - 'location': 'xxx', - 'title': 'condition2' - }], - 'deny_all': None, - 'enforce': None, - 'values': [] - } diff --git a/tests/requirements.txt b/tests/requirements.txt index a6f82d750..1e0921c19 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,6 +1,6 @@ -pytest>=6.2.5 +pytest>=7.2.1 PyYAML>=6.0 tftest>=1.8.1 -marko>=1.2.0 -deepdiff>=5.7.0 -python-hcl2>=3.0.5 +marko>=1.2.2 +deepdiff>=6.2.3 +python-hcl2>=4.3.0 diff --git a/tools/check_documentation.py b/tools/check_documentation.py index 30e765718..47643493b 100755 --- a/tools/check_documentation.py +++ b/tools/check_documentation.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -128,14 +128,14 @@ def _check_dir(dir_name, exclude_files=None, files=False, show_extra=False): elif nc := [v.name for v in newvars if not v.description.endswith('.')]: state = state.FAIL_VARIABLE_PERIOD diff = "\n".join([ - f'----- {mod_name} variables missing colons -----', + f'----- {mod_name} variable descriptions missing ending period -----', ', '.join(nc), ]) elif nc := [o.name for o in newouts if not o.description.endswith('.')]: state = state.FAIL_VARIABLE_PERIOD diff = "\n".join([ - f'----- {mod_name} outputs missing colons -----', + f'----- {mod_name} output descriptions missing ending period -----', ', '.join(nc), ]) diff --git a/tools/check_links.py b/tools/check_links.py index 77dc61739..1e2759dfb 100755 --- a/tools/check_links.py +++ b/tools/check_links.py @@ -86,7 +86,7 @@ def main(dirs, external): state = '✓' if all(l.valid for l in doc.links) else '✗' print(f'[{state}] {doc.relpath} ({len(doc.links)})') if state == '✗': - error = [f'{dir_name}{doc.relpath}'] + error = [f'{dir_name}/{doc.relpath}'] for l in doc.links: if not l.valid: error.append(f' - {l.dest}') diff --git a/tools/plan_summary.py b/tools/plan_summary.py index def79adb4..ae52c86cf 100755 --- a/tools/plan_summary.py +++ b/tools/plan_summary.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2022 Google LLC +# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,29 +15,48 @@ # limitations under the License. import click +import os import sys +import tempfile import yaml from pathlib import Path -BASEDIR = Path(__file__).parents[1] -sys.path.append(str(BASEDIR / 'tests')) - -import fixtures +try: + import fixtures +except ImportError: + BASEDIR = Path(__file__).parents[1] + sys.path.append(str(BASEDIR / 'tests')) + import fixtures @click.command() +@click.option('--example', default=False, is_flag=True) @click.argument('module', type=click.Path(), nargs=1) @click.argument('tfvars', type=click.Path(exists=True), nargs=-1) -def main(module, tfvars): - module = BASEDIR / module - summary = fixtures.plan_summary(module, Path(), tfvars) - print(yaml.dump({'values': summary.values})) - print(yaml.dump({'counts': summary.counts})) - outputs = { - k: v.get('value', '__missing__') for k, v in summary.outputs.items() - } - print(yaml.dump({'outputs': outputs})) +def main(example, module, tfvars): + try: + if example: + tmp_dir = tempfile.TemporaryDirectory() + tmp_path = Path(tmp_dir.name) + common_vars = BASEDIR / 'tests' / 'examples' / 'variables.tf' + (tmp_path / 'main.tf').symlink_to(module) + (tmp_path / 'variables.tf').symlink_to(common_vars) + (tmp_path / 'fabric').symlink_to(BASEDIR) + module = tmp_path + else: + module = BASEDIR / module + + summary = fixtures.plan_summary(module, Path(), tfvars) + print(yaml.dump({'values': summary.values})) + print(yaml.dump({'counts': summary.counts})) + outputs = { + k: v.get('value', '__missing__') for k, v in summary.outputs.items() + } + print(yaml.dump({'outputs': outputs})) + finally: + if example: + tmp_dir.cleanup() if __name__ == '__main__':