diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 0000000000..eb6dcfa314
--- /dev/null
+++ b/.github/labeler.yml
@@ -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.
+
+'on:blueprints':
+ - any:
+ - 'blueprints/**'
+ - '!**/*.md'
+
+'on:documentation':
+ - any:
+ - '**/*.md'
+ - '!**/README.md'
+
+ - 'README.md'
+ - 'blueprints/README.md'
+ - 'blueprints/*/README.md'
+ - 'modules/README.md'
+ - 'fast/README.md'
+ - 'fast/stages/README.md'
+
+'on:FAST':
+ - any:
+ - 'fast/**'
+ - '!**/*.md'
+
+'on:modules':
+ - any:
+ - 'modules/**'
+ - '!**/*.md'
+
+'on:tools':
+ - 'tools/**'
+ - '.github/**'
diff --git a/.github/workflows/container-image-squid.yml b/.github/workflows/container-image-squid.yml
new file mode 100644
index 0000000000..f6b0050270
--- /dev/null
+++ b/.github/workflows/container-image-squid.yml
@@ -0,0 +1,30 @@
+# 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: "Build and push the Squid container image"
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - master
+ paths:
+ - 'modules/cloud-config-container/squid/docker/**'
+
+jobs:
+ build-push-squid-container-image:
+ uses: ./.github/workflows/container-image.yml
+ with:
+ image_name: fabric-squid
+ docker_context: modules/cloud-config-container/squid/docker
diff --git a/.github/workflows/container-image-strongswan.yml b/.github/workflows/container-image-strongswan.yml
new file mode 100644
index 0000000000..1dc5d0eaaa
--- /dev/null
+++ b/.github/workflows/container-image-strongswan.yml
@@ -0,0 +1,30 @@
+# 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: "Build and push the strongSwan container image"
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - master
+ paths:
+ - 'modules/cloud-config-container/onprem/docker-images/strongswan/**'
+
+jobs:
+ build-push-strongswan-container-image:
+ uses: ./.github/workflows/container-image.yml
+ with:
+ image_name: fabric-strongswan
+ docker_context: modules/cloud-config-container/onprem/docker-images/strongswan
diff --git a/.github/workflows/container-image-toolbox.yml b/.github/workflows/container-image-toolbox.yml
new file mode 100644
index 0000000000..37870fce36
--- /dev/null
+++ b/.github/workflows/container-image-toolbox.yml
@@ -0,0 +1,30 @@
+# 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: "Build and push the Toolbox container image"
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - master
+ paths:
+ - 'modules/cloud-config-container/onprem/docker-images/toolbox/**'
+
+jobs:
+ build-push-toolbox-container-image:
+ uses: ./.github/workflows/container-image.yml
+ with:
+ image_name: fabric-toolbox
+ docker_context: modules/cloud-config-container/onprem/docker-images/toolbox
diff --git a/.github/workflows/container-image.yml b/.github/workflows/container-image.yml
new file mode 100644
index 0000000000..0485402525
--- /dev/null
+++ b/.github/workflows/container-image.yml
@@ -0,0 +1,66 @@
+# 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: "Build and push a generic container image"
+
+on:
+ workflow_call:
+ inputs:
+ image_name:
+ required: true
+ type: string
+ docker_context:
+ required: true
+ type: string
+
+permissions:
+ packages: write
+
+env:
+ REGISTRY: ghcr.io
+
+jobs:
+ build-push-generic-container-image:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set image version
+ run: echo IMAGE_VERSION=$(date +'%Y%m%d') >> $GITHUB_ENV
+
+ - name: Normalise image name
+ run: echo IMAGE_NAME=$(echo '${{ github.repository_owner }}/${{ inputs.image_name }}' | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
+
+ - name: Login to GHCR
+ uses: docker/login-action@v2
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.repository_owner }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build and push
+ uses: docker/build-push-action@v3
+ with:
+ context: ${{ inputs.docker_context }}
+ push: true
+ tags: |
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_VERSION }}
+ labels: |
+ org.opencontainers.image.licenses=Apache-2.0
+ org.opencontainers.image.revision=${{ github.sha }}
+ org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
+ org.opencontainers.image.title=${{ inputs.image_name }}
+ org.opencontainers.image.vendor=Google LLC
+ org.opencontainers.image.version=${{ env.IMAGE_VERSION }}
diff --git a/.github/workflows/fast-tests.yml b/.github/workflows/fast-tests.yml
deleted file mode 100644
index f85247662e..0000000000
--- a/.github/workflows/fast-tests.yml
+++ /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.
-
-name: "FAST Tests"
-on:
- pull_request:
- branches:
- - fast-dev
- - fast-dev-gke
- - master
- # paths:
- # - 'modules/**'
- # - 'fast/stages/**'
- # - 'tests/fast/**'
- tags:
- - ci
- - test
-
-env:
- TF_PLUGIN_CACHE_DIR: "/home/runner/.terraform.d/plugin-cache"
- GOOGLE_APPLICATION_CREDENTIALS: "/home/runner/credentials.json"
- PYTEST_ADDOPTS: "--color=yes"
-
-jobs:
- all-fast-tests:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
-
- - name: Config auth
- run: |
- echo '{"type": "service_account", "project_id": "test-only"}' \
- | tee -a $GOOGLE_APPLICATION_CREDENTIALS
-
- - name: Set up Python
- uses: actions/setup-python@v2
- with:
- python-version: "3.9"
-
- - name: Run tests on FAST stages
- run: |
- mkdir -p ${{ env.TF_PLUGIN_CACHE_DIR }}
- pip install -r tests/requirements.txt
- pytest -vv tests/fast
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
new file mode 100644
index 0000000000..c68c4dd303
--- /dev/null
+++ b/.github/workflows/labeler.yml
@@ -0,0 +1,30 @@
+# 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: "Label Pull Requests"
+
+on:
+ pull_request_target:
+
+jobs:
+ triage:
+ permissions:
+ contents: read
+ pull-requests: write
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/labeler@v4
+ with:
+ repo-token: "${{ secrets.GITHUB_TOKEN }}"
+ sync-labels: true
diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml
index 26a7b65215..6d773f9bd6 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
@@ -32,12 +30,12 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
- python-version: "3.9"
+ python-version: "3.10"
- name: Set up Terraform
uses: hashicorp/setup-terraform@v1
with:
- terraform_version: 1.0.9
+ terraform_version: 1.3.2
- name: Install dependencies
run: |
@@ -53,12 +51,12 @@ jobs:
run: |
terraform fmt -recursive -check -diff $GITHUB_WORKSPACE
- - name: Check documentation (fabric)
+ - name: Check documentation
id: documentation-fabric
run: |
- python3 tools/check_documentation.py examples modules fast
+ python3 tools/check_documentation.py modules fast blueprints
- - name: Check documentation links (fabric)
+ - name: Check documentation links
id: documentation-links-fabric
run: |
python3 tools/check_links.py .
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000000..494aa6b5dd
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,65 @@
+# 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: |
+ Create a new release
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Release version"
+ required: true
+ changelog:
+ description: "I have updated the CHANGELOG"
+ required: true
+ type: boolean
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ name: "Release new version"
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: "Validate input"
+ run: |
+ [[ "${{ github.event.inputs.changelog }}" != "true" ]] && { echo 'You didn''t update the changelog.' ; exit 1; }
+ [[ -n "${{ github.event.inputs.version }}" ]] || { echo 'Version not specified!'; exit 1; }
+ [[ "${{ github.event.inputs.version }}" != v* ]] && { echo 'Version does not start with v!' ; exit 1; }
+
+ - uses: actions/setup-go@v3
+ with:
+ go-version: "1.16"
+
+ - name: "Update all module names"
+ run: |
+ cd tools/tfeditor
+ go build .
+ ./tfeditor -path ../.. -module-name "google-pso-tool/cloud-foundation-fabric/{{ .Module }}/${{ github.event.inputs.version }}"
+ cd ../..
+
+ git config --global user.name "Release Automation"
+ git config --global user.email "cloud-foundation-fabric@google.com"
+
+ git commit -a -m "Release version ${{ github.event.inputs.version }}"
+ git push origin master
+
+ - name: "Tag and release"
+ run: |
+ git tag ${{ github.event.inputs.version }}
+ git push origin ${{ github.event.inputs.version }}
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 5375865f12..3e452275b0 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -14,24 +14,24 @@
name: "Tests"
on:
- schedule:
- - cron: "45 2 * * *"
+ # schedule:
+ # - cron: "45 2 * * *"
pull_request:
branches:
- - fast-dev
- - fast-dev-gke
- master
tags:
- ci
- test
env:
- TF_PLUGIN_CACHE_DIR: "/home/runner/.terraform.d/plugin-cache"
GOOGLE_APPLICATION_CREDENTIALS: "/home/runner/credentials.json"
PYTEST_ADDOPTS: "--color=yes"
+ PYTHON_VERSION: "3.10"
+ TF_PLUGIN_CACHE_DIR: "/home/runner/.terraform.d/plugin-cache"
+ TF_VERSION: 1.3.2
jobs:
- doc-examples:
+ examples:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@@ -44,15 +44,29 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
- python-version: "3.9"
+ python-version: ${{ env.PYTHON_VERSION }}
+
+ - name: Set up Terraform
+ uses: hashicorp/setup-terraform@v2
+ with:
+ terraform_version: ${{ env.TF_VERSION }}
+ terraform_wrapper: false
+
+ # avoid conflicts with user-installed providers on local machines
+ - name: Pin provider versions
+ run: |
+ for f in $(find . -name versions.tf); do
+ sed -i 's/>=\(.*# tftest\)/=\1/g' $f;
+ done
- name: Run tests on documentation examples
+ id: pytest
run: |
mkdir -p ${{ env.TF_PLUGIN_CACHE_DIR }}
pip install -r tests/requirements.txt
- pytest -vv tests/doc_examples
+ pytest -vv tests/examples
- examples:
+ blueprints:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@@ -65,19 +79,27 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
- python-version: "3.9"
+ python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Terraform
- uses: hashicorp/setup-terraform@v1
+ uses: hashicorp/setup-terraform@v2
with:
- terraform_version: 1.1.4
+ terraform_version: ${{ env.TF_VERSION }}
terraform_wrapper: false
+ # avoid conflicts with user-installed providers on local machines
+ - name: Pin provider versions
+ run: |
+ for f in $(find . -name versions.tf); do
+ sed -i 's/>=\(.*# tftest\)/=\1/g' $f;
+ done
+
- name: Run tests environments
+ id: pytest
run: |
mkdir -p ${{ env.TF_PLUGIN_CACHE_DIR }}
pip install -r tests/requirements.txt
- pytest -vv tests/examples
+ pytest -vv tests/blueprints
modules:
runs-on: ubuntu-latest
@@ -92,16 +114,59 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
- python-version: "3.9"
+ python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Terraform
- uses: hashicorp/setup-terraform@v1
+ uses: hashicorp/setup-terraform@v2
with:
- terraform_version: 1.1.4
+ terraform_version: ${{ env.TF_VERSION }}
terraform_wrapper: false
+ # avoid conflicts with user-installed providers on local machines
+ - name: Pin provider versions
+ run: |
+ for f in $(find . -name versions.tf); do
+ sed -i 's/>=\(.*# tftest\)/=\1/g' $f;
+ done
+
- name: Run tests modules
+ id: pytest
run: |
mkdir -p ${{ env.TF_PLUGIN_CACHE_DIR }}
pip install -r tests/requirements.txt
pytest -vv tests/modules
+
+ fast:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Config auth
+ run: |
+ echo '{"type": "service_account", "project_id": "test-only"}' \
+ | tee -a $GOOGLE_APPLICATION_CREDENTIALS
+
+ - name: Set up Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ env.PYTHON_VERSION }}
+
+ - name: Set up Terraform
+ uses: hashicorp/setup-terraform@v2
+ with:
+ terraform_version: ${{ env.TF_VERSION }}
+ terraform_wrapper: false
+
+ # avoid conflicts with user-installed providers on local machines
+ - name: Pin provider versions
+ run: |
+ for f in $(find . -name versions.tf); do
+ sed -i 's/>=\(.*# tftest\)/=\1/g' $f;
+ done
+
+ - name: Run tests on FAST stages
+ id: pytest
+ run: |
+ mkdir -p ${{ env.TF_PLUGIN_CACHE_DIR }}
+ pip install -r tests/requirements.txt
+ pytest -vv tests/fast
diff --git a/.gitignore b/.gitignore
index 816a11090e..79fa83df50 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,9 +21,31 @@ bundle.zip
**/*.pkrvars.hcl
fixture_*
fast/configs
-fast/stages/**/*providers.tf
+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*
**/node_modules
+fast/stages/**/globals.auto.tfvars.json
+cloud_sql_proxy
+examples/cloud-operations/binauthz/tenant-setup.yaml
+examples/cloud-operations/binauthz/app/app.yaml
+env/
+examples/cloud-operations/adfs/ansible/vars/vars.yaml
+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/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 ac0af7753e..4f4763f223 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,18 +1,440 @@
# Changelog
All notable changes to this project will be documented in this file.
+
+
+## [Unreleased]
+
+
+### BLUEPRINTS
+
+- [[#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
+
+- [[#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
+
+- [[#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
+
+- [[#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
+
+- [[#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]))
+- [[#1034](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1034)] feat(blueprints): get audience from tfc environment variable ([Thomgrus](https://github.com/Thomgrus))
+- [[#1024](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1024)] Fix Apigee PAYG environment node config ([g-greatdevaks](https://github.com/g-greatdevaks))
+- [[#1019](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1019)] Added endpoint attachments to Apigee module ([apichick](https://github.com/apichick))
+- [[#1000](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1000)] ADFS blueprint fixes ([apichick](https://github.com/apichick))
+- [[#1001](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1001)] Binauthz blueprint fixes related to project creation ([apichick](https://github.com/apichick))
+- [[#1009](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1009)] Fix encryption in Data Playground blueprint ([lcaggio](https://github.com/lcaggio))
+- [[#1003](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1003)] Normalize prefix handling in blueprints ([kunzese](https://github.com/kunzese))
+- [[#995](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/995)] Push container images to GitHub instead of Google Container Registry ([kunzese](https://github.com/kunzese))
+- [[#984](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/984)] **incompatible change:** Apigee module and blueprint ([apichick](https://github.com/apichick))
+- [[#980](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/980)] Have Squid log to /dev/stdout to stream logs to Cloud Logging ([kunzese](https://github.com/kunzese))
+- [[#929](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/929)] Updated list of enabled APIs for network dashboard ([maunope](https://github.com/maunope))
+- [[#968](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/968)] Enforce PROXY protocol in `filtering-proxy-psc` blueprint ([kunzese](https://github.com/kunzese))
+- [[#962](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/962)] Add filtering-proxy-psc blueprint ([kunzese](https://github.com/kunzese))
+- [[#913](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/913)] Adding support for PSA ranges, starting with Redis instances. ([aurelienlegrand](https://github.com/aurelienlegrand))
+- [[#952](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/952)] Remove duplicate GLB+CA blueprint folder ([ludoo](https://github.com/ludoo))
+- [[#949](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/949)] **incompatible change:** Refactor VPC firewall module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#945](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/945)] Org policy factory ([juliocc](https://github.com/juliocc))
+- [[#941](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/941)] **incompatible change:** Refactor ILB module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#939](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/939)] Temporarily duplicate cloud armor example ([ludoo](https://github.com/ludoo))
+- [[#936](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/936)] Enable org policy service and add README notice to modules ([ludoo](https://github.com/ludoo))
+- [[#931](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/931)] **incompatible change:** Refactor compute-mig module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#932](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/932)] feat(project-factory): introduce additive iam bindings to project-fac… ([Malet](https://github.com/Malet))
+- [[#925](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/925)] Network dashboard: update main.tf and README following #922 ([brianhmj](https://github.com/brianhmj))
+- [[#924](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/924)] Fix formatting for gcloud dataflow job launch command ([aymanfarhat](https://github.com/aymanfarhat))
+- [[#921](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/921)] Align documentation, move glb blueprint ([ludoo](https://github.com/ludoo))
+- [[#915](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/915)] TFE OIDC with GCP WIF blueprint added ([averbuks](https://github.com/averbuks))
+- [[#899](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/899)] Static routes monitoring metrics added to network dashboard BP ([maunope](https://github.com/maunope))
+- [[#909](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/909)] GCS2BQ: Move images and templates in sub-folders ([lcaggio](https://github.com/lcaggio))
+- [[#907](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/907)] Fix CloudSQL blueprint ([lcaggio](https://github.com/lcaggio))
+- [[#897](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/897)] Project-factory: allow folder_id to be defined in defaults_file ([Malet](https://github.com/Malet))
+- [[#900](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/900)] Improve net dashboard variables ([juliocc](https://github.com/juliocc))
+- [[#896](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/896)] Network Dashboard: CFv2 and performance improvements ([aurelienlegrand](https://github.com/aurelienlegrand))
+- [[#871](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/871)] Firewall Policy Metrics, parallel writes, aligned timestamps ([maunope](https://github.com/maunope))
+- [[#884](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/884)] BigQuery factory blueprint ([marcjwo](https://github.com/marcjwo))
+- [[#889](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/889)] Minor fixes to PSC hybrid blueprint readmes ([LucaPrete](https://github.com/LucaPrete))
+- [[#888](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/888)] Let the cloudsql module generate a random password ([skalolazka](https://github.com/skalolazka))
+- [[#879](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/879)] New PSC hybrid blueprint ([LucaPrete](https://github.com/LucaPrete))
+- [[#880](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/880)] **incompatible change:** Refactor net-vpc module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#872](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/872)] added support 2nd generation cloud function ([som-nitjsr](https://github.com/som-nitjsr))
+- [[#875](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/875)] **incompatible change:** Refactor GKE nodepool for Terraform 1.3, refactor GKE blueprints and FAST stage ([ludoo](https://github.com/ludoo))
+- [[#873](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/873)] Fix docker tag command and link to Cloud Shell in WP blueprint ([skalolazka](https://github.com/skalolazka))
+- [[#870](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/870)] Temporarily revert to Terraform 1.3.1 to support Cloud Shell ([skalolazka](https://github.com/skalolazka))
+- [[#856](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/856)] Add network firewall metrics to network dashboard ([maunope](https://github.com/maunope))
+- [[#868](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/868)] **incompatible change:** Refactor GKE module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#818](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/818)] Example wordpress ([skalolazka](https://github.com/skalolazka))
+- [[#861](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/861)] Leverage new shared VPC project config defaults across the repo ([juliocc](https://github.com/juliocc))
+- [[#854](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/854)] Added an example of a Nginx reverse proxy cluster using RMIGs ([rosmo](https://github.com/rosmo))
+- [[#850](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/850)] Made sample alert creation optional ([maunope](https://github.com/maunope))
+- [[#837](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/837)] Network dashboard: Subnet IP utilization update ([aurelienlegrand](https://github.com/aurelienlegrand))
+- [[#848](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/848)] updated quota monitoring CF doc ([maunope](https://github.com/maunope))
+- [[#847](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/847)] **incompatible change:** Quotas monitoring, time series format update ([maunope](https://github.com/maunope))
+- [[#839](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/839)] **incompatible change:** Update to terraform 1.3 ([juliocc](https://github.com/juliocc))
+- [[#828](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/828)] Update firewall rules. ([lcaggio](https://github.com/lcaggio))
+- [[#813](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/813)] Add documentation example test for pf ([ludoo](https://github.com/ludoo))
+- [[#809](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/809)] Renaming and moving blueprints ([juliocc](https://github.com/juliocc))
+
+### 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))
+- [[#1006](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1006)] Add settings for autoscaling to Bigtable module. ([iht](https://github.com/iht))
+- [[#1007](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1007)] fast README, one line fix: 00-cicd stage got moved to extras/ ([skalolazka](https://github.com/skalolazka))
+- [[#1003](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1003)] Normalize prefix handling in blueprints ([kunzese](https://github.com/kunzese))
+- [[#987](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/987)] Add tests to factory examples ([juliocc](https://github.com/juliocc))
+- [[#972](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/972)] Add note about TF_PLUGIN_CACHE_DIR ([wiktorn](https://github.com/wiktorn))
+- [[#961](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/961)] Remove extra file from root ([ludoo](https://github.com/ludoo))
+- [[#943](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/943)] Update bootstrap README.md with unique project id requirements ([KPRepos](https://github.com/KPRepos))
+- [[#937](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/937)] Fix typos in blueprints README.md ([kumar-dhanagopal](https://github.com/kumar-dhanagopal))
+- [[#921](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/921)] Align documentation, move glb blueprint ([ludoo](https://github.com/ludoo))
+- [[#898](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/898)] Update FAST bootstrap README.md ([juliocc](https://github.com/juliocc))
+- [[#878](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/878)] chore: update cft and fabric ([bharathkkb](https://github.com/bharathkkb))
+- [[#863](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/863)] Fabric vs CFT doc ([ludoo](https://github.com/ludoo))
+- [[#806](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/806)] FAST Companion Guide ([ajlopezn](https://github.com/ajlopezn))
+
+### FAST
+
+- [[#1023](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1023)] **incompatible change:** Small fix: uniform region in Fast in networking-nva ([skalolazka](https://github.com/skalolazka))
+- [[#1032](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1032)] FAST: fix VPC-SC example in security documentation ([imp14a](https://github.com/imp14a))
+- [[#1007](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1007)] fast README, one line fix: 00-cicd stage got moved to extras/ ([skalolazka](https://github.com/skalolazka))
+- [[#976](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/976)] FAST: fixes to GitHub workflow and 02/net outputs ([ludoo](https://github.com/ludoo))
+- [[#966](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/966)] FAST: improve GitHub workflow, stage 01 output fixes ([ludoo](https://github.com/ludoo))
+- [[#963](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/963)] **incompatible change:** Refactor vps-sc module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#956](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/956)] FAST: bootstrap and extra stage CI/CD improvements and fixes ([ludoo](https://github.com/ludoo))
+- [[#949](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/949)] **incompatible change:** Refactor VPC firewall module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#943](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/943)] Update bootstrap README.md with unique project id requirements ([KPRepos](https://github.com/KPRepos))
+- [[#948](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/948)] Use display_name instead of description for FAST service accounts ([juliocc](https://github.com/juliocc))
+- [[#947](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/947)] Use org policy factory for resman stage ([juliocc](https://github.com/juliocc))
+- [[#941](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/941)] **incompatible change:** Refactor ILB module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#935](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/935)] FAST: enable org policy API, fix run.allowedIngress value ([ludoo](https://github.com/ludoo))
+- [[#931](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/931)] **incompatible change:** Refactor compute-mig module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#930](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/930)] **incompatible change:** Update organization/folder/project modules to use new org policies API and tf1.3 optionals ([juliocc](https://github.com/juliocc))
+- [[#911](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/911)] FAST: Additional PGA DNS records ([sruffilli](https://github.com/sruffilli))
+- [[#903](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/903)] Initial replacement for CI/CD stage ([ludoo](https://github.com/ludoo))
+- [[#898](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/898)] Update FAST bootstrap README.md ([juliocc](https://github.com/juliocc))
+- [[#880](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/880)] **incompatible change:** Refactor net-vpc module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#875](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/875)] **incompatible change:** Refactor GKE nodepool for Terraform 1.3, refactor GKE blueprints and FAST stage ([ludoo](https://github.com/ludoo))
+- [[#566](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/566)] FAST: Separate network environment ([sruffilli](https://github.com/sruffilli))
+- [[#870](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/870)] Temporarily revert to Terraform 1.3.1 to support Cloud Shell ([skalolazka](https://github.com/skalolazka))
+- [[#868](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/868)] **incompatible change:** Refactor GKE module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#867](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/867)] FAST: Replace NVAs in 02-networking-nva with COS-based VMs ([sruffilli](https://github.com/sruffilli))
+- [[#865](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/865)] Enable FAST 00-cicd provider test ([ludoo](https://github.com/ludoo))
+- [[#861](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/861)] Leverage new shared VPC project config defaults across the repo ([juliocc](https://github.com/juliocc))
+- [[#858](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/858)] Default gcp-support to gcp-devops ([juliocc](https://github.com/juliocc))
+- [[#842](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/842)] Comment redundant role in bootstrap stage, align IAM.md files, improve IAM tool ([ludoo](https://github.com/ludoo))
+- [[#841](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/841)] FAST: revert 00-cicd provider changes ([ludoo](https://github.com/ludoo))
+- [[#835](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/835)] Fix workflow-gitlab.yaml template rendering ([muresan](https://github.com/muresan))
+- [[#828](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/828)] Update firewall rules. ([lcaggio](https://github.com/lcaggio))
+- [[#807](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/807)] FAST: refactor Gitlab template ([ludoo](https://github.com/ludoo))
+
+### 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))
+- [[#1036](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1036)] **incompatible change:** Fix status ingress/egress policies in vpc-sc module ([ludoo](https://github.com/ludoo))
+- [[#1033](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1033)] strongSwan: switch base image to debian-slim ([kunzese](https://github.com/kunzese))
+- [[#1026](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1026)] add lifecycle ignore_changes for apigee PAYG env ([g-greatdevaks](https://github.com/g-greatdevaks))
+- [[#1031](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1031)] Fix default_rules_config description in firewall module ([ludoo](https://github.com/ludoo))
+- [[#1028](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1028)] **incompatible change:** Align rest of vpn modules with #1027 ([juliocc](https://github.com/juliocc))
+- [[#1027](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1027)] **incompatible change:** Update VPN-HA module to tf1.3 ([juliocc](https://github.com/juliocc))
+- [[#1025](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1025)] fix apigee PAYG env node config dynamic block ([g-greatdevaks](https://github.com/g-greatdevaks))
+- [[#1024](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1024)] Fix Apigee PAYG environment node config ([g-greatdevaks](https://github.com/g-greatdevaks))
+- [[#1019](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1019)] Added endpoint attachments to Apigee module ([apichick](https://github.com/apichick))
+- [[#1018](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1018)] Apigee instance doc examples ([danistrebel](https://github.com/danistrebel))
+- [[#1016](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1016)] Fix memory/cpu typo in gke cluster module ([joeheaton](https://github.com/joeheaton))
+- [[#1012](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1012)] Fix tag outputs in organization module ([ludoo](https://github.com/ludoo))
+- [[#1006](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1006)] Add settings for autoscaling to Bigtable module. ([iht](https://github.com/iht))
+- [[#999](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/999)] Default nodepool creation fix ([astianseb](https://github.com/astianseb))
+- [[#1005](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1005)] Only set partitioned table when sink type is bigquery ([juliocc](https://github.com/juliocc))
+- [[#997](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/997)] Add BigQuery subcriptions to Pubsub module. ([iht](https://github.com/iht))
+- [[#995](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/995)] Push container images to GitHub instead of Google Container Registry ([kunzese](https://github.com/kunzese))
+- [[#994](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/994)] Add schemas to Pubsub topic module. ([iht](https://github.com/iht))
+- [[#979](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/979)] Add network tags support to the organization module ([LucaPrete](https://github.com/LucaPrete))
+- [[#991](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/991)] Allow cross-project backend services in ILB L7 module ([ludoo](https://github.com/ludoo))
+- [[#984](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/984)] **incompatible change:** Apigee module and blueprint ([apichick](https://github.com/apichick))
+- [[#988](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/988)] Merge cloud function v1 and v2 tests ([juliocc](https://github.com/juliocc))
+- [[#965](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/965)] **incompatible change:** Add triggers to Cloud Functions v2 ([wiktorn](https://github.com/wiktorn))
+- [[#980](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/980)] Have Squid log to /dev/stdout to stream logs to Cloud Logging ([kunzese](https://github.com/kunzese))
+- [[#983](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/983)] **incompatible change:** Add support for serverless NEGs to ILB L7 module ([ludoo](https://github.com/ludoo))
+- [[#978](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/978)] Worker pool support for `cloud-function` ([maunope](https://github.com/maunope))
+- [[#977](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/977)] Replace Docker's `gcplogs` driver with the GCP COS logging agent ([kunzese](https://github.com/kunzese))
+- [[#975](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/975)] Add validation for health check port specification to ILB L7 module ([ludoo](https://github.com/ludoo))
+- [[#974](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/974)] **incompatible change:** Refactor net-ilb-l7 module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#970](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/970)] Update logging sinks to tf1.3 in resman modules ([juliocc](https://github.com/juliocc))
+- [[#969](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/969)] Update folder and project org policy tests ([juliocc](https://github.com/juliocc))
+- [[#964](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/964)] prefix variable consistency across modules ([skalolazka](https://github.com/skalolazka))
+- [[#963](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/963)] **incompatible change:** Refactor vps-sc module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#958](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/958)] Add support for org policy custom constraints ([averbuks](https://github.com/averbuks))
+- [[#960](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/960)] Fix README typo in firewall module ([valeriobponza](https://github.com/valeriobponza))
+- [[#953](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/953)] Added IAM Additive and converted some outputs to static ([muresan](https://github.com/muresan))
+- [[#951](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/951)] cloud-functions v2 - fix reference to bucket_name ([wiktorn](https://github.com/wiktorn))
+- [[#949](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/949)] **incompatible change:** Refactor VPC firewall module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#946](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/946)] **incompatible change:** Deprecate organization-policy module ([juliocc](https://github.com/juliocc))
+- [[#945](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/945)] Org policy factory ([juliocc](https://github.com/juliocc))
+- [[#941](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/941)] **incompatible change:** Refactor ILB module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#940](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/940)] Ensure the implementation of org policies is consistent ([juliocc](https://github.com/juliocc))
+- [[#936](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/936)] Enable org policy service and add README notice to modules ([ludoo](https://github.com/ludoo))
+- [[#931](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/931)] **incompatible change:** Refactor compute-mig module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#930](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/930)] **incompatible change:** Update organization/folder/project modules to use new org policies API and tf1.3 optionals ([juliocc](https://github.com/juliocc))
+- [[#926](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/926)] Fix backwards compatibility for vpc subnet descriptions ([ludoo](https://github.com/ludoo))
+- [[#927](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/927)] Add support for deployment type and api proxy type for Apigee org ([kmucha555](https://github.com/kmucha555))
+- [[#923](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/923)] Fix service account creation error in gke nodepool module ([ludoo](https://github.com/ludoo))
+- [[#908](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/908)] GKE module: autopilot fixes ([ludoo](https://github.com/ludoo))
+- [[#906](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/906)] GKE module: add managed_prometheus to features ([apichick](https://github.com/apichick))
+- [[#916](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/916)] Add support for DNS routing policies ([juliocc](https://github.com/juliocc))
+- [[#918](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/918)] Fix race condition in SimpleNVA ([sruffilli](https://github.com/sruffilli))
+- [[#914](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/914)] **incompatible change:** Update DNS module ([juliocc](https://github.com/juliocc))
+- [[#904](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/904)] Add missing description field ([dsbutler101](https://github.com/dsbutler101))
+- [[#891](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/891)] Add internal_ips output to compute-vm module ([LucaPrete](https://github.com/LucaPrete))
+- [[#890](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/890)] Add auto_delete and instance_redistribution_type to compute-vm and compute-mig modules. ([giovannibaratta](https://github.com/giovannibaratta))
+- [[#883](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/883)] Fix csi-driver, logging and monitoring default values when autopilot … ([danielmarzini](https://github.com/danielmarzini))
+- [[#880](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/880)] **incompatible change:** Refactor net-vpc module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#872](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/872)] added support 2nd generation cloud function ([som-nitjsr](https://github.com/som-nitjsr))
+- [[#877](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/877)] fix autoscaling block ([ludoo](https://github.com/ludoo))
+- [[#875](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/875)] **incompatible change:** Refactor GKE nodepool for Terraform 1.3, refactor GKE blueprints and FAST stage ([ludoo](https://github.com/ludoo))
+- [[#870](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/870)] Temporarily revert to Terraform 1.3.1 to support Cloud Shell ([skalolazka](https://github.com/skalolazka))
+- [[#869](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/869)] Fix optionals for resource_usage_export field in `gke-cluster` ([juliocc](https://github.com/juliocc))
+- [[#868](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/868)] **incompatible change:** Refactor GKE module for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#866](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/866)] Update ipprefix_by_netmask.sh in nva module ([sruffilli](https://github.com/sruffilli))
+- [[#860](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/860)] **incompatible change:** Refactor compute-vm for Terraform 1.3 ([ludoo](https://github.com/ludoo))
+- [[#861](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/861)] Leverage new shared VPC project config defaults across the repo ([juliocc](https://github.com/juliocc))
+- [[#859](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/859)] Make project shared VPC fields optional ([juliocc](https://github.com/juliocc))
+- [[#853](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/853)] Fixes NVA issue when health checks are not enabled ([sruffilli](https://github.com/sruffilli))
+- [[#846](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/846)] COS based simple networking appliance ([sruffilli](https://github.com/sruffilli))
+- [[#851](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/851)] nginx-tls: only use hostname part for TLS certificate ([rosmo](https://github.com/rosmo))
+- [[#844](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/844)] Management of GCP project default service accounts ([ddaluka](https://github.com/ddaluka))
+- [[#845](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/845)] added root password support for MS SQL Server ([cmalpe](https://github.com/cmalpe))
+- [[#843](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/843)] Add support for disk encryption to instance templates in compute-vm module ([ludoo](https://github.com/ludoo))
+- [[#840](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/840)] **incompatible change:** Refactor net-address module for 1.3 ([ludoo](https://github.com/ludoo))
+- [[#839](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/839)] **incompatible change:** Update to terraform 1.3 ([juliocc](https://github.com/juliocc))
+- [[#824](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/824)] Add simple composer 2 blueprint ([lcaggio](https://github.com/lcaggio))
+- [[#834](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/834)] Add support for service_label property in internal load balancer ([kmucha555](https://github.com/kmucha555))
+- [[#833](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/833)] regional MySQL DBs - automatic backup conf ([skalolazka](https://github.com/skalolazka))
+- [[#827](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/827)] Project module: Add Artifactregistry Service Identity SA creation. ([lcaggio](https://github.com/lcaggio))
+- [[#826](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/826)] Added new binary_authorization argument in gke-cluster module ([sirohia](https://github.com/sirohia))
+- [[#819](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/819)] Removed old and unused modules ([juliocc](https://github.com/juliocc))
+
+### 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))
+- [[#1017](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1017)] Fix auto-labeling ([ludoo](https://github.com/ludoo))
+- [[#1013](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1013)] Update labeler.yml ([ludoo](https://github.com/ludoo))
+- [[#1010](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1010)] Enforce nonempty descriptions ending in a dot ([juliocc](https://github.com/juliocc))
+- [[#1004](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/1004)] Use `actions/labeler` to automatically label pull requests ([kunzese](https://github.com/kunzese))
+- [[#998](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/998)] Add missing `write_package` permission ([kunzese](https://github.com/kunzese))
+- [[#996](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/996)] Fix `repository name must be lowercase` on docker build ([kunzese](https://github.com/kunzese))
+- [[#993](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/993)] Fix variable and output sort check ([juliocc](https://github.com/juliocc))
+- [[#950](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/950)] Add a pytest fixture to convert tfvars to yaml ([ludoo](https://github.com/ludoo))
+- [[#942](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/942)] Bump tftest and improve dns tests ([juliocc](https://github.com/juliocc))
+- [[#919](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/919)] Rename workflow names ([juliocc](https://github.com/juliocc))
+- [[#902](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/902)] Bring back sorted variables check ([juliocc](https://github.com/juliocc))
+- [[#887](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/887)] Disable parallel execution of tests and plugin cache ([ludoo](https://github.com/ludoo))
+- [[#886](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/886)] Revert "Improve handling of tf plugin cache in tests" ([ludoo](https://github.com/ludoo))
+- [[#885](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/885)] Improve handling of tf plugin cache in tests ([ludoo](https://github.com/ludoo))
+- [[#881](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/881)] Run tests in parallel using `pytest-xdist` ([ludoo](https://github.com/ludoo))
+- [[#876](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/876)] Make changelog tool slower to work around inconsistencies in API results ([ludoo](https://github.com/ludoo))
+- [[#865](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/865)] Enable FAST 00-cicd provider test ([ludoo](https://github.com/ludoo))
+- [[#864](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/864)] **incompatible change:** Bump terraform required version ([ludoo](https://github.com/ludoo))
+- [[#842](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/842)] Comment redundant role in bootstrap stage, align IAM.md files, improve IAM tool ([ludoo](https://github.com/ludoo))
+- [[#811](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/811)] Fix changelog generator ([ludoo](https://github.com/ludoo))
+- [[#810](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/810)] Fully recursive e2e test runner for examples ([juliocc](https://github.com/juliocc))
+
+## [18.0.0] - 2022-09-09
+
+
+
+- [[#808](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/808)] Rename examples to blueprints ([juliocc](https://github.com/juliocc))
+
+### FAST
+
+- [[#804](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/804)] GKE CI/CD ([ludoo](https://github.com/ludoo))
+- [[#803](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/803)] FAST: fix GCS location in stage 00 and 01 ([miklosn](https://github.com/miklosn))
+- [[#700](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/700)] FAST: GKE multitenant infrastructure ([ludoo](https://github.com/ludoo))
+- [[#800](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/800)] FAST: add support for storage locations in stages 0 and 1 ([ludoo](https://github.com/ludoo))
+- [[#799](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/799)] FAST: add support for project parents to bootstrap stage ([ludoo](https://github.com/ludoo))
+- [[#793](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/793)] FAST: fix typo in CI/CD stage outputs. ([fawzihmouda](https://github.com/fawzihmouda))
+- [[#774](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/774)] FAST: fix data-platform-dev folder in stage 03-data-platform ([sttomm](https://github.com/sttomm))
+- [[#770](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/770)] FAST: fix to move without `output_location` ([daisuky-jp](https://github.com/daisuky-jp))
+- [[#767](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/767)] Allow interpolating SAs in project factory subnet IAM bindings ([ludoo](https://github.com/ludoo))
+- [[#766](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/766)] FAST: refactor teams branch ([ludoo](https://github.com/ludoo))
+- [[#765](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/765)] FAST: move region trigrams to a variable in network stages ([ludoo](https://github.com/ludoo))
+- [[#759](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/759)] FAST: fix missing value to format principalSet ([imp14a](https://github.com/imp14a))
+- [[#753](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/753)] Add support for IAM bindings on service accounts to project factory ([ludoo](https://github.com/ludoo))
+- [[#745](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/745)] FAST: specify gitlab / github providers in CI/CD stage ([imp14a](https://github.com/imp14a))
+- [[#734](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/734)] FAST: Use spot VMs for test VM and for NVAs ([sruffilli](https://github.com/sruffilli))
+- [[#733](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/733)] FAST: fix data platform drop BQ dataset name ([juliocc](https://github.com/juliocc))
+- [[#730](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/730)] FAST: add billing IAM for billing group ([ludoo](https://github.com/ludoo))
+- [[#721](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/721)] FAST: add billing.costManager role to project factory SAs ([sruffilli](https://github.com/sruffilli))
+- [[#716](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/716)] FAST: added missing format argument to project factory CI/CD IAM bindings ([mgfeller](https://github.com/mgfeller))
+- [[#715](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/715)] FAST: fix optional service accounts in networking stages ([ludoo](https://github.com/ludoo))
+- [[#711](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/711)] FAST: update several stage READMEs about usage of *.auto.tfvars files ([mgfeller](https://github.com/mgfeller))
+- [[#703](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/703)] FAST: configuration switches for features ([ludoo](https://github.com/ludoo))
+- [[#706](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/706)] Bump providers versions and pin versions for tests ([juliocc](https://github.com/juliocc))
+- [[#702](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/702)] FAST: also trigger GitHub workflow on PR synchronize event ([mgfeller](https://github.com/mgfeller))
+- [[#692](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/692)] FAST: fix KMS delegation role in security stage ([lcaggio](https://github.com/lcaggio))
+- [[#699](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/699)] FAST: add `repository_owner` to GitHub identity attributes ([ludoo](https://github.com/ludoo))
+- [[#694](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/694)] FAST: add 00-cicd stage to allow managing repositories in Gitlab/GitHub, other CI/CD improvements ([rosmo](https://github.com/rosmo))
+- [[#690](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/690)] FAST: fix stage tfvars link paths in documentation ([lcaggio](https://github.com/lcaggio))
+- [[#676](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/676)] FAST: add group creation GIF to documentation ([amgoogle](https://github.com/amgoogle))
+- [[#687](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/687)] FAST: fix service identity/SA mismatch in project factory ([dosti-tee](https://github.com/dosti-tee))
+- [[#668](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/668)] FAST: add cleanup instructions to documentation ([ajlopezn](https://github.com/ajlopezn))
+- [[#682](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/682)] FAST: fix CI/CD source repositories in stage 01 ([imp14a](https://github.com/imp14a))
+- [[#675](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/675)] FAST: fix audit logs when using pubsub as destination ([juliocc](https://github.com/juliocc))
+- [[#674](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/674)] FAST: remove team folders comment from 01 variables, clarify README ([ludoo](https://github.com/ludoo))
+- [[#671](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/671)] FAST: fix Gitlab WIF attributes ([ludoo](https://github.com/ludoo))
+- [[#669](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/669)] FAST: CI/CD support for Source Repository and Cloud Build ([ludoo](https://github.com/ludoo))
+
+### EXAMPLES
+
+- [[#801](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/801)] Update Cloud SQL example ([lcaggio](https://github.com/lcaggio))
+- [[#802](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/802)] Fix Data Platform example ([lcaggio](https://github.com/lcaggio))
+- [[#790](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/790)] Cloud Identity Group factory ([lcaggio](https://github.com/lcaggio))
+- [[#740](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/740)] Update to multiple READMEs ([bluPhy](https://github.com/bluPhy))
+- [[#738](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/738)] Improve Data Playground example ([lcaggio](https://github.com/lcaggio))
+- [[#771](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/771)] Example of a multi-cluster mesh on GKE configuring managed control pl… ([apichick](https://github.com/apichick))
+- [[#743](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/743)] Update Readme.md: gcs to bq + cloud armor / glb ([bensadikgoogle](https://github.com/bensadikgoogle))
+- [[#757](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/757)] Remove key_algorithm from glb/ilb-l7 examples ([ludoo](https://github.com/ludoo))
+- [[#753](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/753)] Add support for IAM bindings on service accounts to project factory ([ludoo](https://github.com/ludoo))
+- [[#746](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/746)] Update multi region cloud SQL documentation ([bensadikgoogle](https://github.com/bensadikgoogle))
+- [[#733](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/733)] FAST: fix data platform drop BQ dataset name ([juliocc](https://github.com/juliocc))
+- [[#712](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/712)] New AD FS example ([apichick](https://github.com/apichick))
+- [[#655](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/655)] New example for a data playground Terraform setup ([aymanfarhat](https://github.com/aymanfarhat))
+- [[#706](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/706)] Bump providers versions and pin versions for tests ([juliocc](https://github.com/juliocc))
+
+### MODULES
+
+- [[#805](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/805)] Change `modules/project` service_config default ([juliocc](https://github.com/juliocc))
+- [[#787](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/787)] Support manager role in cloud identity group module ([lcaggio](https://github.com/lcaggio))
+- [[#786](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/786)] Secret manager flag sensitive output ([ddaluka](https://github.com/ddaluka))
+- [[#775](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/775)] net-glb: Added support for regional external HTTP(s) load balancing ([rosmo](https://github.com/rosmo))
+- [[#784](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/784)] fix envoy-traffic-director config for xDS v3 ([drebes](https://github.com/drebes))
+- [[#785](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/785)] nginx-tls module ([drebes](https://github.com/drebes))
+- [[#783](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/783)] fix service unit indent on cloud-config-container module ([drebes](https://github.com/drebes))
+- [[#782](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/782)] typo fix (max_scale -> min_scale) ([skalolazka](https://github.com/skalolazka))
+- [[#778](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/778)] **incompatible change:** instance_termination_action must be set for compute-vm spot instances ([sruffilli](https://github.com/sruffilli))
+- [[#727](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/727)] Fix `ip_range` variable description in `apigee-x-instance` module ([alexlo03](https://github.com/alexlo03))
+- [[#773](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/773)] **incompatible change:** Refactor Cloud Run module ([ludoo](https://github.com/ludoo))
+- [[#754](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/754)] Add support to a public access to cloudsql-instance ([alefmreis](https://github.com/alefmreis))
+- [[#768](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/768)] Add egress / ingress policy example to VPC SC module ([ludoo](https://github.com/ludoo))
+- [[#767](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/767)] Allow interpolating SAs in project factory subnet IAM bindings ([ludoo](https://github.com/ludoo))
+- [[#764](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/764)] Add dependency on shared vpc service project attachment to project module outputs ([apichick](https://github.com/apichick))
+- [[#761](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/761)] Fix gke hub module features condition ([ludoo](https://github.com/ludoo))
+- [[#760](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/760)] **incompatible change:** GKE hub module refactor ([ludoo](https://github.com/ludoo))
+- [[#756](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/756)] Set cluster id output to sensitive in GKE module ([apichick](https://github.com/apichick))
+- [[#752](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/752)] Also depend on shared vpc host in project module ([apichick](https://github.com/apichick))
+- [[#747](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/747)] Added gkehub.googleapis.com to jit services ([apichick](https://github.com/apichick))
+- [[#744](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/744)] Fixed issue with missing project reference in Cloud DNS data source ([rosmo](https://github.com/rosmo))
+- [[#741](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/741)] Added servicemesh feature to GKE hub and included fleet robot service… ([apichick](https://github.com/apichick))
+- [[#737](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/737)] Move Cloud Run VPC Connector annotations to template metadata (#735) ([sethmoon](https://github.com/sethmoon))
+- [[#732](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/732)] Add support for topic message duration to pubsub module ([ludoo](https://github.com/ludoo))
+- [[#731](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/731)] Avoid setting empty IAM binding in subnet factory ([ludoo](https://github.com/ludoo))
+- [[#729](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/729)] Fix connector create logic in cloud run module ([ludoo](https://github.com/ludoo))
+- [[#726](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/726)] Fix documentation for organization-policy module ([averbuks](https://github.com/averbuks))
+- [[#722](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/722)] OrgPolicy module (factory) using new org-policy API, #698 ([averbuks](https://github.com/averbuks))
+- [[#695](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/695)] Modified reserved IP address outputs in net-glb module ([apichick](https://github.com/apichick))
+- [[#709](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/709)] Fix incompatibility between logging and monitor config/service arguments in GKE module ([psabhishekgoogle](https://github.com/psabhishekgoogle))
+- [[#708](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/708)] Fix incompatibility between backup and autopilot in GKE module ([ludoo](https://github.com/ludoo))
+- [[#707](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/707)] Fix addons for autopilot clusters and add specific tests in GKE module ([juliocc](https://github.com/juliocc))
+- [[#706](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/706)] Bump providers versions and pin versions for tests ([juliocc](https://github.com/juliocc))
+- [[#704](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/704)] Add `consumer_accept_list` to `apigee-x-instance` ([juliocc](https://github.com/juliocc))
+- [[#696](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/696)] Added missing image in GLB and Cloud Armor example ([apichick](https://github.com/apichick))
+- [[#689](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/689)] New binary authorization module and example ([apichick](https://github.com/apichick))
+- [[#686](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/686)] Revert "Binary authorization module and example" ([ludoo](https://github.com/ludoo))
+- [[#683](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/683)] Binary authorization module and example ([apichick](https://github.com/apichick))
+- [[#684](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/684)] Cloud function module: add support for secrets ([ludoo](https://github.com/ludoo))
+
+### TOOLS
+
+- [[#796](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/796)] Remove duplicate path component from doc_examples test names. ([juliocc](https://github.com/juliocc))
+- [[#794](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/794)] Test documentation examples in the `examples/` folder ([juliocc](https://github.com/juliocc))
+- [[#788](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/788)] fix yaml quotes for merge-pr workflow ([drebes](https://github.com/drebes))
+- [[#763](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/763)] Changelog generator ([ludoo](https://github.com/ludoo))
+- [[#762](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/762)] Update changelog on pull request merge ([ludoo](https://github.com/ludoo))
+- [[#680](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/680)] Tools: fix `ValueError` raised in `check_names.py` when overlong names are detected ([27Bslash6](https://github.com/27Bslash6))
+- [[#672](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/pull/672)] Module attribution and version updater tool, plus release automation ([rosmo](https://github.com/rosmo))
+
+## [16.0.0] - 2022-06-06
+
+- add support for [Spot VMs](https://cloud.google.com/compute/docs/instances/spot) to `gke-nodepool` module
+- **incompatible change** add support for [Spot VMs](https://cloud.google.com/compute/docs/instances/spot) to `compute-vm` module
+- SQL Server AlwaysOn availability groups example
+- fixed Terraform change detection in CloudSQL when backup is disabled
+- allow multiple CIDR blocks in the ip_range for Apigee Instance
+- add prefix to project factory SA bindings
+- **incompatible change** `subnets_l7ilb` variable is deprecated in the `net-vpc` module, instead `subnets_proxy_only` variable [should be used](https://cloud.google.com/load-balancing/docs/proxy-only-subnets#proxy_only_subnet_create)
+- add support for [Private Service Connect](https://cloud.google.com/vpc/docs/private-service-connect#psc-subnets) and [Proxy-only](https://cloud.google.com/load-balancing/docs/proxy-only-subnets) subnets to `net-vpc` module
+- bump Google provider versions to `>= 4.17.0`
+- bump Terraform version to `>= 1.1.0`
+- add `shielded_instance_config` support for instance template on `compute-vm` module
+- add support for `gke_backup_agent_config` to GKE module addons
+- add support for subscription filters to PubSub module
+- refactor Hub and Spoke with VPN example
+- fix tfdoc parsing on newllines in outputs
+- fix subnet factory example in vpc module README
+- fix condition in subnet factory flow logs
+- added new example on GLB and Cloud Armor
+- revamped and expanded Contributing Guide
+- add support for Workload Identity Federation and CI/CD repositories
+- simplify VPN tunnel configuration in the Hub and Spoke VPN network stage
+- fix subnet YAML schema
+
+## [15.0.0] - 2022-04-05
-## Unreleased
-
+- **incompatible change** the variable for PSA ranges in the `net-vpc` module has changed to support configuring peering routes
- fix permadiff in `net-vpc-firewall` module rules
- new [gke-hub](modules/gke-hub) module
-- new [unmanaged-instances-healthcheck](examples/cloud-operations/unmanaged-instances-healthcheck) example
+- new [unmanaged-instances-healthcheck](blueprints/cloud-operations/unmanaged-instances-healthcheck) example
- add support for IAM to `data-catalog-policy-tag` module
- add support for IAM additive to `folder` module, fixes #580
-- **incompatible change** the variable for PSA ranges in the `net-vpc` module has changed to support configuring peering routes
-
-**FAST**
-
+- optionally turn off gcplogs driver in COS modules
+- 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
- **incompatible change** the variable for PSA ranges in networking stages have changed
@@ -21,19 +443,17 @@ All notable changes to this project will be documented in this file.
- **incompatible change** removed `iam` key from logging sink configuration in the `project` and `organization` modules
- remove GCS to BQ with Dataflow example, replace by GCS to BQ with least privileges
- the `net-vpc` and `project` modules now use the beta provider for shared VPC-related resources
-- new [iot-core](modules/iot-core) module
+- new iot-core module
- **incompatible change** the variables for host and service Shared VPCs have changed in the project module
- **incompatible change** the variable for service identities IAM has changed in the project factory
- add `data-catalog-policy-tag` module
-- new [workload identity federetion example](examples/cloud-operations/workload-identity-federation)
-- new `api-gateway` [module](/modules/api-gateway) and [example](examples/serverless/api-gateway).
+- new [workload identity federetion example](blueprints/cloud-operations/workload-identity-federation)
+- new `api-gateway` [module](./modules/api-gateway) and [example](blueprints/serverless/api-gateway).
- **incompatible change** the `psn_ranges` variable has been renamed to `psa_ranges` in the `net-vpc` module and its type changed from `list(string)` to `map(string)`
- **incompatible change** removed `iam` flag for organization and folder level sinks
- **incompatible change** removed `ingress_settings` configuration option in the `cloud-functions` module.
-- new [m4ce VM example](examples/cloud-operations/vm-migration/)
+- new [m4ce VM example](blueprints/cloud-operations/vm-migration/)
- Support for resource management tags in the `organization`, `folder`, `project`, `compute-vm`, and `kms` modules
-
-**FAST**
- new [data platform](fast/stages/03-data-platform) stage 3
- new [02-networking-nva](fast/stages/02-networking-nva) networking stage
- allow customizing the names of custom roles
@@ -45,12 +465,11 @@ All notable changes to this project will be documented in this file.
- swtich to folder-level `xpnAdmin` and `xpnServiceAdmin`
- moved networking projects to folder matching their enviroments
-
## [13.0.0] - 2022-01-27
-- **initial Fabric Fast implementation**
+- **initial Fabric FAST implementation**
- new `net-glb` module for Global External Load balancer
-- new `project-factory` module in [`examples/factories`](./examples/factories)
+- new `project-factory` module in [`blueprints/factories`](./blueprints/factories)
- add missing service identity accounts (artifactregistry, composer) in project module
- new "Cloud Storage to Bigquery with Cloud Dataflow with least privileges" example
- support service dependencies for crypto key bindings in project module
@@ -82,9 +501,9 @@ All notable changes to this project will be documented in this file.
- fix cases where bridge perimeter status resources are `null` in `vpc-sc` module
- re-release 9.0.3 as a major release as it contains breaking changes
- - update hierarchical firewall resources to use the newer `google_compute_firewall_*` resources
- - **incompatible change** rename `firewall_policy_attachments` to `firewall_policy_association` in the `organization` and `folder` modules
- - **incompatible change** updated API for the `net-vpc-sc` module
+- update hierarchical firewall resources to use the newer `google_compute_firewall_*` resources
+- **incompatible change** rename `firewall_policy_attachments` to `firewall_policy_association` in the `organization` and `folder` modules
+- **incompatible change** updated API for the `net-vpc-sc` module
## [9.0.3] - 2021-12-31
@@ -102,7 +521,7 @@ All notable changes to this project will be documented in this file.
- added gVNIC support to `compute-vm` module
- added a rule factory to `net-vpc-firewall` module
- added a subnet factory to `net-vpc` module
-- **incompatible change** added support for partitioned tables to `organization` module sinks
+- **incompatible change** added support for partitioned tables to `organization` module sinks
- **incompatible change** renamed `private_service_networking_range` variable to `psc_ranges` in `net-vpc`module, and changed its type to `list(string)`
- added a firewall policy factory to `organization` and `firewall` module
- refactored `tfdoc`
@@ -116,7 +535,7 @@ All notable changes to this project will be documented in this file.
## [7.0.0] - 2021-10-21
-- new cloud operations example showing how to deploy infrastructure for [Compute Engine image builder based on Hashicorp Packer](./examples/cloud-operations/packer-image-builder)
+- new cloud operations example showing how to deploy infrastructure for [Compute Engine image builder based on Hashicorp Packer](./blueprints/cloud-operations/packer-image-builder)
- **incompatible change** the format of the `records` variable in the `dns` module has changed, to better support dynamic values
- new `naming-convention` module
- new `cloudsql-instance` module
@@ -133,16 +552,15 @@ All notable changes to this project will be documented in this file.
- **incompatible change** changed maintenance window definition from `maintenance_start_time` to `maintenance_config` in `gke-cluster`
- added `monitoring_config`,`logging_config`, `dns_config` and `enable_l4_ilb_subsetting` to `gke-cluster`
-
## [6.0.0] - 2021-10-04
- new `apigee-organization` and `apigee-x-instance`
-- generate `email` and `iam_email` statically in the `iam-service-account` module
+- generate `email` and `iam_email` statically in the `iam-service-account` module
- new `billing-budget` module
- fix `scheduled-asset-inventory-export-bq` module
- output custom role information from the `organization` module
- enable multiple `vpc-sc` perimeters over multiple modules
-- new cloud operations example showing how to [restrict service usage using delegated role grants](./examples/cloud-operations/iam-delegated-role-grants)
+- new cloud operations example showing how to [restrict service usage using delegated role grants](./blueprints/cloud-operations/iam-delegated-role-grants)
- **incompatible change** multiple instance support has been removed from the `compute-vm` module, to bring its interface in line with other modules and enable simple use of `for_each` at the module level; its variables have also slightly changed (`attached_disks`, `boot_disk_delete`, `crate_template`, `zone`)
- **incompatible change** dropped the `admin_ranges_enabled` variable in `net-vpc-firewall`. Set `admin_ranges = []` to get the same effect
- added the `named_ranges` variable to `net-vpc-firewall`
@@ -155,8 +573,8 @@ All notable changes to this project will be documented in this file.
- add support for CMEK keys in Data Foundation end to end example
- add support for VPC-SC perimeters in Data Foundation end to end example
- fix `vpc-sc` module
-- new networking example showing how to use [Private Service Connect to call a Cloud Function from on-premises](./examples/networking/private-cloud-function-from-onprem/)
-- new networking example showing how to organize [decentralized firewall](./examples/networking/decentralized-firewall/) management on GCP
+- new networking example showing how to use [Private Service Connect to call a Cloud Function from on-premises](./blueprints/networking/private-cloud-function-from-onprem/)
+- new networking example showing how to organize [decentralized firewall](./blueprints/networking/decentralized-firewall/) management on GCP
## [5.0.0] - 2021-06-17
@@ -192,7 +610,6 @@ All notable changes to this project will be documented in this file.
## [4.6.1] - 2021-04-01
- **incompatible change** support one group per zone in the `compute-vm` module
- the `group` output is now renamed to `groups`
## [4.6.0] - 2021-03-31
@@ -312,8 +729,7 @@ All notable changes to this project will be documented in this file.
## [3.1.1] - 2020-08-26
- fix error in `project` module
-
-- **incompatible change** make HA VPN Gateway creation optional for `net-vpn-ha` module. Now an existing HA VPN Gateway can be used. Updating to the new version of the module will cause VPN Gateway recreation which can be handled by `terraform state rm/terraform import` operations.
+- **incompatible change** make HA VPN Gateway creation optional for `net-vpn-ha` module. Now an existing HA VPN Gateway can be used. Updating to the new version of the module will cause VPN Gateway recreation which can be handled by `terraform state rm/terraform import` operations.
## [3.1.0] - 2020-08-16
@@ -436,7 +852,6 @@ All notable changes to this project will be documented in this file.
- new `envoy-traffic-director` module in the `cloud-config-container` suite
- new `pubsub` module
-
## [1.4.1] - 2020-05-02
- new `secret-manager` module
@@ -448,14 +863,10 @@ All notable changes to this project will be documented in this file.
- fix Cloud NAT module internal router name lookup
- re-enable and update outputs for the foundations environments example
- add peering route configuration for private clusters to GKE cluster module
-- **incompatible changes** in the GKE nodepool module
- - rename `node_config_workload_metadata_config` variable to `workload_metadata_config`
- - new default for `workload_metadata_config` is `GKE_METADATA_SERVER`
-- **incompatible change** in the `compute-vm` module
- - removed support for MIG and the `group_manager` variable
+- **incompatible changes** in the GKE nodepool module: rename `node_config_workload_metadata_config` variable to `workload_metadata_config`, new default for `workload_metadata_config` is `GKE_METADATA_SERVER`
+- **incompatible change** in the `compute-vm` module: removed support for MIG and the `group_manager` variable
- add `compute-mig` and `net-ilb` modules
-- **incompatible change** in `net-vpc`
- - a new `name` attribute has been added to the `subnets` variable, allowing to directly set subnet name, to update to the new module add an extra `name = false` attribute to each subnet
+- **incompatible change** in `net-vpc`: a new `name` attribute has been added to the `subnets` variable, allowing to directly set subnet name, to update to the new module add an extra `name = false` attribute to each subnet
## [1.3.0] - 2020-04-08
@@ -479,69 +890,69 @@ 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/terraform-google-modules/cloud-foundation-fabric/compare/v14.0.0...HEAD
-[14.0.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v13.0.0...v14.0.0
-[13.0.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v12.0.0...v13.0.0
-[12.0.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v11.2.0...v12.0.0
-[11.2.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v11.1.0...v11.2.0
-[11.1.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v11.0.0...v11.1.0
-[11.0.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v10.0.1...v11.0.0
-[10.0.1]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v10.0.0...v10.0.1
-[10.0.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v9.0.3...v10.0.0
-[9.0.3]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v9.0.2...v9.0.3
-[9.0.2]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v9.0.0...v9.0.2
-[9.0.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v8.0.0...v9.0.0
-[8.0.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v7.0.0...v8.0.0
-[7.0.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v6.0.0...v7.0.0
-[6.0.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v5.1.0...v6.0.0
-[5.1.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v5.0.0...v5.1.0
-[5.0.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v4.9.0...v5.0.0
-[4.9.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v4.8.0...v4.9.0
-[4.8.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v4.7.0...v4.8.0
-[4.7.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v4.6.1...v4.7.0
-[4.6.1]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v4.6.0...v4.6.1
-[4.6.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v4.5.1...v4.6.0
-[4.5.1]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v4.5.0...v4.5.1
-[4.5.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v4.4.2...v4.5.0
-[4.4.2]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v4.4.1...v4.4.2
-[4.4.1]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v4.4.0...v4.4.1
-[4.4.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v4.3.0...v4.4.0
-[4.3.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v4.2.0...v4.3.0
-[4.2.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v4.1.0...v4.2.0
-[4.1.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v4.0.0...v4.1.0
-[4.0.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v3.5.0...v4.0.0
-[3.5.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v3.4.0...v3.5.0
-[3.4.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v3.3.0...v3.4.0
-[3.3.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v3.2.0...v3.3.0
-[3.2.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v3.1.1...v3.2.0
-[3.1.1]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v3.1.0...v3.1.1
-[3.1.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v3.0.0...v3.1.0
-[3.0.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v2.8.0...v3.0.0
-[2.8.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v2.7.1...v2.8.0
-[2.7.1]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v2.7.0...v2.7.1
-[2.7.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v2.6.0...v2.7.0
-[2.6.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v2.5.0...v2.6.0
-[2.5.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v2.4.2...v2.5.0
-[2.4.2]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v2.4.1...v2.4.2
-[2.4.1]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v2.4.0...v2.4.1
-[2.4.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v2.3.0...v2.4.0
-[2.3.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v2.2.0...v2.3.0
-[2.2.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v2.1.0...v2.2.0
-[2.1.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v2.0.0...v2.1.0
-[2.0.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v1.9.0...v2.0.0
-[1.9.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v1.8.1...v1.9.0
-[1.8.1]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v1.8.0...v1.8.1
-[1.8.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v1.7.0...v1.8.0
-[1.7.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v1.6.0...v1.7.0
-[1.6.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v1.5.0...v1.6.0
-[1.5.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v1.4.1...v1.5.0
-[1.4.1]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v1.4.0...v1.4.1
-[1.4.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v1.3.0...v1.4.0
-[1.3.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v1.2...v1.3.0
-[1.2.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v1.1...v1.2
-[1.1.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v1.0...v1.1
-[1.0.0]: https://github.com/terraform-google-modules/cloud-foundation-fabric/compare/v0.1...v1.0
-[#82]: https://github.com/terraform-google-modules/cloud-foundation-fabric/pull/82
-[#103]: https://github.com/terraform-google-modules/cloud-foundation-fabric/pull/103
-[#156]: https://github.com/terraform-google-modules/cloud-foundation-fabric/pull/156
-
+[Unreleased]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v19.0.0...HEAD
+[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
+[14.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v13.0.0...v14.0.0
+[13.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v12.0.0...v13.0.0
+[12.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v11.2.0...v12.0.0
+[11.2.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v11.1.0...v11.2.0
+[11.1.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v11.0.0...v11.1.0
+[11.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v10.0.1...v11.0.0
+[10.0.1]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v10.0.0...v10.0.1
+[10.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v9.0.3...v10.0.0
+[9.0.3]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v9.0.2...v9.0.3
+[9.0.2]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v9.0.0...v9.0.2
+[9.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v8.0.0...v9.0.0
+[8.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v7.0.0...v8.0.0
+[7.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v6.0.0...v7.0.0
+[6.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v5.1.0...v6.0.0
+[5.1.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v5.0.0...v5.1.0
+[5.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v4.9.0...v5.0.0
+[4.9.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v4.8.0...v4.9.0
+[4.8.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v4.7.0...v4.8.0
+[4.7.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v4.6.1...v4.7.0
+[4.6.1]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v4.6.0...v4.6.1
+[4.6.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v4.5.1...v4.6.0
+[4.5.1]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v4.5.0...v4.5.1
+[4.5.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v4.4.2...v4.5.0
+[4.4.2]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v4.4.1...v4.4.2
+[4.4.1]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v4.4.0...v4.4.1
+[4.4.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v4.3.0...v4.4.0
+[4.3.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v4.2.0...v4.3.0
+[4.2.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v4.1.0...v4.2.0
+[4.1.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v4.0.0...v4.1.0
+[4.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v3.5.0...v4.0.0
+[3.5.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v3.4.0...v3.5.0
+[3.4.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v3.3.0...v3.4.0
+[3.3.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v3.2.0...v3.3.0
+[3.2.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v3.1.1...v3.2.0
+[3.1.1]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v3.1.0...v3.1.1
+[3.1.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v3.0.0...v3.1.0
+[3.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v2.8.0...v3.0.0
+[2.8.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v2.7.1...v2.8.0
+[2.7.1]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v2.7.0...v2.7.1
+[2.7.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v2.6.0...v2.7.0
+[2.6.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v2.5.0...v2.6.0
+[2.5.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v2.4.2...v2.5.0
+[2.4.2]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v2.4.1...v2.4.2
+[2.4.1]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v2.4.0...v2.4.1
+[2.4.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v2.3.0...v2.4.0
+[2.3.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v2.2.0...v2.3.0
+[2.2.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v2.1.0...v2.2.0
+[2.1.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v2.0.0...v2.1.0
+[2.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v1.9.0...v2.0.0
+[1.9.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v1.8.1...v1.9.0
+[1.8.1]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v1.8.0...v1.8.1
+[1.8.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v1.7.0...v1.8.0
+[1.7.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v1.6.0...v1.7.0
+[1.6.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v1.5.0...v1.6.0
+[1.5.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v1.4.1...v1.5.0
+[1.4.1]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v1.4.0...v1.4.1
+[1.4.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v1.3.0...v1.4.0
+[1.3.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v1.2.0...v1.3.0
+[1.2.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v1.1.0...v1.2.0
+[1.1.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v1.0.0...v1.1.0
+[1.0.0]: https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/compare/v0.1...v1.0.0
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ed2815b709..18a42f59dd 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,16 +1,826 @@
# Contributing
-We welcome any contributions on bugs, or feature requests you would like to submit!
+Contributors are the engine that keeps Fabric alive so if you were or are planning to be active in this repo, a huge thanks from all of us for dedicating your time!!! If you have free time and are looking for suggestions on what to work on, our issue tracker generally has a few pending feature requests: you are welcome to send a PR for any of them.
-The basic process is pretty simple:
+## Table of Contents
-* Fork the Project
-* Create your Feature Branch
`git checkout -b feature/AmazingFeature`
-* Commit your Changes
`git commit -m 'Add some AmazingFeature`
-* Make sure tests pass!
`pytest # in the root or `tests` folder`
-* Make sure Terraform linting is ok (hint: `terraform fmt -recursive` in the root folder)
-* Make sure any changes to variables and outputs are reflected in READMEs
`./tools/tfdoc.py [changed folder]`
-* Push to the Branch
`git push origin feature/AmazingFeature`
-* Open a Pull Request
+[I just found a bug / have a feature request!](#i-just-found-a-bug--have-a-feature-request)
-When implementing your new feature, please follow our [core design principles](./MANIFESTO.md#core-design-principles).
+[Quick developer workflow](#quick-developer-workflow)
+
+[Developer's Handbook](#developers-handbook)
+
+- [The Zen of Fabric](#the-zen-of-fabric)
+- [Design principles in action](#design-principles-in-action)
+- [FAST stage design](#fast-stage-design)
+- [Style guide reference](#style-guide-reference)
+- [Checks, tests and tools](#interacting-with-checks-tests-and-tools)
+
+## I just found a bug / have a feature request
+
+Feel free to open a new issue if you find something that does not work, need clarifications on usage (a good incentive for us to improve docs!), or have a feature request.
+
+If you feel like tackling it directly via a PR check out the quick developer workflow below, we always welcome new contributors!
+
+## Quick developer workflow
+
+For small or first time issues the simplest way is to fork our repo, but if you are a regular contributor or are developing a large addition, ask us to be added directly to the repo so you can work on local branches, it makes life easier for both of us!
+
+Fork or clone and go through the usual edit/add/commit cycle until your code is ready.
+
+```bash
+git checkout master
+git pull
+git checkout -b username/my-feature
+git add -A
+git commit -m "changed ham so that spam and eggs"
+```
+
+Once you are satisfied with your changes, make sure Terraform linting is ok. If you changed Python code you need to conform to our standard linting, see the last section for details on how to configure it.
+
+```bash
+terraform fmt -recursive
+```
+
+If you changed variables or outputs you need to regenerate the relevant tables in the documentation via our `tfdoc` tool. For help installing Python requirements and setting up virtualenv see the last section.
+
+```bash
+# point tfdoc to the folder containing your changed variables and outputs
+./tools/tfdoc.py modules/my-changed-module
+```
+
+If the folder contains files which won't be pushed to the repository, for example provider files used in FAST stages, you need to change the command above to specifically exclude them from `tfdoc` generated output.
+
+```bash
+# exclude a local provider file from the generated documentation
+./tools/tfdoc.py -x 00-bootstrap-providers.tf fast/stages/00-bootstrap
+```
+
+Run tests to make sure your changes work and you didn't accidentally break their consumers. Again, if you need help setting up the Python virtualenv and requirements or want to run specific test subsets see the last section.
+
+```bash
+pytest tests
+```
+
+Keep in mind we also test documentation examples so even if your PR only changes README files, you need to run a subset of tests.
+
+```bash
+# use if you only changed README examples, ignore if you ran all tests
+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"
+```
+
+## Developer's handbook
+
+Over the years we have assembled a specific set of design principles and style conventions that allow for better readability and make understanding and changing code more predictable.
+
+We expect your code to conform to those principles in both design and style, so that it integrates well with the rest of Fabric/FAST without having to go through long and painful PR cycles before it can be merged.
+
+The sections below describe our design approach and style conventions, with specific mentions of FAST stages where their larger scope requires additional rules.
+
+### The Zen of Fabric
+
+While our approach to Terraform is constantly evolving as we meet new requirements or language features are released, there's a small set of core principles which influences all our code, and that you are expected to make yours before sending a PR.
+
+Borrowing the format from the [Zen of Python](https://peps.python.org/pep-0020/) here is our fundamental design philosophy:
+
+- always **design for composition** as our objective is to support whole infrastructures
+- **encapsulate logical entities** that match single functional units in modules or stages to improve readability and composition (don't design by product or feature)
+- **adopt common interfaces** across modules and **design small variable spaces** to decrease cognitive overload
+- **write flat and concise code** which is easy to clone, evolve and troubleshoot independently
+- **don't aim at covering all use cases** but make default ones simple and complex ones possible, to support rapid prototyping and specific production requirements
+- when in doubt always **prefer code readability** for simplified maintenance and to achieve IaC as documentation
+- **don't be too opinionated** in resource configurations as this makes it harder for users to implement their exact requirements
+- **avoid side effects** and never rely on external tools to eliminate friction and reduce fragility
+
+The following sections describe how these principles are applied in practice, with actual code examples from modules and FAST stages.
+
+### Design principles in action
+
+This section illustrates how our design principles translate into actual code. We consider this a living document that can be updated at any time.
+
+#### Design by logical entity instead of product/feature
+
+This is probably our oldest and most important design principle. When designing a module or a FAST stage we look at its domain from a functional point of view: **what is the subset of resources (or modules for FAST) that fully describes one entity and allows encapsulating its full configuration?**
+
+It's a radically different approach from designing by product or feature, where boundaries are drawn around a single GCP functionality.
+
+Our modules -- and in a much broader sense our FAST stages -- are all designed to encapsulate a set of functionally related resources and their configurations. This achieves two main goals: to dramatically improve readability by using a single block of code -- a module declaration -- for a logical component; and to allow consumers to rely on outputs without having to worry about the dependency chain, as all related resources and configurations are managed internally in the module or stage.
+
+Taking IAM as an example, we do not offer a single module to centrally manage role bindings (the product/feature based approach) but implement it instead in each module (the logical component approach) since:
+
+- users understand IAM as an integral part of each resource, having bindings in the same context improves readability and speeds up changes
+- resources are not fully usable before their relevant IAM bindings have been applied, encapsulating those allows referencing fully configured resources from the outside
+- managing resources and their bindings in a single module makes code more portable with fewer dependencies
+
+The most extensive examples of this approach are our resource management modules. For instance, the `project` module encapsulates resources for project, project services, logging sinks, project-level IAM bindings, Shared VPC enablement and attachment, metrics scope, budget alerts, organization policies, and several other functionalities in a single place.
+
+A typical project module code block is easy to read as it centralizes all the information in one place, and allows consumers referencing it to trust that it will behave as a fully configured unit.
+
+```hcl
+module "project" {
+ source = "./modules/project"
+ parent = "folders/1234567890"
+ name = "project-example"
+ billing_account = local.billing_account
+ services = [
+ "container.googleapis.com",
+ "stackdriver.googleapis.com",
+ "storage.googleapis.com",
+ ]
+ iam = {
+ "roles/viewer" = ["user1:one@example.org"]
+ }
+ policy_boolean = {
+ "constraints/compute.disableGuestAttributesAccess" = true
+ "constraints/compute.skipDefaultNetworkCreation" = true
+ }
+ service_encryption_key_ids = {
+ compute = [local.kms.europe-west1.compute]
+ storage = [local.kms.europe.gcs]
+ }
+ shared_vpc_service_config = {
+ attach = true
+ host_project = "project-host"
+ }
+}
+```
+
+#### Define and reuse stable interfaces
+
+Our second oldest and most important principle also stems from the need to design for composition: **whenever the same functionality is implemented in different modules, a stable variables interface should be designed and reused identically across them**.
+
+Adopting the same interface across different modules reduces cognitive overload on users, improves readability by turning configurations into repeated patterns, and makes code more robust by using the same implementation everywhere.
+
+Taking IAM again as an example, every module that allows management of IAM bindings conforms to the same interface.
+
+```hcl
+module "project" {
+ source = "./modules/project"
+ name = "project-example"
+ iam = {
+ "roles/viewer" = ["user1:one@example.org"]
+ }
+}
+
+module "pubsub" {
+ source = "./modules/pubsub"
+ project_id = module.project.project_id
+ name = "my-topic"
+ iam = {
+ "roles/pubsub.viewer" = ["group:foo@example.com"]
+ "roles/pubsub.subscriber" = ["user:user1@example.com"]
+ }
+}
+```
+
+We have several such interfaces defined for IAM, log sinks, organizational policies, etc. and always reuse them across modules.
+
+#### Design interfaces to support actual usage
+
+Variables should not simply map to the underlying resource attributes, but their **interfaces should be designed to match common use cases** to reduce friction and offer the highest possible degree of legibility.
+
+This translates into different practical approaches:
+
+- multiple sets of interfaces that support the same feature which are then internally combined into the same resources (e.g. IAM groups below)
+- functional interfaces that don't map 1:1 to resources (e.g. project service identities below)
+- crossing the project boundary to configure resources which support key logical functionality (e.g shared VPC below)
+
+The most pervasive example of the first practical approach above is IAM: given its importance we implement both a role-based interface and a group-based interface, which is less verbose and makes it easy to understand at a glance the roles assigned to a specific group. Both interfaces provide data that is then internally combined to drive the same IAM binding resource, and are available for authoritative and additive roles.
+
+```hcl
+module "project" {
+ source = "./modules/project"
+ name = "project-example"
+ group_iam = {
+ "roles/editor" = [
+ "group:foo@example.com"
+ ]
+ }
+ iam = {
+ "roles/editor" = [
+ "serviceAccount:${module.project.service_accounts.cloud_services}"
+ ]
+ }
+}
+```
+
+Another practical consequence of this design principle is supporting common use cases via interfaces that don't directly map to a resource. The example below shows support for enabling service identities access to KMS keys used for CMEK encryption in the `project` module: there's no specific resource for service identities, but it's such a frequent use case that we support them directly in the module.
+
+```hcl
+module "project" {
+ source = "./modules/project"
+ name = "project-example"
+ service_encryption_key_ids = {
+ compute = [local.kms.europe-west1.compute]
+ storage = [local.kms.europe.gcs]
+ }
+}
+```
+
+The principle also applies to output interfaces: it's often useful to assemble specific pieces of information in the module itself, as this improves overall code legibility. For example, we also support service identities in the `project` module's outputs (used here self-referentially).
+
+```hcl
+module "project" {
+ source = "./modules/project"
+ name = "project-example"
+ iam = {
+ "roles/editor" = [
+ "serviceAccount:${module.project.service_accounts.cloud_services}"
+ ]
+ }
+}
+```
+
+And the last practical application of the principle which we show here is crossing project boundaries to support specific functionality, as in the two examples below that support Shared VPC in the `project` module.
+
+Host-based management, typically used where absolute control over service project attachment is required:
+
+```hcl
+module "project" {
+ source = "./modules/project"
+ name = "project-host"
+ shared_vpc_host_config = {
+ enabled = true
+ service_projects = [
+ "prj-1", "prj-2"
+ ]
+ }
+}
+```
+
+Service-based attachment, more common and typically used to delegate service project attachment at project creation, possibly from a project factory.
+
+```hcl
+module "project" {
+ source = "./modules/project"
+ name = "prj-1"
+ shared_vpc_service_config = {
+ attach = true
+ host_project = "project-host"
+ }
+}
+```
+
+#### Design compact variable spaces
+
+Designing variable spaces is one of the most complex aspects to get right, as they are the main entry point through which users consume modules, examples and FAST stages. We always strive to **design small variable spaces by leveraging objects and implementing defaults** so that users can quickly produce highly readable code.
+
+One of many examples of this approach comes from disk support in the `compute-vm` module, where preset defaults allow quick VM management with very few lines of code, and optional variables allow progressively expanding the code when more control is needed.
+
+This brings up an instance with a 10GB PD balanced boot disk using a Debian 11 image, and is generally a good default when a quick VM is needed for experimentation.
+
+```hcl
+module "simple-vm-example" {
+ source = "./modules/compute-vm"
+ project_id = var.project_id
+ zone = "europe-west1-b"
+ name = "test"
+}
+```
+
+Changing boot disks defaults is of course possible, and adds some verbosity to the simple example above as you need to specify all of them.
+
+```hcl
+module "simple-vm-example" {
+ source = "./modules/compute-vm"
+ project_id = var.project_id
+ zone = "europe-west1-b"
+ name = "test"
+ boot_disk = {
+ image = "projects/debian-cloud/global/images/family/cos-97-lts"
+ type = "pd-balanced"
+ size = 10
+ }
+}
+```
+
+Where this results in objects with too many attributes, we usually split attributes between required and optional by adding a second level, as in this example where VM `attached_disks[].options` contains less used attributes and can be set to null if not needed.
+
+```hcl
+module "simple-vm-example" {
+ source = "./modules/compute-vm"
+ project_id = var.project_id
+ zone = "europe-west1-b"
+ name = "test"
+ attached_disks = [
+ { name="data", size=10, source=null, source_type=null, options=null }
+ ]
+}
+```
+
+Whenever options are not passed like in the example above, we typically infer their values from a defaults variable which can be customized when using defaults across several items. In the following example instead of specifying regional PD options for both disks, we set their options to `null` and change the defaults used for all disks.
+
+```hcl
+module "simple-vm-example" {
+ source = "./modules/compute-vm"
+ project_id = var.project_id
+ zone = "europe-west1-b"
+ name = "test"
+ attached_disk_defaults = {
+ auto_delete = false
+ mode = "READ_WRITE"
+ replica_zone = "europe-west1-c"
+ type = "pd-balanced"
+ }
+ attached_disks = [
+ { name="data1", size=10, source=null, source_type=null, options=null },
+ { name="data2", size=10, source=null, source_type=null, options=null }
+ ]
+}
+```
+
+#### Depend outputs on internal resources
+
+We mentioned this principle when discussing encapsulation above but it's worth repeating it explicitly: **set explicit dependencies in outputs so consumers will wait for full resource configuration**.
+
+As an example, users can safely reference the project module's `project_id` output from other modules, knowing that the dependency tree for project configurations (service activation, IAM, etc.) has already been defined inside the module itself. In this particular example the output is also interpolated instead of derived from the resource, so as to avoid issues when used in `for_each` keys.
+
+```hcl
+output "project_id" {
+ description = "Project id."
+ value = "${local.prefix}${var.name}"
+ depends_on = [
+ google_project.project,
+ data.google_project.project,
+ google_project_organization_policy.boolean,
+ google_project_organization_policy.list,
+ google_project_service.project_services,
+ google_compute_shared_vpc_service_project.service_projects,
+ google_project_iam_member.shared_vpc_host_robots,
+ google_kms_crypto_key_iam_member.service_identity_cmek,
+ google_project_service_identity.servicenetworking,
+ google_project_iam_member.servicenetworking
+ ]
+}
+```
+
+#### Why we don't use random strings in names
+
+This is more a convention than a design principle, but it's still important enough to be mentioned here: we **never use random strings for resource naming** and instead rely on an optional `prefix` variable which is implemented in most modules.
+
+This matches actual use where naming is a key requirement that needs to integrate with company-wide CMDBs and naming schemes used on-prem or in other clouds, and usually is formed by concatenating progressively more specific tokens (something like `myco-gcp-dev-net-hub-0`).
+
+Our approach supports easy implementation of company-specific policies and good readability, while still allowing a fairly compact way of ensuring unique resources have unique names.
+
+```hcl
+# prefix = "foo-gcp-dev"
+
+module "project" {
+ source = "./modules/project"
+ name = "net-host-0"
+ prefix = var.prefix
+}
+
+module "project" {
+ source = "./modules/project"
+ name = "net-svc-0"
+ prefix = var.prefix
+}
+```
+
+### FAST stage design
+
+Due to their increased complexity and larger scope, FAST stages have some additional design considerations. Please refer to the [FAST documentation](./fast/) for additional context.
+
+#### Standalone usage
+
+Each FAST stage should be designed so that it can optionally be used in isolation, with no dependencies on anything other than its variables.
+
+#### Stage interfaces
+
+Stages are designed based on the concept of ["contracts" or interfaces](./fast/README.md#contracts-and-stages), which define what information is produced by one stage via outputs, which is then consumed by subsequent stages via variables.
+
+Interfaces are compact in size (few variables) but broad in scope (variables typically leverage maps), so that consumers can declare in variable types only the bits of information they are interested in.
+
+For example, resource management stages only export three map variables: `folder_ids`, `service_accounts`, `tag_names`. Those variables contain values for all the relevant resources created, but consumers are only interested in some of them and only need to declare those: networking stages for example only declare the folder and service account names they need.
+
+```hcl
+variable "folder_ids" {
+ # tfdoc:variable:source 01-resman
+ description = "Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created."
+ type = object({
+ networking = string
+ networking-dev = string
+ networking-prod = string
+ })
+}
+```
+
+When creating a new stage or adding a feature to an existing one, always try to leverage the existing interfaces when some of the information you produce needs to cross the stage boundary, so as to minimize impact on producers and consumers logically dependent on your stage.
+
+#### Output files
+
+FAST stages rely on generated provider and tfvars files, as an easy convenience that allows automated setup and passing of contract values between stages.
+
+Files are written to a special GCS bucket in order to be leveraged by both humans and CI/CD workflows, and optionally also written to local storage if needed.
+
+When editing or adding a stage, you are expected to maintain the output files system so any new contact output is also present in files.
+
+### Style guide reference
+
+Similarly to our design principles above, we evolved a set of style conventions that we try to standardize on to make code more legible and uniform. This reduces friction when coding, and ideally moves us closer to the goal of using IaC as live documentation.
+
+#### Group logical resources or modules in separate files
+
+Over time and as our codebase got larger, we switched away from the canonical `main.tf`/`outputs.tf`/`variables.tf` triplet of file names and now tend to prefer descriptive file names that refer to the logical entities (resources or modules) they contain.
+
+We still use traditional names for variables and outputs, but tend to use main only for top-level locals or resources (e.g. the project resource in the `project` module), or for those resources that would end up in very small files.
+
+While some older modules and examples are still using three files, we are slowly bringing all code up to date and any new development should use descriptive file names.
+
+Our `tfdoc` tool has a way of generating a documentation table that maps file names with descriptions and the actual resources and modules they contain, refer to the last section for details on how to activate the mode in your code.
+
+#### Enforce line lengths
+
+We enforce line length for legibility, and adopted the 79 characters convention from other languages for simplicity.
+
+This convention is relaxed for long resource attribute names (even though in some cases you might want to alias them to short local names), and for variable and output descriptions.
+
+In most other cases you should break long lines, especially in `for` and `for_each` loops. Some of the conventions we adopted:
+
+- break after opening and before closing braces/parenthesis
+- break after a colon in `for` loops
+- add extra parenthesis and breaks to split long ternary operators
+- break right before the `:` and `?` in long ternary operators
+
+This is one of many examples.
+
+```hcl
+locals {
+ sink_bindings = {
+ for type in ["bigquery", "pubsub", "logging", "storage"] :
+ type => {
+ for name, sink in var.logging_sinks :
+ name => sink if sink.iam && sink.type == type
+ }
+ }
+}
+```
+
+#### Use alphabetical order for outputs and variables
+
+We enforce alphabetical ordering for outputs and variables and have a check that prevents PRs using the wrong order to be merged. We also tend to prefer alphabetical ordering in locals when there's no implied logical grouping (e.g. for successive data transformations).
+
+Additionally, we adopt a convention similar to the one used in Python for private class members, so that locals only referenced from inside the same locals block are prefixed by `_`, as in the example shown in the next section.
+
+```hcl
+locals {
+ # compute the host project IAM bindings for this project's service identities
+ _svpc_service_iam = flatten([
+ for role, services in local._svpc_service_identity_iam : [
+ for service in services : { role = role, service = service }
+ ]
+ ])
+ _svpc_service_identity_iam = coalesce(
+ local.svpc_service_config.service_identity_iam, {}
+ )
+ svpc_host_config = {
+ enabled = coalesce(
+ try(var.shared_vpc_host_config.enabled, null), false
+ )
+ service_projects = coalesce(
+ try(var.shared_vpc_host_config.service_projects, null), []
+ )
+ }
+ svpc_service_config = coalesce(var.shared_vpc_service_config, {
+ host_project = null, service_identity_iam = {}
+ })
+ svpc_service_iam = {
+ for b in local._svpc_service_iam : "${b.role}:${b.service}" => b
+ }
+}
+```
+
+#### Move complex transformations to locals
+
+When data needs to be transformed in a `for` or `for_each` loop, we prefer moving the relevant code to `locals` so that module or resource attribute values have as little line noise as possible. This is especially relevant for complex transformations, which should be split in multiple smaller stages with descriptive names.
+
+This is an example from the `project` module. Notice how we're breaking two of the rules above: line length in the last local so as to use the same formatting as the previous one, and alphabetical ordering so the order follows the transformation steps. Our rules are meant to improve legibility, so when they don't feel free to ignore them (and sometimes we'll push back anyway).
+
+```hcl
+locals {
+ _group_iam_roles = distinct(flatten(values(var.group_iam)))
+ _group_iam = {
+ for r in local._group_iam_roles : r => [
+ for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
+ ]
+ }
+ _iam_additive_pairs = flatten([
+ for role, members in var.iam_additive : [
+ for member in members : { role = role, member = member }
+ ]
+ ])
+ _iam_additive_member_pairs = flatten([
+ for member, roles in var.iam_additive_members : [
+ for role in roles : { role = role, member = member }
+ ]
+ ])
+ iam = {
+ for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
+ role => concat(
+ try(var.iam[role], []),
+ try(local._group_iam[role], [])
+ )
+ }
+ iam_additive = {
+ for pair in concat(local._iam_additive_pairs, local._iam_additive_member_pairs) :
+ "${pair.role}-${pair.member}" => pair
+ }
+}
+```
+
+#### 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" {
+ description = "Optional prefix used for resource names."
+ type = string
+ default = null
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty, please use null instead."
+ }
+}
+
+# main.tf
+locals {
+ prefix = var.prefix == null ? "" : "${var.prefix}-"
+}
+```
+
+For blueprints the prefix is mandatory:
+
+```hcl
+variable "prefix" {
+ description = "Prefix used for resource names."
+ type = string
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty."
+ }
+}
+```
+
+### Interacting with checks, tests and tools
+
+Our modules are designed for composition and live in a monorepo together with several end-to-end blueprints, so it was inevitable that over time we found ways of ensuring that a change does not break consumers.
+
+Our tests exercise most of the code in the repo including documentation examples, and leverages the [tftest Python library](https://pypi.org/project/tftest/) we developed and independently published on PyPi.
+
+Automated workflows run checks on PRs to ensure all tests pass, together with a few other controls that ensure code is linted, documentation reflects variables and outputs files, etc.
+
+The following sections describe how interact with the above, and how to leverage some of the small utilities contained in this repo.
+
+#### Python environment setup
+
+All our tests and tools use Python, this section shows you how to bring up an environment with the correct dependencies installed.
+
+First, follow the [official guide](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/) so that you have a working virtual environment and `pip` installed.
+
+Once you have created and activated a virtual environment, install the dependencies we use for testing and tools.
+
+```bash
+pip install -r tests/requirements.txt
+pip install -r tools/requirements.txt
+```
+
+#### Automated checks on PRs
+
+We run two GitHub workflows on PRs:
+
+- `.github/workflows/linting.yml`
+- `.github/workflows/tests.yml`
+
+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`
+- that all Python code has been formatted with the correct `yapf` style
+
+You can run those checks individually on your code to address any error before sending a PR, all you need to do is run the same command used in the workflow file from within your virtual environment. To run documentation tests for example if you changed the `project` module:
+
+```bash
+./tools/check_documentation.py modules/project
+```
+
+Our tools generally support a `--help` switch, so you can also use them for other purposes:
+
+```bash
+/tools/check_documentation.py --help
+Usage: check_documentation.py [OPTIONS] [DIRS]...
+
+ Cycle through modules and ensure READMEs are up-to-date.
+
+Options:
+ -x, --exclude-file TEXT
+ --files / --no-files
+ --show-diffs / --no-show-diffs
+ --show-extra / --no-show-extra
+ --help Show this message and exit.
+```
+
+The test workflow runs test suites in parallel. Refer to the next section for more details on running and writing tests.
+
+#### Using and writing tests
+
+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, leveraging 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 via `tfvars` and `yaml`
+
+Our new approach to testing requires you to:
+
+- 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
+mkdir tests/modules/net_glb
+touch tests/modules/net_glb/__init__.py
+```
+
+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
+# file: tests/modules/net_glb/test-simple.tfvars
+name = "glb-test-0"
+project_id = "my-project"
+backend_buckets_config = {
+ default = {
+ bucket_name = "my-bucket"
+ }
+}
+```
+
+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
+mkdir tests/modules/net_glb
+touch tests/modules/net_glb/__init__.py
+```
+
+Now create a file containing your tests, e.g. `test_plan.py`:
+```python
+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, the tests for [`organization`](./tests/modules/organization/test_plan_org_policies.py) and [`net-vpc`](./tests/modules/net_vpc/test_routes.py) modules.
+
+#### 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 expected.
+
+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" {
+ source = "./modules/dns"
+ project_id = "myproject"
+ type = "private"
+ name = "test-example"
+ domain = "test.example."
+ client_networks = [var.vpc.self_link]
+ recordsets = {
+ "A localhost" = { ttl = 300, records = ["127.0.0.1"] }
+ }
+}
+# 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.
+
+By default, `tfdoc` expects the path to a folder as its argument, and will parse variables and outputs files contained in it and embed generated tables in its README file.
+
+You decide where the generated tables will be placed (or replaced if they already exist) via two special HTML comment tags, that mark the beginning and end of the space that will be managed by `tfdoc`.
+
+```html
+
+
+
+```
+
+You can also set `tfdoc` options directly in a README file, so that a) you don't need to remember to pass the right options when running the tool, and b) our automated workflow checks will know how to generate the right output.
+
+```html
+
+
+```
+
+When generating the files table, a special annotation can be used to fill in the file description in Terraform files:
+
+```hcl
+# tfdoc:file:description Networking stage resources.
+```
+
+The tool can also be run so that it prints the generated output on standard output instead of replacing in files. Run `tfdoc --help` to see all available options.
diff --git a/FABRIC-AND-CFT.md b/FABRIC-AND-CFT.md
new file mode 100644
index 0000000000..7d4f167870
--- /dev/null
+++ b/FABRIC-AND-CFT.md
@@ -0,0 +1,164 @@
+# Cloud Foundation Fabric and Cloud Foundation Toolkit
+
+This page highlights the main differences (both technical and philosophical) between Cloud Foundation Fabric and Cloud Foundation Toolkit for end users, to guide them in their decision making process for identifying the best suite of modules for their use cases.
+
+## Cloud Foundation Fabric (a.k.a Fabric, this repo)
+
+Fabric is a collection of Terraform modules and end to end examples meant to be cloned as a single unit and used as is for fast prototyping or decomposed and modified for usage in organizations.
+
+## Cloud Foundation Toolkit (a.k.a CFT)
+
+CFT is a collection of Terraform modules and examples with opinionated GCP best practices implemented as individual modules for gradual adoption and off the shelf usage in organizations.
+
+## Key Differences
+
+
+ | +Fabric + | +CFT + | +
Target User + | +Organizations interested in forking, maintaining and customizing Terraform modules. + | +Organizations interested in using opinionated, prebuilt Terraform modules. + | +
Configuration + | +Less opinionated allowing end users higher flexibility. + | +Opinionated by default, end users may need to fork if it does not meet their use case. + | +
Extensibility + | +Built with extensibility in mind catering to fork and use patterns. Modules are often lightweight and easy to adopt / tailor to specific use cases. + | +Not built with fork and use extensibility catering primarily to off the shelf consumption. Modules are tailored towards common usecases and extensible via composition. + | +
Config customization + | +Prefer customization using variables via objects, tight variable space. + | +Prefer customization using variables via primitives. + | +
Examples + | +Thorough examples for individual modules, and end to end examples composing multiple modules covering a wide variety of use cases from foundations to solutions. + | +Examples for a module mostly focus on that individual module. Composition is often not shown in examples but in larger modules built using smaller modules. + | +
Resources + | +Leaner modules wrapping resources. + | +Heavier root modules that often compose leaner sub modules wrapping resources. + | +
Resource grouping + | +Generally grouped by logical entities. + | +Generally grouped by products/product areas. + | +
Release Cadence + | +Modules versioned and released together. + | +Modules versioned and released individually. + | +
Individual module usage + | +Individual modules consumed directly using Git as a module source.
+ +For production usage, customers are encouraged to “fork and own” their own repository. + |
+ Individual repositories consumed via the Terraform registry.
+ +For production/airgapped usage, customers may also mirror modules to a private registry. + |
+
Factories + | +Fabric implements several "factories" in modules, where users can drive or automate Terraform via YAML files (projects, subnetworks, firewalls, etc.). + | +CFT does not implement factories and generally show examples usable with variable definitions files (.tfvars). + | +
Organizational adoption + | +Mono repo cloned into an organizational VCS (or catalog) and separated into individual modules for internal consumption. + | +Individual repos forked (for air gap) or wrapping upstream sources to create individual modules for internal consumption. + | +
Distribution + | +Distributed via Git/GitHub. + | +Distributed via Git/GitHub and Terraform Registry. + | +
Testing + | +Every PR performs unit tests on modules, examples, and documentation snippets by evaluating a Terraform plan via Python tftest library. + | +Every PR performs full end-to-end deployment with integration tests using the blueprint test framework. + | +
- +
# Terraform Examples and Modules for Google Cloud -This repository provides **end-to-end examples** and a **suite of Terraform modules** for Google Cloud, which support different use cases: +This repository provides **end-to-end blueprints** and a **suite of Terraform modules** for Google Cloud, which support different use cases: - organization-wide [landing zone blueprint](fast/) used to bootstrap real-world cloud foundations -- reference [examples](./examples/) used to deep dive on network patterns or product features -- a comprehensive source of lean [modules](./modules/dns) that lend themselves well to changes +- reference [blueprints](./blueprints/) used to deep dive on network patterns or product features +- a comprehensive source of lean [modules](./modules/) that lend themselves well to changes -The whole repository is meant to be cloned as a single unit, and then forked into separate owned repositories to seed production usage, or used as-is and periodically updated as a complete toolkit for prototyping. You can read more on this approach in our [manifesto](./MANIFESTO.md). +The whole repository is meant to be cloned as a single unit, and then forked into separate owned repositories to seed production usage, or used as-is and periodically updated as a complete toolkit for prototyping. You can read more on this approach in our [contributing guide](./CONTRIBUTING.md), and a comparison against similar toolkits [here](./FABRIC-AND-CFT.md). ## Organization blueprint (Fabric FAST) @@ -26,16 +29,16 @@ The current list of modules supports most of the core foundational and networkin Currently available modules: -- **foundational** - [folder](./modules/folder), [organization](./modules/organization), [project](./modules/project), [service accounts](./modules/iam-service-account), [logging bucket](./modules/logging-bucket), [billing budget](./modules/billing-budget), [naming convention](./modules/naming-convention), [projects-data-source](./modules/projects-data-source) -- **networking** - [VPC](./modules/net-vpc), [VPC firewall](./modules/net-vpc-firewall), [VPC peering](./modules/net-vpc-peering), [VPN static](./modules/net-vpn-static), [VPN dynamic](./modules/net-vpn-dynamic), [HA VPN](./modules/net-vpn-ha), [NAT](./modules/net-cloudnat), [address reservation](./modules/net-address), [DNS](./modules/dns), [L4 ILB](./modules/net-ilb), [Service Directory](./modules/service-directory), [Cloud Endpoints](./modules/endpoints) -- **compute** - [VM/VM group](./modules/compute-vm), [MIG](./modules/compute-mig), [GKE cluster](./modules/gke-cluster), [GKE nodepool](./modules/gke-nodepool), [GKE hub](./modules/gke-hub), [COS container](./modules/cloud-config-container/cos-generic-metadata/) (coredns, mysql, onprem, squid) -- **data** - [GCS](./modules/gcs), [BigQuery dataset](./modules/bigquery-dataset), [Pub/Sub](./modules/pubsub), [Datafusion](./modules/datafusion), [Bigtable instance](./modules/bigtable-instance), [Cloud SQL instance](./modules/cloudsql-instance), [Data Catalog Policy Tag](./modules/data-catalog-policy-tag) -- **development** - [Cloud Source Repository](./modules/source-repository), [Container Registry](./modules/container-registry), [Artifact Registry](./modules/artifact-registry), [Apigee Organization](./modules/apigee-organization), [Apigee X Instance](./modules/apigee-x-instance), [API Gateway](./modules/api-gateway) -- **security** - [KMS](./modules/kms), [SecretManager](./modules/secret-manager), [VPC Service Control](./modules/vpc-sc) +- **foundational** - [billing budget](./modules/billing-budget), [Cloud Identity group](./modules/cloud-identity-group/), [folder](./modules/folder), [service accounts](./modules/iam-service-account), [logging bucket](./modules/logging-bucket), [organization](./modules/organization), [project](./modules/project), [projects-data-source](./modules/projects-data-source) +- **networking** - [DNS](./modules/dns), [Cloud Endpoints](./modules/endpoints), [address reservation](./modules/net-address), [NAT](./modules/net-cloudnat), [Global Load Balancer (classic)](./modules/net-glb/), [L4 ILB](./modules/net-ilb), [L7 ILB](./modules/net-ilb-l7), [VPC](./modules/net-vpc), [VPC firewall](./modules/net-vpc-firewall), [VPC peering](./modules/net-vpc-peering), [VPN dynamic](./modules/net-vpn-dynamic), [HA VPN](./modules/net-vpn-ha), [VPN static](./modules/net-vpn-static), [Service Directory](./modules/service-directory) +- **compute** - [VM/VM group](./modules/compute-vm), [MIG](./modules/compute-mig), [COS container](./modules/cloud-config-container/cos-generic-metadata/) (coredns, mysql, onprem, squid), [GKE cluster](./modules/gke-cluster), [GKE hub](./modules/gke-hub), [GKE nodepool](./modules/gke-nodepool) +- **data** - [BigQuery dataset](./modules/bigquery-dataset), [Bigtable instance](./modules/bigtable-instance), [Cloud SQL instance](./modules/cloudsql-instance), [Data Catalog Policy Tag](./modules/data-catalog-policy-tag), [Datafusion](./modules/datafusion), [GCS](./modules/gcs), [Pub/Sub](./modules/pubsub) +- **development** - [API Gateway](./modules/api-gateway), [Apigee](./modules/apigee), [Artifact Registry](./modules/artifact-registry), [Container Registry](./modules/container-registry), [Cloud Source Repository](./modules/source-repository) +- **security** - [Binauthz](./modules/binauthz/), [KMS](./modules/kms), [SecretManager](./modules/secret-manager), [VPC Service Control](./modules/vpc-sc) - **serverless** - [Cloud Function](./modules/cloud-function), [Cloud Run](./modules/cloud-run) For more information and usage examples see each module's README file. -## End-to-end examples +## End-to-end blueprints -The [examples](./examples/) in this repository are split in several main sections: **[foundational examples](./examples/foundations/)** that bootstrap the organizational hierarchy and automation prerequisites, **[networking examples](./examples/networking/)** that implement core patterns or features, **[data solutions examples](./examples/data-solutions/)** that demonstrate how to integrate data services in complete scenarios, **[cloud operations examples](./examples/cloud-operations/)** that leverage specific products to meet specific operational needs and **[factories](./examples/factories/)** that implement resource factories for the repetitive creation of specific resources. +The [blueprints](./blueprints/) in this repository are split in several main sections: **[networking blueprints](./blueprints/networking/)** that implement core patterns or features, **[data solutions blueprints](./blueprints/data-solutions/)** that demonstrate how to integrate data services in complete scenarios, **[cloud operations blueprints](./blueprints/cloud-operations/)** that leverage specific products to meet specific operational needs and **[factories](./blueprints/factories/)** that implement resource factories for the repetitive creation of specific resources, and finally **[GKE](./blueprints/gke)**, **[serverless](./blueprints/serverless)**, and **[third-party solutions](./blueprints/third-party-solutions/)** design blueprints. diff --git a/REFERENCES.md b/REFERENCES.md index 6416feb2a0..86407ea314 100644 --- a/REFERENCES.md +++ b/REFERENCES.md @@ -21,6 +21,12 @@ This is a non-exhaustive list of Fabric usage in the wild. Send us a PR if you s ## GCP Reference Architectures +- [Decide a resource hierarchy for your Google Cloud landing zone](https://cloud.google.com/architecture/landing-zones/decide-resource-hierarchy) +- [Decide the network design for your Google Cloud landing zone](https://cloud.google.com/architecture/landing-zones/decide-network-design) - [Hub-and-spoke network architecture](https://cloud.google.com/architecture/deploy-hub-spoke-vpc-network-topology) - [Deploy a hub-and-spoke network using VPC Network Peering](https://cloud.google.com/architecture/deploy-hub-spoke-network-using-peering) - [Deploy a hub-and-spoke network using Cloud VPN](https://cloud.google.com/architecture/deploy-hub-spoke-network-using-vpn) + +## Third-party reviews + +- [Google Cloud Landing Zone Comparison](https://www.meshcloud.io/2022/09/09/gcp-landing-zone-comparison/) by Meshcloud. diff --git a/assets/images/cloud-shell-button.png b/assets/images/cloud-shell-button.png new file mode 100644 index 0000000000..21a3f3de9d Binary files /dev/null and b/assets/images/cloud-shell-button.png differ diff --git a/assets/logos/fabric-logo-colors-gray-800.png b/assets/logos/fabric-logo-colors-gray-800.png new file mode 100644 index 0000000000..8b9e02f1bc Binary files /dev/null and b/assets/logos/fabric-logo-colors-gray-800.png differ diff --git a/assets/logos/fabric-logo-colors-gray.svg b/assets/logos/fabric-logo-colors-gray.svg new file mode 100644 index 0000000000..7d68c68978 --- /dev/null +++ b/assets/logos/fabric-logo-colors-gray.svg @@ -0,0 +1,223 @@ + + + + diff --git a/blueprints/README.md b/blueprints/README.md new file mode 100644 index 0000000000..83588bfea9 --- /dev/null +++ b/blueprints/README.md @@ -0,0 +1,16 @@ +# Terraform end-to-end blueprints for Google Cloud + +This section provides **[networking blueprints](./networking/)** that implement core patterns or features, **[data solutions blueprints](./data-solutions/)** that demonstrate how to integrate data services in complete scenarios, **[cloud operations blueprints](./cloud-operations/)** that leverage specific products to meet specific operational needs, **[GKE](./gke/)** and **[Serverless](./serverless/)** blueprints, and **[factories](./factories/)** that implement resource factories for the repetitive creation of specific resources. + +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) +- **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, [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) + +For more information see the individual README files in each section. diff --git a/blueprints/apigee/README.md b/blueprints/apigee/README.md new file mode 100644 index 0000000000..4cec9de9cf --- /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/). + +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)
| ✓ | |
+| [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
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [ip_address](outputs.tf#L17) | IP address. | |
+
+
diff --git a/blueprints/apigee/bigquery-analytics/diagram1.png b/blueprints/apigee/bigquery-analytics/diagram1.png
new file mode 100644
index 0000000000..863c1d8ef3
Binary files /dev/null and b/blueprints/apigee/bigquery-analytics/diagram1.png differ
diff --git a/blueprints/apigee/bigquery-analytics/diagram2.png b/blueprints/apigee/bigquery-analytics/diagram2.png
new file mode 100644
index 0000000000..2e8f03500f
Binary files /dev/null and b/blueprints/apigee/bigquery-analytics/diagram2.png differ
diff --git a/blueprints/apigee/bigquery-analytics/functions/export/index.js b/blueprints/apigee/bigquery-analytics/functions/export/index.js
new file mode 100644
index 0000000000..7176e38046
--- /dev/null
+++ b/blueprints/apigee/bigquery-analytics/functions/export/index.js
@@ -0,0 +1,103 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+const functions = require("@google-cloud/functions-framework");
+const superagent = require("superagent");
+const { LoggingBunyan } = require("@google-cloud/logging-bunyan");
+const bunyan = require("bunyan");
+const { GoogleAuth } = require("google-auth-library");
+
+const loggingBunyan = new LoggingBunyan();
+const logger = bunyan.createLogger({
+ name: "analyticsExport",
+ streams: [
+ { stream: process.stdout, level: "info" },
+ loggingBunyan.stream("info"),
+ ],
+});
+
+const ORGANIZATION = process.env.ORGANIZATION;
+const ENVIRONMENTS = process.env.ENVIRONMENTS.split(',');
+const DATASTORE = process.env.DATASTORE;
+
+const MANAGEMENT_API_URL = "https://apigee.googleapis.com/v1";
+
+function formatDate(date) {
+ var d = new Date(date),
+ month = "" + (d.getMonth() + 1),
+ day = "" + d.getDate(),
+ year = d.getFullYear();
+
+ if (month.length < 2) month = "0" + month;
+ if (day.length < 2) day = "0" + day;
+
+ return [year, month, day].join("-");
+}
+
+async function getAccessToken() {
+ logger.info("Requesting access token...");
+ const auth = new GoogleAuth();
+ const token = await auth.getAccessToken();
+ logger.info("Got access token ");
+ return token;
+}
+
+async function scheduleAnalyticsExport(org, env, token, startDate, endDate) {
+ logger.info(
+ `Sending request for an analytics export from ${startDate} to ${endDate} for environment ${env}`
+ );
+ try {
+ const response = await superagent
+ .post(
+ `${MANAGEMENT_API_URL}/organizations/${org}/environments/${env}/analytics/exports`
+ )
+ .send({
+ name: `Analytics from ${startDate} to ${endDate}`,
+ description: `Analytics from ${startDate} to ${endDate}`,
+ dateRange: {
+ start: startDate,
+ end: endDate,
+ },
+ outputFormat: "csv",
+ csvDelimiter: ",",
+ datastoreName: DATASTORE,
+ })
+ .set('Authorization', `Bearer ${token}`)
+ .accept('json');
+ logger.info('Analytics export scheduled');
+ return response;
+ } catch (error) {
+ logger.error('Error scheduling analytics export');
+ logger.error(error);
+ throw error;
+ }
+}
+
+functions.cloudEvent("export", async (cloudEvent) => {
+ const today = new Date();
+ const endDate = formatDate(today);
+ const yesterday = new Date(today.setDate(today.getDate() - 1));
+ const startDate = formatDate(yesterday);
+ const token = await getAccessToken();
+ try {
+ for(let i = 0; i < ENVIRONMENTS.length; i++) {
+ const env = ENVIRONMENTS[i];
+ const response = await scheduleAnalyticsExport(ORGANIZATION, env, token, startDate, endDate);
+ logger.error('Export scheduled: ' + response.body.self);
+ }
+ } catch (error) {
+ logger.error('Analytics exports was not scheduled');
+ logger.error(error);
+ }
+});
diff --git a/blueprints/apigee/bigquery-analytics/functions/export/package-lock.json b/blueprints/apigee/bigquery-analytics/functions/export/package-lock.json
new file mode 100644
index 0000000000..737005beea
--- /dev/null
+++ b/blueprints/apigee/bigquery-analytics/functions/export/package-lock.json
@@ -0,0 +1,5548 @@
+{
+ "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/apigee/bigquery-analytics/functions/export/package.json b/blueprints/apigee/bigquery-analytics/functions/export/package.json
new file mode 100644
index 0000000000..76f39edda0
--- /dev/null
+++ b/blueprints/apigee/bigquery-analytics/functions/export/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "export",
+ "version": "1.0.0",
+ "description": "Apigee analytics export to GCS",
+ "main": "index.js",
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "",
+ "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"
+ }
+}
diff --git a/blueprints/apigee/bigquery-analytics/functions/gcs2bq/index.js b/blueprints/apigee/bigquery-analytics/functions/gcs2bq/index.js
new file mode 100644
index 0000000000..71975a3dbe
--- /dev/null
+++ b/blueprints/apigee/bigquery-analytics/functions/gcs2bq/index.js
@@ -0,0 +1,76 @@
+/**
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ const functions = require("@google-cloud/functions-framework");
+ const { Storage } = require('@google-cloud/storage');
+ const { BigQuery } = require('@google-cloud/bigquery');
+ const bunyan = require('bunyan');
+ const schema = require('./schema.json');
+
+ const { LoggingBunyan } = require('@google-cloud/logging-bunyan');
+
+ const loggingBunyan = new LoggingBunyan();
+
+ const logger = bunyan.createLogger({
+ name: 'gcs2bq',
+ streams: [
+ { stream: process.stdout, level: 'info' },
+ loggingBunyan.stream('info')
+ ],
+ });
+
+ const DATASET = process.env.DATASET
+ const TABLE = process.env.TABLE
+ const LOCATION = process.env.LOCATION
+
+ async function loadCSVFromGCS(datasetId, tableId, timePartition, bucket, filename) {
+ const metadata = {
+ sourceFormat: 'CSV',
+ skipLeadingRows: 1,
+ maxBadRecords: 1000,
+ schema: {
+ fields: schema
+ },
+ location: LOCATION
+ };
+
+ logger.info(`Trying to load ${bucket}/${filename} in ${timePartition} time partition of table ${tableId}...`);
+ const bigquery = new BigQuery();
+ const storage = new Storage();
+ const [job] = await bigquery
+ .dataset(datasetId)
+ .table(`${tableId}\$${timePartition}`)
+ .load(storage.bucket(bucket).file(filename), metadata);
+ logger.info(`Job ${job.id} completed.`);
+ const errors = job.status.errors;
+ if (errors && errors.length > 0) {
+ logger.info('Errors occurred:' + JSON.stringify(errors));
+ throw new Error('File could not be loaded in time partition');
+ }
+ }
+
+ functions.cloudEvent("gcs2bq", async (cloudEvent) => {
+
+ const data = JSON.parse(Buffer.from(cloudEvent.data.message.data, 'base64').toString());
+ logger.info('Notification received');
+ logger.info(data);
+ const pattern = /([^/]+\/)?[0-9]{14}\-[0-9a-z]{8}\-[0-9a-z]{4}\-[0-9a-z]{4}\-[0-9a-z]{4}\-[0-9a-z]{12}\-api\-from\-([0-9]{8})0000\-to\-([0-9]{8})0000\/result\-[0-9]+\.csv\.gz/
+ const result = data.name.match(pattern);
+ const timePartition = result[2];
+
+ await loadCSVFromGCS(DATASET, TABLE, timePartition, data.bucket, data.name)
+
+ });
diff --git a/blueprints/apigee/bigquery-analytics/functions/gcs2bq/package-lock.json b/blueprints/apigee/bigquery-analytics/functions/gcs2bq/package-lock.json
new file mode 100644
index 0000000000..c5a7620b08
--- /dev/null
+++ b/blueprints/apigee/bigquery-analytics/functions/gcs2bq/package-lock.json
@@ -0,0 +1,5675 @@
+{
+ "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/apigee/bigquery-analytics/functions/gcs2bq/package.json b/blueprints/apigee/bigquery-analytics/functions/gcs2bq/package.json
new file mode 100644
index 0000000000..e346cbf747
--- /dev/null
+++ b/blueprints/apigee/bigquery-analytics/functions/gcs2bq/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "gcs2bq",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "",
+ "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"
+ }
+}
diff --git a/blueprints/apigee/bigquery-analytics/functions/gcs2bq/schema.json b/blueprints/apigee/bigquery-analytics/functions/gcs2bq/schema.json
new file mode 100644
index 0000000000..389ffad8b9
--- /dev/null
+++ b/blueprints/apigee/bigquery-analytics/functions/gcs2bq/schema.json
@@ -0,0 +1,447 @@
+[
+ {
+ "mode": "NULLABLE",
+ "name": "organization",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "environment",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "apiproxy",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "request_uri",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "proxy",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "proxy_basepath",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "request_verb",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "request_size",
+ "type": "INTEGER"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "response_status_code",
+ "type": "INTEGER"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "is_error",
+ "type": "INTEGER"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "client_received_start_timestamp",
+ "type": "TIMESTAMP"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "client_received_end_timestamp",
+ "type": "TIMESTAMP"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "target_sent_start_timestamp",
+ "type": "TIMESTAMP"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "target_sent_end_timestamp",
+ "type": "TIMESTAMP"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "target_received_start_timestamp",
+ "type": "TIMESTAMP"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "target_received_end_timestamp",
+ "type": "TIMESTAMP"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "client_sent_start_timestamp",
+ "type": "TIMESTAMP"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "client_sent_end_timestamp",
+ "type": "TIMESTAMP"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "client_ip",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "client_id",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "developer",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "developer_app",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "api_product",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "flow_resource",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "target",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "target_url",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "target_host",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "apiproxy_revision",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "proxy_pathsuffix",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "proxy_client_ip",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "target_basepath",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "target_ip",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "request_path",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "response_size",
+ "type": "INTEGER"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "developer_email",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "virtual_host",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "gateway_flow_id",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "message_count",
+ "type": "INTEGER"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "total_response_time",
+ "type": "INTEGER"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "request_processing_latency",
+ "type": "INTEGER"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "response_processing_latency",
+ "type": "INTEGER"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "target_response_time",
+ "type": "INTEGER"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "cache_hit",
+ "type": "INTEGER"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "x_forwarded_for_ip",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "useragent",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "target_response_code",
+ "type": "INTEGER"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "target_error",
+ "type": "INTEGER"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "policy_error",
+ "type": "INTEGER"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_created_time",
+ "type": "TIMESTAMP"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_ua_agent_type",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_ua_os_version",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_ua_os_family",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_ua_agent_version",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_ua_device_category",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_ua_agent_family",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "gateway_source",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_month_of_year",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_hour_of_day",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_week_of_month",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_day_of_week",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_cache_key",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_cache_l1_count",
+ "type": "INTEGER"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_cache_source",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_cache_executed",
+ "type": "INTEGER"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_cache_name",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_edge_execution_fault_code",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_edge_is_apigee_fault",
+ "type": "INTEGER"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_execution_fault_policy_name",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_execution_fault_flow_state",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_execution_fault_flow_name",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_dn_region",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_true_client_ip",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "x_apigee_edge_execution_stats_request_flow_end_timestamp",
+ "type": "TIMESTAMP"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "x_apigee_edge_execution_stats_request_flow_start_timestamp",
+ "type": "TIMESTAMP"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "x_apigee_edge_execution_stats_response_flow_end_timestamp",
+ "type": "TIMESTAMP"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "x_apigee_edge_execution_stats_response_flow_start_timestamp",
+ "type": "TIMESTAMP"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "x_apigee_edge_stats_steps",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "senseaction",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "ax_resolved_client_ip",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "x_forwarded_proto",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "x_apigee_intelligence_service",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "envgroup_hostname",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "x_apigee_mintng_rate",
+ "type": "NUMERIC"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "x_apigee_mintng_rate_plan_id",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "x_apigee_mintng_dev_share",
+ "type": "NUMERIC"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "x_apigee_mintng_currency",
+ "type": "STRING"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "x_apigee_mintng_price",
+ "type": "NUMERIC"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "x_apigee_mintng_tx_success",
+ "type": "BOOLEAN"
+ },
+ {
+ "mode": "NULLABLE",
+ "name": "x_apigee_mintng_price_multiplier",
+ "type": "NUMERIC"
+ }
+]
diff --git a/blueprints/apigee/bigquery-analytics/main.tf b/blueprints/apigee/bigquery-analytics/main.tf
new file mode 100644
index 0000000000..8ecca62a85
--- /dev/null
+++ b/blueprints/apigee/bigquery-analytics/main.tf
@@ -0,0 +1,296 @@
+/**
+ * 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 "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
+ project_create = var.project_create != null
+ services = [
+ "apigee.googleapis.com",
+ "bigquery.googleapis.com",
+ "cloudbuild.googleapis.com",
+ "cloudfunctions.googleapis.com",
+ "cloudscheduler.googleapis.com",
+ "logging.googleapis.com",
+ "compute.googleapis.com",
+ "pubsub.googleapis.com",
+ "servicenetworking.googleapis.com",
+ "storage.googleapis.com"
+ ]
+ iam = {
+ "roles/bigquery.jobUser" = [
+ module.function_gcs2bq.service_account_iam_email
+ ]
+ "roles/logging.logWriter" = [
+ module.function_export.service_account_iam_email
+ ]
+ "roles/logging.logWriter" = [
+ module.function_gcs2bq.service_account_iam_email
+ ]
+ "roles/apigee.admin" = [
+ module.function_export.service_account_iam_email
+ ]
+ "roles/storage.admin" = [
+ "serviceAccount:${module.project.service_accounts.robots.apigee}"
+ ]
+ }
+}
+
+module "vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = var.organization.authorized_network
+ vpc_create = var.vpc_create
+ subnets_psc = [for k, v in var.psc_config : {
+ ip_cidr_range = v
+ name = "subnet-psc-${k}"
+ region = k
+ }]
+ psa_config = {
+ ranges = {
+ for k, v in var.instances : "apigee-${k}" => v.psa_ip_cidr_range
+ }
+ }
+}
+
+module "apigee" {
+ source = "../../../modules/apigee"
+ project_id = module.project.project_id
+ organization = var.organization
+ envgroups = var.envgroups
+ environments = var.environments
+ instances = var.instances
+ depends_on = [
+ module.vpc
+ ]
+}
+
+module "glb" {
+ source = "../../../modules/net-glb"
+ name = "glb"
+ project_id = module.project.project_id
+ protocol = "HTTPS"
+ use_classic_version = false
+ backend_service_configs = {
+ default = {
+ backends = [for k, v in var.instances : { backend = k }]
+ protocol = "HTTPS"
+ health_checks = []
+ }
+ }
+ health_check_configs = {
+ default = {
+ https = { port_specification = "USE_SERVING_PORT" }
+ }
+ }
+ neg_configs = {
+ for k, v in var.instances : k => {
+ psc = {
+ region = v.region
+ target_service = module.apigee.instances[k].service_attachment
+ network = module.vpc.network.self_link
+ subnetwork = (
+ module.vpc.subnets_psc["${v.region}/subnet-psc-${v.region}"].self_link
+ )
+ }
+ }
+ }
+ ssl_certificates = {
+ managed_configs = {
+ default = {
+ domains = flatten([for k, v in var.envgroups : v])
+ }
+ }
+ }
+}
+
+module "pubsub_export" {
+ source = "../../../modules/pubsub"
+ project_id = module.project.project_id
+ name = "topic-export"
+}
+
+module "bucket_export" {
+ source = "../../../modules/gcs"
+ project_id = module.project.project_id
+ name = "${module.project.project_id}-export"
+ iam = {
+ "roles/storage.objectViewer" = [
+ module.function_gcs2bq.service_account_iam_email
+ ]
+ }
+ notification_config = {
+ enabled = true
+ payload_format = "JSON_API_V1"
+ sa_email = module.project.service_accounts.robots.storage
+ topic_name = "topic-gcs2bq"
+ event_types = ["OBJECT_FINALIZE"]
+ custom_attributes = {}
+ }
+}
+
+module "function_export" {
+ source = "../../../modules/cloud-function"
+ project_id = module.project.project_id
+ name = "export"
+ bucket_name = "${module.project.project_id}-code-export"
+ region = var.organization.analytics_region
+ ingress_settings = "ALLOW_INTERNAL_ONLY"
+ bucket_config = {
+ location = null
+ lifecycle_delete_age = 1
+ }
+ bundle_config = {
+ source_dir = "${path.module}/functions/export"
+ output_path = "${path.module}/bundle-export.zip"
+ excludes = null
+ }
+ function_config = {
+ entry_point = "export"
+ instances = null
+ memory = null
+ runtime = "nodejs16"
+ timeout = 180
+ }
+ environment_variables = {
+ ORGANIZATION = module.apigee.org_name,
+ ENVIRONMENTS = join(",", [for k, v in module.apigee.environments : k])
+ DATASTORE = var.datastore_name
+ }
+ trigger_config = {
+ event = "google.pubsub.topic.publish"
+ resource = module.pubsub_export.id
+ retry = null
+ }
+ service_account_create = true
+}
+
+module "function_gcs2bq" {
+ source = "../../../modules/cloud-function"
+ project_id = module.project.project_id
+ name = "gcs2bq"
+ bucket_name = "${module.project.project_id}-code-gcs2bq"
+ region = var.organization.analytics_region
+ ingress_settings = "ALLOW_INTERNAL_ONLY"
+ bucket_config = {
+ location = null
+ lifecycle_delete_age = 1
+ }
+ bundle_config = {
+ source_dir = "${path.module}/functions/gcs2bq"
+ output_path = "${path.module}/bundle-gcs2bq.zip"
+ excludes = null
+ }
+ function_config = {
+ entry_point = "gcs2bq"
+ instances = null
+ memory = null
+ runtime = "nodejs16"
+ timeout = 180
+ }
+ environment_variables = {
+ DATASET = module.bigquery_dataset.dataset_id
+ TABLE = module.bigquery_dataset.tables["analytics"].table_id
+ LOCATION = var.organization.analytics_region
+ }
+ trigger_config = {
+ event = "google.pubsub.topic.publish"
+ resource = module.bucket_export.topic
+ retry = null
+ }
+ service_account_create = true
+}
+
+module "bigquery_dataset" {
+ source = "../../../modules/bigquery-dataset"
+ project_id = module.project.project_id
+ id = "apigee"
+ location = var.organization.analytics_region
+ tables = {
+ analytics = {
+ friendly_name = "analytics"
+ labels = {}
+ options = null
+ partitioning = {
+ field = "client_received_end_timestamp"
+ range = null
+ time = { type = "DAY", expiration_ms = null }
+ }
+ schema = file("${path.module}/functions/gcs2bq/schema.json")
+ deletion_protection = false
+ }
+ }
+ iam = {
+ "roles/bigquery.dataEditor" = [
+ module.function_gcs2bq.service_account_iam_email
+ ]
+ }
+}
+
+resource "google_app_engine_application" "app" {
+ project = module.project.project_id
+ location_id = (
+ (
+ var.organization.analytics_region == "europe-west1" ||
+ var.organization.analytics_region == "us-central1"
+ )
+ ? substr(
+ var.organization.analytics_region, 0, length(var.organization.analytics_region) - 1
+ )
+ : var.organization.analytics_region
+ )
+}
+
+resource "google_cloud_scheduler_job" "job" {
+ name = "export"
+ schedule = "0 4 * * *"
+ time_zone = "Etc/UTC"
+ attempt_deadline = "320s"
+ project = module.project.project_id
+ region = var.organization.analytics_region
+ pubsub_target {
+ topic_name = module.pubsub_export.id
+ data = base64encode("test")
+ }
+}
+
+resource "local_file" "create_datastore_file" {
+ content = templatefile("${path.module}/templates/create-datastore.sh.tpl", {
+ org_name = module.apigee.org_name
+ project_id = module.project.project_id
+ datastore_name = var.datastore_name
+ bucket_name = module.bucket_export.name
+ path = var.path
+ })
+ filename = "${path.module}/create-datastore.sh"
+ file_permission = "0777"
+}
+
+resource "local_file" "deploy_apiproxy_file" {
+ content = templatefile("${path.module}/templates/deploy-apiproxy.sh.tpl", {
+ org_name = module.apigee.org_name
+ })
+ filename = "${path.module}/deploy-apiproxy.sh"
+ file_permission = "0777"
+}
diff --git a/blueprints/apigee/bigquery-analytics/outputs.tf b/blueprints/apigee/bigquery-analytics/outputs.tf
new file mode 100644
index 0000000000..58df8ac0f0
--- /dev/null
+++ b/blueprints/apigee/bigquery-analytics/outputs.tf
@@ -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.
+ */
+
+output "ip_address" {
+ description = "IP address."
+ value = module.glb.address
+}
diff --git a/blueprints/apigee/bigquery-analytics/package-lock.json b/blueprints/apigee/bigquery-analytics/package-lock.json
new file mode 100644
index 0000000000..97ea79948a
--- /dev/null
+++ b/blueprints/apigee/bigquery-analytics/package-lock.json
@@ -0,0 +1,251 @@
+{
+ "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/apigee/bigquery-analytics/send-requests.sh b/blueprints/apigee/bigquery-analytics/send-requests.sh
new file mode 100755
index 0000000000..b8908925fe
--- /dev/null
+++ b/blueprints/apigee/bigquery-analytics/send-requests.sh
@@ -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.
+
+#!/bin/bash
+
+if [ $# -lt 2 ]; then
+ echo "Usage: $0 ENVGROUP_HOSTNAME NUM_REQUESTS"
+ exit 1
+fi
+
+ENVGROUP_HOSTNAME=$1
+NUM_REQUESTS=$2
+
+for i in $(seq 1 $NUM_REQUESTS)
+do
+curl -v https://$ENVGROUP_HOSTNAME/httpbin/headers
+done
diff --git a/blueprints/apigee/bigquery-analytics/templates/create-datastore.sh.tpl b/blueprints/apigee/bigquery-analytics/templates/create-datastore.sh.tpl
new file mode 100644
index 0000000000..b163b97c7f
--- /dev/null
+++ b/blueprints/apigee/bigquery-analytics/templates/create-datastore.sh.tpl
@@ -0,0 +1,30 @@
+# 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
+
+curl "https://apigee.googleapis.com/v1/organizations/${org_name}/analytics/datastores" \
+-X POST \
+-H "Content-type:application/json" \
+-H "Authorization: Bearer $(gcloud auth print-access-token)" \
+-d \
+'{
+ "displayName": "${datastore_name}",
+ "targetType": "gcs",
+ "datastoreConfig": {
+ "projectId": "${project_id}",
+ "bucketName": "${bucket_name}",
+ "path": "${path}"
+ }
+}'
diff --git a/blueprints/apigee/bigquery-analytics/templates/deploy-apiproxy.sh.tpl b/blueprints/apigee/bigquery-analytics/templates/deploy-apiproxy.sh.tpl
new file mode 100644
index 0000000000..830898f9e6
--- /dev/null
+++ b/blueprints/apigee/bigquery-analytics/templates/deploy-apiproxy.sh.tpl
@@ -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.
+
+#!/bin/bash
+
+if [ $# -lt 1 ]; then
+ echo "Usage: $0 ENV_NAME"
+ exit 1
+fi
+
+ORG_NAME=${org_name}
+ENV_NAME=$1
+
+wget https://github.com/apigee/api-platform-samples/raw/master/sample-proxies/apigee-quickstart/httpbin_rev1_2020_02_02.zip -O apiproxy.zip
+
+export TOKEN=$(gcloud auth print-access-token)
+
+curl -v -X POST \
+-H "Authorization: Bearer $TOKEN" \
+-H "Content-Type:application/octet-stream" \
+-T 'apiproxy.zip' \
+"https://apigee.googleapis.com/v1/organizations/$ORG_NAME/apis?name=httpbin&action=import"
+
+curl -v -X POST \
+-H "Authorization: Bearer $TOKEN" \
+"https://apigee.googleapis.com/v1/organizations/$ORG_NAME/environments/$ENV_NAME/apis/httpbin/revisions/1/deployments"
+
+curl -v \
+-H "Authorization: Bearer $TOKEN" \
+"https://apigee.googleapis.com/v1/organizations/$ORG_NAME/environments/$ENV_NAME/apis/httpbin/revisions/1/deployments"
diff --git a/blueprints/apigee/bigquery-analytics/terraform.tfvars.sample b/blueprints/apigee/bigquery-analytics/terraform.tfvars.sample
new file mode 100644
index 0000000000..db4213210f
--- /dev/null
+++ b/blueprints/apigee/bigquery-analytics/terraform.tfvars.sample
@@ -0,0 +1,23 @@
+project_create = {
+ billing_account_id = "12345-12345-123456"
+ parent = "folders/123456789"
+}
+project_id = "my-project"
+envgroups = {
+ test = ["test.myorg.org"]
+}
+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/apigee/bigquery-analytics/variables.tf b/blueprints/apigee/bigquery-analytics/variables.tf
new file mode 100644
index 0000000000..ba7f5d78ae
--- /dev/null
+++ b/blueprints/apigee/bigquery-analytics/variables.tf
@@ -0,0 +1,107 @@
+/**
+ * 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 "datastore_name" {
+ description = "Datastore."
+ type = string
+ nullable = false
+ default = "gcs"
+}
+
+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 "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 "path" {
+ description = "Bucket path."
+ type = string
+ default = "/analytics"
+ nullable = false
+}
+
+variable "project_create" {
+ description = "Parameters for the creation of the new project."
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = null
+}
+
+variable "project_id" {
+ description = "Project ID."
+ type = string
+ nullable = false
+}
+
+variable "psc_config" {
+ description = "PSC configuration."
+ type = map(string)
+ nullable = false
+}
+
+variable "vpc_create" {
+ description = "Boolean flag indicating whether the VPC should be created or not."
+ type = bool
+ default = true
+}
diff --git a/blueprints/apigee/bigquery-analytics/versions.tf b/blueprints/apigee/bigquery-analytics/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/apigee/bigquery-analytics/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/apigee/hybrid-gke/README.md b/blueprints/apigee/hybrid-gke/README.md
new file mode 100644
index 0000000000..cee4aec1a9
--- /dev/null
+++ b/blueprints/apigee/hybrid-gke/README.md
@@ -0,0 +1,59 @@
+# 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
+ ```
+
+## Testing the blueprint
+
+2. Deploy an api proxy
+
+ ```
+ ./deploy-apiproxy.sh
+ ```
+
+3. In the console check the IP address that has been allocated to the Apigee ingress gateway and send some traffic to the deployed API proxy.
+
+ ```
+ curl -k -v -H "Host:HOSTNAME" \
+ --resolve HOSTNAME:443:IP_ADDRESS \
+ 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"
|
+
+
diff --git a/blueprints/apigee/hybrid-gke/ansible.tf b/blueprints/apigee/hybrid-gke/ansible.tf
new file mode 100644
index 0000000000..e5a491a3c5
--- /dev/null
+++ b/blueprints/apigee/hybrid-gke/ansible.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 Ansible generated files.
+
+resource "local_file" "vars_file" {
+ content = yamlencode({
+ cluster = module.cluster.name
+ region = var.region
+ project_id = module.project.project_id
+ envgroup = local.envgroup
+ env = local.environment
+ hostname = var.hostname
+ })
+ 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 0000000000..654f1729dc
--- /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 0000000000..842da83f43
--- /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 0000000000..1daa4d86a2
--- /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/main.yaml b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/main.yaml
new file mode 100644
index 0000000000..4b72039b8a
--- /dev/null
+++ b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/main.yaml
@@ -0,0 +1,143 @@
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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: Install cert-manager
+ shell: >
+ kubectl apply \
+ --validate=false \
+ -f https://github.com/jetstack/cert-manager/releases/download/v1.7.2/cert-manager.yaml
+
+- name: Wait until pods are ready in cert-manager namespace
+ shell: >
+ kubectl wait --for=condition=ready pods \
+ -l app.kubernetes.io/instance=cert-manager \
+ -n cert-manager \
+ --timeout=90s
+
+- name: 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 service accounts
+ shell: >
+ ~/hybrid-files/tools/create-service-account -i {{ project_id }} -e non-prod -d ~/hybrid-files/service-accounts
+
+- name: Create certificates
+ shell: >
+ openssl req \
+ -nodes \
+ -new \
+ -x509 \
+ -keyout ~/hybrid-files/certs/{{ envgroup }}.key \
+ -out ~/hybrid-files/certs/{{ envgroup }}.cert -subj '/CN='{{ hostname }}'' -days 3650
+
+- 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-non-prod@{{ 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 until pods are ready in apigee-system namespace
+ shell: >
+ kubectl wait --for=condition=ready pods \
+ -l app=apigee-controller \
+ -n apigee-system \
+ --timeout=300s
+
+- name: Wait until pods are ready in apigee namespace
+ shell: >
+ kubectl wait --for=condition=ready pods \
+ -l app=apigee-ingressgateway-manager \
+ -n apigee \
+ --timeout=300s
+
+- 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: Check status of the deployment
+ shell: >
+ while [ -n "$(kubectl get pods -n apigee | tail -n +2 | grep -v Running | grep -v Completed)" ]; do sleep 1; done
+ args:
+ chdir: ~/hybrid-files
\ No newline at end of file
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 0000000000..1c2c09ed8a
--- /dev/null
+++ b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/templates/overrides.yaml.j2
@@ -0,0 +1,63 @@
+gcp:
+ region: {{ region }}
+ projectID: {{ project_id }}
+
+k8sCluster:
+ name: {{ cluster }}
+ region: CLUSTER_LOCATION # Must be the closest Google Cloud region to your cluster.
+org: {{ project_id }}
+
+instanceID: "instance-1"
+
+cassandra:
+ hostNetwork: false
+ # Set to false for single region installations and multi-region installations
+ # with connectivity between pods in different clusters, for example GKE installations.
+ # Set to true for multi-region installations with no communication between
+ # pods in different clusters, for example GKE On-prem, GKE on AWS, Anthos on bare metal,
+ # AKS, EKS, and OpenShift installations.
+ # See Multi-region deployment: Prerequisites
+
+virtualhosts:
+ - name: {{ envgroup }}
+ selector:
+ app: apigee-ingressgateway
+ sslCertPath: ./certs/{{ envgroup }}.cert
+ sslKeyPath: ./certs/{{ envgroup }}.key
+
+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:
+ - name: {{ env }}
+ serviceAccountPaths:
+ synchronizer: ./service-accounts/{{ project_id }}-apigee-non-prod.json
+ udca: ./service-accounts/{{ project_id }}-apigee-non-prod.json
+ runtime: ./service-accounts/{{ project_id }}-apigee-non-prod.json
+
+mart:
+ serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json
+
+connectAgent:
+ serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json
+
+metrics:
+ serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json
+
+udca:
+ serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json
+
+watcher:
+ serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json
+
+logger:
+ enabled: true
+ serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json
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 0000000000..b438a63423
--- /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 0000000000..e3dc6b2e6c
--- /dev/null
+++ b/blueprints/apigee/hybrid-gke/apigee.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.
+ */
+
+locals {
+ envgroup = "test"
+ environment = "apis-test"
+}
+
+module "apigee" {
+ source = "../../../modules/apigee"
+ project_id = module.project.project_id
+ organization = {
+ analytics_region = var.region
+ runtime_type = "HYBRID"
+ }
+ envgroups = {
+ (local.envgroup) = [var.hostname]
+ }
+ environments = {
+ (local.environment) = {
+ envgroups = [local.envgroup]
+ }
+ }
+}
+
+resource "local_file" "deploy_apiproxy_file" {
+ content = templatefile("${path.module}/templates/deploy-apiproxy.sh.tpl", {
+ org = module.project.project_id
+ env = local.environment
+ })
+ filename = "${path.module}/deploy-apiproxy.sh"
+ file_permission = "0777"
+}
diff --git a/blueprints/apigee/hybrid-gke/diagram.png b/blueprints/apigee/hybrid-gke/diagram.png
new file mode 100644
index 0000000000..6d5c2d6bc9
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 0000000000..22cf06fab9
--- /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/blueprints/apigee/hybrid-gke/main.tf b/blueprints/apigee/hybrid-gke/main.tf
new file mode 100644
index 0000000000..5f1a676b2d
--- /dev/null
+++ b/blueprints/apigee/hybrid-gke/main.tf
@@ -0,0 +1,44 @@
+/**
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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]
+ }
+}
\ No newline at end of file
diff --git a/blueprints/apigee/hybrid-gke/mgmt.tf b/blueprints/apigee/hybrid-gke/mgmt.tf
new file mode 100644
index 0000000000..f51975f5f7
--- /dev/null
+++ b/blueprints/apigee/hybrid-gke/mgmt.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 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
+ }
+}
\ No newline at end of file
diff --git a/blueprints/apigee/hybrid-gke/templates/deploy-apiproxy.sh.tpl b/blueprints/apigee/hybrid-gke/templates/deploy-apiproxy.sh.tpl
new file mode 100644
index 0000000000..86647026d6
--- /dev/null
+++ b/blueprints/apigee/hybrid-gke/templates/deploy-apiproxy.sh.tpl
@@ -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.
+
+#!/bin/bash
+
+ORG_NAME=${org}
+ENV_NAME=${env}
+
+wget https://github.com/apigee/api-platform-samples/raw/master/sample-proxies/apigee-quickstart/httpbin_rev1_2020_02_02.zip -O apiproxy.zip
+
+export TOKEN=$(gcloud auth print-access-token)
+
+curl -v -X POST \
+-H "Authorization: Bearer $TOKEN" \
+-H "Content-Type:application/octet-stream" \
+-T 'apiproxy.zip' \
+"https://apigee.googleapis.com/v1/organizations/$ORG_NAME/apis?name=httpbin&action=import"
+
+curl -v -X POST \
+-H "Authorization: Bearer $TOKEN" \
+"https://apigee.googleapis.com/v1/organizations/$ORG_NAME/environments/$ENV_NAME/apis/httpbin/revisions/1/deployments"
+
+curl -v \
+-H "Authorization: Bearer $TOKEN" \
+"https://apigee.googleapis.com/v1/organizations/$ORG_NAME/environments/$ENV_NAME/apis/httpbin/revisions/1/deployments"
diff --git a/blueprints/apigee/hybrid-gke/templates/gssh.sh.tpl b/blueprints/apigee/hybrid-gke/templates/gssh.sh.tpl
new file mode 100644
index 0000000000..b366231d48
--- /dev/null
+++ b/blueprints/apigee/hybrid-gke/templates/gssh.sh.tpl
@@ -0,0 +1,30 @@
+#!/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
+#
+# 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.
+
+host="$${@: -2: 1}"
+cmd="$${@: -1: 1}"
+
+gcloud_args="
+--tunnel-through-iap
+--zone=${zone}
+--project=${project_id}
+--quiet
+--no-user-output-enabled
+--
+-C
+"
+
+exec gcloud compute ssh "$host" $gcloud_args "$cmd"
\ No newline at end of file
diff --git a/blueprints/apigee/hybrid-gke/terraform.tfvars.sample b/blueprints/apigee/hybrid-gke/terraform.tfvars.sample
new file mode 100644
index 0000000000..c5d1b4f356
--- /dev/null
+++ b/blueprints/apigee/hybrid-gke/terraform.tfvars.sample
@@ -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/blueprints/apigee/hybrid-gke/variables.tf b/blueprints/apigee/hybrid-gke/variables.tf
new file mode 100644
index 0000000000..a5a8e8f343
--- /dev/null
+++ b/blueprints/apigee/hybrid-gke/variables.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.
+ */
+
+variable "cluster_machine_type" {
+ description = "Cluster nachine type."
+ type = string
+ default = "e2-standard-4"
+}
+
+variable "cluster_network_config" {
+ description = "Cluster network configuration."
+ type = object({
+ nodes_cidr_block = string
+ pods_cidr_block = string
+ services_cidr_block = string
+ master_authorized_cidr_blocks = map(string)
+ master_cidr_block = string
+ })
+ default = {
+ nodes_cidr_block = "10.0.1.0/24"
+ pods_cidr_block = "172.16.0.0/20"
+ services_cidr_block = "192.168.0.0/24"
+ master_authorized_cidr_blocks = {
+ internal = "10.0.0.0/8"
+ }
+ master_cidr_block = "10.0.0.0/28"
+ }
+}
+
+variable "hostname" {
+ description = "Host name."
+ type = string
+}
+
+variable "mgmt_server_config" {
+ description = "Mgmt server configuration."
+ type = object({
+ disk_size = number
+ disk_type = string
+ image = string
+ instance_type = string
+ })
+ default = {
+ disk_size = 50
+ disk_type = "pd-ssd"
+ image = "projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts"
+ instance_type = "n1-standard-2"
+ }
+}
+
+variable "mgmt_subnet_cidr_block" {
+ description = "Management subnet CIDR block."
+ type = string
+ default = "10.0.2.0/28"
+}
+
+variable "project_create" {
+ description = "Parameters for the creation of the new project."
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = null
+}
+
+variable "project_id" {
+ description = "Project ID."
+ 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/apigee/hybrid-gke/vpc.tf b/blueprints/apigee/hybrid-gke/vpc.tf
new file mode 100644
index 0000000000..5424471cca
--- /dev/null
+++ b/blueprints/apigee/hybrid-gke/vpc.tf
@@ -0,0 +1,63 @@
+/**
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 "vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = "vpc"
+ subnets = [
+ {
+ ip_cidr_range = var.cluster_network_config.nodes_cidr_block
+ name = "subnet-apigee"
+ region = var.region
+ secondary_ip_ranges = {
+ pods = var.cluster_network_config.pods_cidr_block
+ services = var.cluster_network_config.services_cidr_block
+ }
+ },
+ {
+ ip_cidr_range = var.mgmt_subnet_cidr_block
+ name = "subnet-mgmt"
+ region = var.region
+ }
+ ]
+}
+
+module "firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.project.project_id
+ network = module.vpc.name
+ ingress_rules = {
+ # implicit allow action
+ allow-cp = {
+ description = "Allow control plane access to pods."
+ targets = ["node"]
+ rules = [{
+ protocol = "tcp"
+ ports = [15017, 9443]
+ }]
+ }
+ }
+}
+
+module "nat" {
+ source = "../../../modules/net-cloudnat"
+ project_id = module.project.project_id
+ region = var.region
+ name = "nat"
+ router_create = true
+ router_network = module.vpc.name
+}
diff --git a/blueprints/apigee/network-patterns/README.md b/blueprints/apigee/network-patterns/README.md
new file mode 100644
index 0000000000..241c412d4c
--- /dev/null
+++ b/blueprints/apigee/network-patterns/README.md
@@ -0,0 +1,6 @@
+# Apigee X network patterns
+
+The blueprints in this folder 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](./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 0000000000..21bd9940bc
--- /dev/null
+++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/README.md
@@ -0,0 +1,68 @@
+# 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#L47) | Parameters for the creation of the new project. | string
| ✓ | |
+| [hostname](variables.tf#L52) | Host name. | string
| ✓ | |
+| [onprem_project_id](variables.tf#L57) | Project ID. | string
| ✓ | |
+| [parent](variables.tf#L75) | 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_psa_ip_cidr_range](variables.tf#L29) | Apigee PSA IP CIDR range. | string
| | "10.0.4.0/22"
|
+| [apigee_psc_subnet_ip_cidr_range](variables.tf#L35) | Subnet IP CIDR range. | string
| | "10.2.2.0/24"
|
+| [apigee_subnet_ip_cidr_range](variables.tf#L41) | Subnet IP CIDR range. | string
| | "10.2.0.0/24"
|
+| [onprem_proxy_only_subnet_ip_cidr_range](variables.tf#L63) | Subnet IP CIDR range. | string
| | "10.1.1.0/24"
|
+| [onprem_subnet_ip_cidr_range](variables.tf#L69) | Subnet IP CIDR range. | string
| | "10.1.0.0/24"
|
+| [region](variables.tf#L80) | Region. | string
| | "europe-west1"
|
+| [zone](variables.tf#L86) | 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 0000000000..0e4faabfb5
--- /dev/null
+++ b/blueprints/apigee/network-patterns/nb-glb-psc-neg-sb-psc-ilbl7-hybrid-neg/apigee.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.
+ */
+
+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" = var.apigee_psa_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]
+ psa_ip_cidr_range = var.apigee_psa_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 0000000000..b568da9a05
--- /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 0000000000..e6df149b2c
--- /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 0000000000..a94b11eece
--- /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 0000000000..a277b3cda5
--- /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 @@
+
+string
| ✓ | |
+| [adfs_dns_domain_name](variables.tf#L26) | ADFS DNS domain name. | string
| ✓ | |
+| [prefix](variables.tf#L64) | Prefix used for resource names. | string
| ✓ | |
+| [project_id](variables.tf#L82) | Host project ID. | string
| ✓ | |
+| [ad_ip_cidr_block](variables.tf#L20) | Managed AD IP CIDR block. | string
| | "10.0.0.0/24"
|
+| [disk_size](variables.tf#L31) | Disk size. | number
| | 50
|
+| [disk_type](variables.tf#L37) | Disk type. | string
| | "pd-ssd"
|
+| [image](variables.tf#L43) | Image. | string
| | "projects/windows-cloud/global/images/family/windows-2022"
|
+| [instance_type](variables.tf#L49) | Instance type. | string
| | "n1-standard-2"
|
+| [network_config](variables.tf#L55) | Network configuration. | object({…})
| | null
|
+| [project_create](variables.tf#L73) | Parameters for the creation of the new project. | object({…})
| | null
|
+| [region](variables.tf#L87) | Region. | string
| | "europe-west1"
|
+| [subnet_ip_cidr_block](variables.tf#L93) | Subnet IP CIDR block. | string
| | "10.0.1.0/28"
|
+| [zone](variables.tf#L99) | Zone. | string
| | "europe-west1-c"
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [ip_address](outputs.tf#L15) | IP address. | |
+
+
diff --git a/blueprints/cloud-operations/adfs/ansible/ansible.cfg b/blueprints/cloud-operations/adfs/ansible/ansible.cfg
new file mode 100644
index 0000000000..e822a18f59
--- /dev/null
+++ b/blueprints/cloud-operations/adfs/ansible/ansible.cfg
@@ -0,0 +1,8 @@
+[defaults]
+inventory = inventory/hosts.ini
+
+[ssh_connection]
+pipelining = True
+ssh_executable = ./gssh.sh
+transfer_method = piped
+
diff --git a/blueprints/cloud-operations/adfs/ansible/inventory/hosts.ini b/blueprints/cloud-operations/adfs/ansible/inventory/hosts.ini
new file mode 100644
index 0000000000..bef015937d
--- /dev/null
+++ b/blueprints/cloud-operations/adfs/ansible/inventory/hosts.ini
@@ -0,0 +1 @@
+adfs ansible_connection=ssh ansible_shell_type=powershell
\ No newline at end of file
diff --git a/blueprints/cloud-operations/adfs/ansible/playbook.yaml b/blueprints/cloud-operations/adfs/ansible/playbook.yaml
new file mode 100644
index 0000000000..9b2db5ab39
--- /dev/null
+++ b/blueprints/cloud-operations/adfs/ansible/playbook.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
+#
+# 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.
+
+- name: Prepare
+ hosts: adfs
+ gather_facts: no
+ vars_files:
+ - vars/vars.yaml
+ roles:
+ - role: server-setup
+
+- name: Provision organizational units users, groups and memberships
+ hosts: adfs
+ gather_facts: no
+ vars_files:
+ - vars/vars.yaml
+ vars:
+ ansible_become: yes
+ ansible_become_method: runas
+ ansible_become_user: "SetupAdmin@{{ ad_dns_domain_name }}"
+ ansible_become_password: "{{ setupadmin_password }}"
+ roles:
+ - role: ad-provisioning
+
+- name: Install AD FS
+ hosts: adfs
+ gather_facts: no
+ vars_files:
+ - vars/vars.yaml
+ vars:
+ ansible_become: yes
+ ansible_become_method: runas
+ adfssvc_password: "{{ lookup('ansible.builtin.password', '~/.adfssvc-password.txt chars=ascii_letters,digits') }}"
+ roles:
+ - role: adfs-prerequisites
+ vars:
+ ansible_become_user: "SetupAdmin@{{ ad_dns_domain_name }}"
+ ansible_become_password: "{{ setupadmin_password }}"
+ - role: adfs-installation
+ vars:
+ ansible_become_user: "adfssvc@{{ ad_dns_domain_name }}"
+ ansible_become_password: "{{ adfssvc_password }}"
\ No newline at end of file
diff --git a/blueprints/cloud-operations/adfs/ansible/roles/ad-provisioning/files/groups.json b/blueprints/cloud-operations/adfs/ansible/roles/ad-provisioning/files/groups.json
new file mode 100644
index 0000000000..5ba88d2088
--- /dev/null
+++ b/blueprints/cloud-operations/adfs/ansible/roles/ad-provisioning/files/groups.json
@@ -0,0 +1,8 @@
+[
+ "gcp-billing-admins",
+ "gcp-devops",
+ "gcp-network-admins",
+ "gcp-organization-admins",
+ "gcp-security-admins",
+ "gcp-support"
+]
\ No newline at end of file
diff --git a/blueprints/cloud-operations/adfs/ansible/roles/ad-provisioning/files/memberships.json b/blueprints/cloud-operations/adfs/ansible/roles/ad-provisioning/files/memberships.json
new file mode 100644
index 0000000000..38d26253d6
--- /dev/null
+++ b/blueprints/cloud-operations/adfs/ansible/roles/ad-provisioning/files/memberships.json
@@ -0,0 +1,82 @@
+[
+ {
+ "group": "gcp-devops",
+ "member": "pamela.reed"
+ },
+ {
+ "group": "gcp-devops",
+ "member": "joshua.banks"
+ },
+ {
+ "group": "gcp-devops",
+ "member": "clayton.espinoza"
+ },
+ {
+ "group": "gcp-devops",
+ "member": "maureen.morgan"
+ },
+ {
+ "group": "gcp-network-admins",
+ "member": "pamela.reed"
+ },
+ {
+ "group": "gcp-network-admins",
+ "member": "william.bowen"
+ },
+ {
+ "group": "gcp-network-admins",
+ "member": "clayton.espinoza"
+ },
+ {
+ "group": "gcp-network-admins",
+ "member": "stacy.holland"
+ },
+ {
+ "group": "gcp-network-admins",
+ "member": "joshua.banks"
+ },
+ {
+ "group": "gcp-network-admins",
+ "member": "charlene.mckenzie"
+ },
+ {
+ "group": "gcp-network-admins",
+ "member": "lisa.harris"
+ },
+ {
+ "group": "gcp-organization-admins",
+ "member": "maureen.morgan"
+ },
+ {
+ "group": "gcp-organization-admins",
+ "member": "pamela.reed"
+ },
+ {
+ "group": "gcp-support",
+ "member": "maureen.morgan"
+ },
+ {
+ "group": "gcp-support",
+ "member": "pamela.reed"
+ },
+ {
+ "group": "gcp-support",
+ "member": "lisa.harris"
+ },
+ {
+ "group": "gcp-support",
+ "member": "tina.ferguson"
+ },
+ {
+ "group": "gcp-support",
+ "member": "stacy.holland"
+ },
+ {
+ "group": "gcp-support",
+ "member": "william.bowen"
+ },
+ {
+ "group": "gcp-support",
+ "member": "clayton.espinoza"
+ }
+]
\ No newline at end of file
diff --git a/blueprints/cloud-operations/adfs/ansible/roles/ad-provisioning/files/users.json b/blueprints/cloud-operations/adfs/ansible/roles/ad-provisioning/files/users.json
new file mode 100644
index 0000000000..f11f9fa0e5
--- /dev/null
+++ b/blueprints/cloud-operations/adfs/ansible/roles/ad-provisioning/files/users.json
@@ -0,0 +1,56 @@
+[
+ {
+ "first_name": "Pamela",
+ "last_name": "Reed",
+ "username": "pamela.reed",
+ "password": "Ig_17BbZVu"
+ },
+ {
+ "first_name": "Charlene",
+ "last_name": "Mckenzie",
+ "username": "charlene.mckenzie",
+ "password": "$y0IsMLPy5"
+ },
+ {
+ "first_name": "William",
+ "last_name": "Bowen",
+ "username": "william.bowen",
+ "password": "y882QxMHE@"
+ },
+ {
+ "first_name": "Joshua",
+ "last_name": "Banks",
+ "username": "joshua.banks",
+ "password": ")00+LN!r0$"
+ },
+ {
+ "first_name": "Clayton",
+ "last_name": "Espinoza",
+ "username": "clayton.espinoza",
+ "password": "gIf@52FqUY"
+ },
+ {
+ "first_name": "Stacy",
+ "last_name": "Holland",
+ "username": "stacy.holland",
+ "password": "da4PLSQDb^"
+ },
+ {
+ "first_name": "Maureen",
+ "last_name": "Morgan",
+ "username": "maureen.morgan",
+ "password": "V)c2Vfc%i#"
+ },
+ {
+ "first_name": "Lisa",
+ "last_name": "Harris",
+ "username": "lisa.harris",
+ "password": "0@1Oid71co"
+ },
+ {
+ "first_name": "Tina",
+ "last_name": "Ferguson",
+ "username": "tina.ferguson",
+ "password": "+f#0C#_oi6"
+ }
+]
\ No newline at end of file
diff --git a/blueprints/cloud-operations/adfs/ansible/roles/ad-provisioning/tasks/main.yaml b/blueprints/cloud-operations/adfs/ansible/roles/ad-provisioning/tasks/main.yaml
new file mode 100644
index 0000000000..f95bc7f0cc
--- /dev/null
+++ b/blueprints/cloud-operations/adfs/ansible/roles/ad-provisioning/tasks/main.yaml
@@ -0,0 +1,58 @@
+# 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.
+
+- name: Read files
+ set_fact:
+ ad_users: "{{ lookup('file','users.json') | from_json }}"
+ ad_groups: "{{ lookup('file','groups.json') | from_json }}"
+ ad_memberships: "{{ lookup('file','memberships.json') | from_json }}"
+
+- name: Create organizational units
+ community.windows.win_domain_ou:
+ name: "{{ item }}"
+ path: "{{ cloud_path }}"
+ state: present
+ protected: true
+ with_items:
+ - "Users"
+ - "Groups"
+
+- name: Create users
+ community.windows.win_domain_user:
+ name: "{{ item.username }}"
+ firstname: "{{ item.first_name }}"
+ surname: "{{ item.last_name }}"
+ email: "{{ item.username }}@{{ ad_dns_domain_name }}"
+ sam_account_name: "{{ item.username }}"
+ upn: "{{ item.username }}@{{ ad_dns_domain_name }}"
+ password: "{{ item.password }}"
+ path: "OU=Users,{{ cloud_path }}"
+ state: present
+ with_items: "{{ ad_users }}"
+
+- name: Create groups
+ community.windows.win_domain_group:
+ name: "{{ item }}"
+ path: "OU=Groups,{{ cloud_path }}"
+ scope: global
+ state: present
+ with_items: "{{ ad_groups }}"
+
+- name: Create memberships
+ community.windows.win_domain_group_membership:
+ name: "{{ item.group }}"
+ members:
+ - "{{ item.member }}"
+ state: present
+ with_items: "{{ ad_memberships }}"
\ No newline at end of file
diff --git a/blueprints/cloud-operations/adfs/ansible/roles/adfs-installation/tasks/main.yaml b/blueprints/cloud-operations/adfs/ansible/roles/adfs-installation/tasks/main.yaml
new file mode 100644
index 0000000000..ccbe99d201
--- /dev/null
+++ b/blueprints/cloud-operations/adfs/ansible/roles/adfs-installation/tasks/main.yaml
@@ -0,0 +1,104 @@
+# 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.
+
+- name: Create server certificate
+ ansible.windows.win_powershell:
+ script: |
+ $Certificate = Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object {$_.Subject -eq "CN={{ adfs_dns_domain_name }}"}
+ if(-not $Certificate) {
+ $Certificate = New-SelfSignedCertificate `
+ -Subject {{ adfs_dns_domain_name }} `
+ -KeyAlgorithm RSA `
+ -KeyLength 2048 `
+ -KeyExportPolicy NonExportable `
+ -KeyUsage DigitalSignature, KeyEncipherment `
+ -Provider 'Microsoft Platform Crypto Provider' `
+ -NotAfter (Get-Date).AddDays(365) `
+ -Type SSLServerAuthentication `
+ -CertStoreLocation 'Cert:\LocalMachine\My' `
+ -DnsName {{ adfs_dns_domain_name }}
+ }
+ $Certificate.Thumbprint
+ register: server_cert
+
+- name: Create token signing certificate
+ ansible.windows.win_powershell:
+ script: |
+ $Certificate = Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object {$_.Subject -eq "CN=ADFS Signing"}
+ if(-not $Certificate) {
+ $Certificate = New-SelfSignedCertificate `
+ -Subject "ADFS Signing" `
+ -KeyAlgorithm RSA `
+ -KeyLength 2048 `
+ -KeyExportPolicy NonExportable `
+ -KeyUsage DigitalSignature, KeyEncipherment `
+ -Provider 'Microsoft RSA SChannel Cryptographic Provider' `
+ -NotAfter (Get-Date).AddDays(365) `
+ -DnsName {{ adfs_dns_domain_name }} `
+ -CertStoreLocation 'Cert:\LocalMachine\My'
+ }
+ $Certificate.Thumbprint
+ register: token_signing_cert
+
+- name: Create AD FS DKM container
+ ansible.windows.win_powershell:
+ script: |
+ $DkmContainer = Get-ADObject -LDAPFilter "(Objectclass=container)" -SearchBase "CN=ADFS Data,{{ cloud_path }}" -SearchScope 1
+ if(-not $DkmContainer) {
+ $DkmContainer.DistinguishedName
+ $Name = (New-Guid).Guid
+ $DkmContainer = New-ADObject `
+ -Name $Name `
+ -Type Container `
+ -Path "CN=ADFS Data,{{ cloud_path }}" `
+ -PassThru
+ }
+ $DkmContainer.DistinguishedName
+ register: adfs_dkm_container
+
+- name: Install ADFS
+ ansible.windows.win_powershell:
+ script: |
+ try {
+ $AdfsFarm = Get-AdfsFarmInformation
+ } catch [System.ServiceModel.EndpointNotFoundException] {
+ $AdfsCredential = New-Object `
+ -TypeName System.Management.Automation.PSCredential `
+ -ArgumentList "$env:userdomain\adfssvc", (ConvertTo-SecureString {{ adfssvc_password }} -AsPlainText -Force)
+ Install-ADFSFarm `
+ -CertificateThumbprint {{ server_cert.output[0] }} `
+ -SigningCertificateThumbprint {{ token_signing_cert.output[0] }} `
+ -DecryptionCertificateThumbprint {{ token_signing_cert.output[0] }}`
+ -FederationServiceName {{ adfs_dns_domain_name }} `
+ -ServiceAccountCredential $AdfsCredential `
+ -OverwriteConfiguration `
+ -AdminConfiguration @{"DKMContainerDn"="{{ adfs_dkm_container.output[0] }}"}
+ }
+ no_log: yes
+
+- name: Configure TLS
+ ansible.windows.win_powershell:
+ script: |
+ netsh http show sslcert ipport=0.0.0.0:443
+ if($LastExitCode -gt 0) {
+ netsh http add sslcert ipport=0.0.0.0:443 certhash={{ server_cert.output[0] }} appid="{5d89a20c-beab-4389-9447-324788eb944a}" certstorename=MY
+ }
+
+- name: Restart computer
+ ansible.windows.win_reboot:
+
+- name: Enable the Idp-Initiated Sign on page
+ ansible.windows.win_powershell:
+ script: |
+ Set-AdfsProperties -EnableIdpInitiatedSignonPage $true
\ No newline at end of file
diff --git a/blueprints/cloud-operations/adfs/ansible/roles/adfs-prerequisites/tasks/main.yaml b/blueprints/cloud-operations/adfs/ansible/roles/adfs-prerequisites/tasks/main.yaml
new file mode 100644
index 0000000000..eeb6e1fc39
--- /dev/null
+++ b/blueprints/cloud-operations/adfs/ansible/roles/adfs-prerequisites/tasks/main.yaml
@@ -0,0 +1,45 @@
+# 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.
+
+- name: Create AD FS service user
+ community.windows.win_domain_user:
+ name: "adfssvc"
+ password: "{{ adfssvc_password }}"
+ spn: "http/{{ adfs_dns_domain_name }}"
+ path: "OU=Users,{{ cloud_path }}"
+ state: present
+
+- name: Add AD FS service user to local Administrators group
+ ansible.windows.win_group_membership:
+ name: Administrators
+ members:
+ - "adfssvc@{{ ad_dns_domain_name }}"
+ state: present
+
+- name: Create AD FS Data container
+ ansible.windows.win_powershell:
+ script: |
+ try {
+ Get-ADObject -Identity "CN=ADFS Data,{{ cloud_path }}"
+ } catch [Microsoft.ActiveDirectory.Management.ADIdentityResolutionException] {
+ New-ADObject `
+ -Name "ADFS Data" `
+ -Type Container `
+ -Path "{{ cloud_path }}"
+ }
+
+- name: Grant the AD FS user full control on the container
+ ansible.windows.win_powershell:
+ script: |
+ dsacls.exe "CN=ADFS Data,{{ cloud_path }}" /G $env:userdomain\adfssvc:GA /I:T
\ No newline at end of file
diff --git a/blueprints/cloud-operations/adfs/ansible/roles/anthos/tasks/main.yaml b/blueprints/cloud-operations/adfs/ansible/roles/anthos/tasks/main.yaml
new file mode 100644
index 0000000000..4ca1d7f21a
--- /dev/null
+++ b/blueprints/cloud-operations/adfs/ansible/roles/anthos/tasks/main.yaml
@@ -0,0 +1,67 @@
+# 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.
+
+$ApplicationGroup = Get-AdfsApplicationGroup -Name Anthos
+
+$ApplicationGroupName = "Anthos"
+$ApplicationGroupIdentifier = (New-Guid).Guid
+New-AdfsApplicationGroup -Name $ApplicationGroupName `
+-ApplicationGroupIdentifier $ApplicationGroupIdentifier
+
+$ServerApplicationName = "$ApplicationGroupName Server App"
+$ServerApplicationIdentifier = (New-Guid).Guid
+$RelyingPartyTrustName = "Anthos"
+$RelyingPartyTrustIdentifier = (New-Guid).Guid
+$RedirectURI1 = "http://localhost:1025/callback"
+$RedirectURI2 = "https://console.cloud.google.com/kubernetes/oidc"
+
+$ADFSApp = Add-AdfsServerApplication -Name $ServerApplicationName `
+-ApplicationGroupIdentifier $ApplicationGroupIdentifier `
+-RedirectUri $RedirectURI1,$RedirectURI2 `
+-Identifier $ServerApplicationIdentifier `
+-GenerateClientSecret
+
+$IssuanceTransformRules = @'
+@RuleTemplate = "LdapClaims"
+@RuleName = "groups"
+c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"]
+=> issue(store = "Active Directory", types = ("http://schemas.xmlsoap.org/claims/Group"), query = ";tokenGroups(domainQualifiedName);{0}", param = c.Value);
+'@
+
+Add-AdfsRelyingPartyTrust -Name $RelyingPartyTrustName `
+-Identifier $RelyingPartyTrustIdentifier `
+-AccessControlPolicyName "Permit everyone" `
+-IssuanceTransformRules "$IssuanceTransformRules"
+
+Grant-ADFSApplicationPermission -ClientRoleIdentifier $ServerApplicationIdentifier `
+-ServerRoleIdentifier $RelyingPartyTrustIdentifier `
+-ScopeName "allatclaims", "openid"
+
+$ClientId = $ADFSApp.Identifier
+$ClientSecret = $ADFSApp.ClientSecret
+
+@"
+authentication:
+ oidc:
+ clientID: $ADFSApp.Identifier
+ clientSecret: $ADFSApp.ClientSecret
+ extraParams: resource=$RelyingPartyTrustIdentifier
+ group: groups
+ groupPrefix: ""
+ issuerURI: https://{{ adfs_dns_domain_name }}/adfs
+ kubectlRedirectURL: $RedirectURI1
+ scopes: openid
+ username: upn
+ usernamePrefix: ""
+"@
\ No newline at end of file
diff --git a/blueprints/cloud-operations/adfs/ansible/roles/server-setup/tasks/main.yaml b/blueprints/cloud-operations/adfs/ansible/roles/server-setup/tasks/main.yaml
new file mode 100644
index 0000000000..6b846f41a0
--- /dev/null
+++ b/blueprints/cloud-operations/adfs/ansible/roles/server-setup/tasks/main.yaml
@@ -0,0 +1,86 @@
+# 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.
+
+- name: Install Windows features
+ ansible.windows.win_feature:
+ name: "{{ item.feature }}"
+ include_mamangement_tools: "{{ item.include_management_tools }}"
+ state: present
+ with_items:
+ - { "feature": "RSAT-AD-Tools", "include_management_tools": false }
+ - { "feature": "GPMC", "include_management_tools": false }
+ - { "feature": "RSAT-DNS-Server", "include_management_tools": false }
+ - { "feature": "ADFS-Federation", "include_management_tools": true }
+ - { "feature": "RSAT-AD-PowerShell", "include_management_tools": false }
+ - { "feature": "RSAT-ADDS-Tools", "include_management_tools": false }
+
+- name: Check if SetupAdmin password has already been reset
+ stat:
+ path: ~/.setupadmin-password.txt
+ register: setupadmin_password_file_check
+ delegate_to: localhost
+
+- name: Set AD SetupAdmin password fact
+ set_fact:
+ setupadmin_password: "{{ lookup('file', '~/.setupadmin-password.txt') }}"
+ no_log: true
+ when: setupadmin_password_file_check.stat.exists
+ delegate_to: localhost
+
+- name: Reset AD deletegated admin password
+ shell: >
+ gcloud active-directory domains reset-admin-password {{ ad_dns_domain_name }}
+ --project={{ project_id }}
+ --quiet
+ --format "value(password)"
+ register: setupadmin_password_reset
+ no_log: yes
+ when: not setupadmin_password_file_check.stat.exists
+ delegate_to: localhost
+
+- name: Set AD SetupAdmin password fact
+ set_fact:
+ setupadmin_password: "{{ setupadmin_password_reset.stdout }}"
+ no_log: yes
+ when: not setupadmin_password_file_check.stat.exists
+
+- name: Creating a file setupadmin password
+ copy:
+ dest: ~/.setupadmin-password.txt
+ content: "{{ setupadmin_password }}"
+ when: not setupadmin_password_file_check.stat.exists
+ delegate_to: localhost
+
+- name: Add computer to domain
+ ansible.windows.win_domain_membership:
+ dns_domain_name: "{{ ad_dns_domain_name }}"
+ domain_admin_user: "SetupAdmin@{{ ad_dns_domain_name }}"
+ domain_admin_password: "{{ setupadmin_password }}"
+ state: domain
+ register: domain_state
+
+- name: Restart computer
+ ansible.windows.win_reboot:
+ when: domain_state.reboot_required
+
+- name: Get Domain info
+ community.windows.win_domain_object_info:
+ filter: ObjectClass -eq 'domain'
+ domain_username: "SetupAdmin@{{ ad_dns_domain_name }}"
+ domain_password: "{{ setupadmin_password }}"
+ register: ad_domain
+
+- name: Set facts
+ set_fact:
+ cloud_path: "OU=Cloud,{{ ad_domain.objects[0].DistinguishedName }}"
diff --git a/blueprints/cloud-operations/adfs/architecture.png b/blueprints/cloud-operations/adfs/architecture.png
new file mode 100644
index 0000000000..c5cca554e9
Binary files /dev/null and b/blueprints/cloud-operations/adfs/architecture.png differ
diff --git a/blueprints/cloud-operations/adfs/main.tf b/blueprints/cloud-operations/adfs/main.tf
new file mode 100644
index 0000000000..8948e49b7f
--- /dev/null
+++ b/blueprints/cloud-operations/adfs/main.tf
@@ -0,0 +1,141 @@
+# 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.
+
+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
+ prefix = var.project_create == null ? null : var.prefix
+ name = var.project_id
+ services = [
+ "compute.googleapis.com",
+ "dns.googleapis.com",
+ "managedidentities.googleapis.com"
+ ]
+}
+
+module "vpc" {
+ count = var.network_config == null ? 1 : 0
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = "${var.prefix}-vpc"
+ subnets = [
+ {
+ ip_cidr_range = var.subnet_ip_cidr_block
+ name = "subnet"
+ region = var.region
+ }
+ ]
+}
+
+resource "google_active_directory_domain" "ad_domain" {
+ project = module.project.project_id
+ domain_name = var.ad_dns_domain_name
+ locations = [var.region]
+ authorized_networks = [module.vpc[0].network.id]
+ reserved_ip_range = var.ad_ip_cidr_block
+}
+
+module "server" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project.project_id
+ zone = var.zone
+ name = "adfs"
+ instance_type = var.instance_type
+ network_interfaces = [{
+ network = var.network_config == null ? module.vpc[0].self_link : var.network_config.network
+ subnetwork = var.network_config == null ? module.vpc[0].subnet_self_links["${var.region}/subnet"] : var.network_config.subnet
+ }]
+ metadata = {
+ # Enables OpenSSH in the Windows instance
+ sysprep-specialize-script-cmd = "googet -noconfirm=true update && googet -noconfirm=true install google-compute-engine-ssh"
+ enable-windows-ssh = "TRUE"
+ # Set the default OpenSSH shell to Powershell
+ windows-startup-script-ps1 = <string
| ✓ | |
+| [bundle_path](variables.tf#L17) | Path used to write the intermediate Cloud Function code bundle. | string
| | "./bundle.zip"
|
+| [name](variables.tf#L23) | Arbitrary string used to name created resources. | string
| | "asset-feed"
|
+| [project_create](variables.tf#L29) | Create project instead of using an existing one. | bool
| | false
|
+| [region](variables.tf#L40) | Compute region used in the example. | string
| | "europe-west1"
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [cf_logs](outputs.tf#L17) | Cloud Function logs read command. | |
+| [subscription_pull](outputs.tf#L29) | Subscription pull command. | |
+| [tag_add](outputs.tf#L39) | Instance add tag command. | |
+| [tag_show](outputs.tf#L49) | Instance add tag command. | |
+
+
diff --git a/examples/cloud-operations/asset-inventory-feed-remediation/backend.tf.sample b/blueprints/cloud-operations/asset-inventory-feed-remediation/backend.tf.sample
similarity index 100%
rename from examples/cloud-operations/asset-inventory-feed-remediation/backend.tf.sample
rename to blueprints/cloud-operations/asset-inventory-feed-remediation/backend.tf.sample
diff --git a/examples/cloud-operations/asset-inventory-feed-remediation/cf/main.py b/blueprints/cloud-operations/asset-inventory-feed-remediation/cf/main.py
similarity index 100%
rename from examples/cloud-operations/asset-inventory-feed-remediation/cf/main.py
rename to blueprints/cloud-operations/asset-inventory-feed-remediation/cf/main.py
diff --git a/examples/cloud-operations/asset-inventory-feed-remediation/cf/requirements.txt b/blueprints/cloud-operations/asset-inventory-feed-remediation/cf/requirements.txt
similarity index 100%
rename from examples/cloud-operations/asset-inventory-feed-remediation/cf/requirements.txt
rename to blueprints/cloud-operations/asset-inventory-feed-remediation/cf/requirements.txt
diff --git a/examples/cloud-operations/asset-inventory-feed-remediation/cloud-shell-readme.txt b/blueprints/cloud-operations/asset-inventory-feed-remediation/cloud-shell-readme.txt
similarity index 100%
rename from examples/cloud-operations/asset-inventory-feed-remediation/cloud-shell-readme.txt
rename to blueprints/cloud-operations/asset-inventory-feed-remediation/cloud-shell-readme.txt
diff --git a/examples/cloud-operations/asset-inventory-feed-remediation/diagram.png b/blueprints/cloud-operations/asset-inventory-feed-remediation/diagram.png
similarity index 100%
rename from examples/cloud-operations/asset-inventory-feed-remediation/diagram.png
rename to blueprints/cloud-operations/asset-inventory-feed-remediation/diagram.png
diff --git a/blueprints/cloud-operations/asset-inventory-feed-remediation/main.tf b/blueprints/cloud-operations/asset-inventory-feed-remediation/main.tf
new file mode 100644
index 0000000000..6fc1948e99
--- /dev/null
+++ b/blueprints/cloud-operations/asset-inventory-feed-remediation/main.tf
@@ -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.
+ */
+
+locals {
+ role_id = "projects/${var.project_id}/roles/${local.role_name}"
+ role_name = "feeds_cf"
+}
+
+module "project" {
+ source = "../../../modules/project"
+ name = var.project_id
+ project_create = var.project_create
+ services = [
+ "cloudasset.googleapis.com",
+ "cloudbuild.googleapis.com",
+ "cloudfunctions.googleapis.com",
+ "compute.googleapis.com"
+ ]
+ custom_roles = {
+ (local.role_name) = [
+ "compute.instances.list",
+ "compute.instances.setTags",
+ "compute.zones.list",
+ "compute.zoneOperations.get",
+ "compute.zoneOperations.list"
+ ]
+ }
+ iam = {
+ (local.role_id) = [module.service-account.iam_email]
+ }
+}
+
+module "vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = var.name
+ subnets = [{
+ ip_cidr_range = "192.168.0.0/24"
+ name = "${var.name}-default"
+ region = var.region
+ }]
+}
+
+module "pubsub" {
+ source = "../../../modules/pubsub"
+ project_id = module.project.project_id
+ name = var.name
+ subscriptions = { "${var.name}-default" = null }
+ iam = {
+ "roles/pubsub.publisher" = [
+ "serviceAccount:${module.project.service_accounts.robots.cloudasset}"
+ ]
+ }
+}
+
+module "service-account" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.project.project_id
+ name = "${var.name}-cf"
+ # iam_project_roles = { (module.project.project_id) = [local.role_id] }
+}
+
+module "cf" {
+ source = "../../../modules/cloud-function"
+ project_id = module.project.project_id
+ name = var.name
+ bucket_name = "${var.name}-${random_pet.random.id}"
+ bucket_config = {
+ location = var.region
+ }
+ bundle_config = {
+ source_dir = "cf"
+ output_path = var.bundle_path
+ }
+ service_account = module.service-account.email
+ trigger_config = {
+ v1 = {
+ event = "google.pubsub.topic.publish"
+ resource = module.pubsub.topic.id
+ }
+ }
+}
+
+module "simple-vm-example" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project.project_id
+ zone = "${var.region}-b"
+ name = var.name
+ network_interfaces = [{
+ network = module.vpc.self_link
+ subnetwork = try(module.vpc.subnet_self_links["${var.region}/${var.name}-default"], "")
+ }]
+ tags = ["${var.project_id}-test-feed", "shared-test-feed"]
+}
+
+resource "random_pet" "random" {
+ length = 1
+}
+
+# Create a feed that sends notifications about instance updates.
+resource "google_cloud_asset_project_feed" "project_feed" {
+ project = module.project.project_id
+ feed_id = var.name
+ content_type = "RESOURCE"
+ asset_types = ["compute.googleapis.com/Instance"]
+
+ feed_output_config {
+ pubsub_destination {
+ topic = module.pubsub.topic.id
+ }
+ }
+}
diff --git a/examples/cloud-operations/asset-inventory-feed-remediation/outputs.tf b/blueprints/cloud-operations/asset-inventory-feed-remediation/outputs.tf
similarity index 100%
rename from examples/cloud-operations/asset-inventory-feed-remediation/outputs.tf
rename to blueprints/cloud-operations/asset-inventory-feed-remediation/outputs.tf
diff --git a/examples/cloud-operations/asset-inventory-feed-remediation/variables.tf b/blueprints/cloud-operations/asset-inventory-feed-remediation/variables.tf
similarity index 100%
rename from examples/cloud-operations/asset-inventory-feed-remediation/variables.tf
rename to blueprints/cloud-operations/asset-inventory-feed-remediation/variables.tf
diff --git a/blueprints/cloud-operations/asset-inventory-feed-remediation/versions.tf b/blueprints/cloud-operations/asset-inventory-feed-remediation/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/cloud-operations/asset-inventory-feed-remediation/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/cloud-operations/dns-fine-grained-iam/README.md b/blueprints/cloud-operations/dns-fine-grained-iam/README.md
new file mode 100644
index 0000000000..adfc769fa7
--- /dev/null
+++ b/blueprints/cloud-operations/dns-fine-grained-iam/README.md
@@ -0,0 +1,120 @@
+# Fine-grained Cloud DNS IAM via Service Directory
+
+This blueprint shows how to leverage [Service Directory](https://cloud.google.com/blog/products/networking/introducing-service-directory) and Cloud DNS Service Directory private zones, to implement fine-grained IAM controls on DNS by
+
+- creating a Service Directory namespace with two services and their endpoints
+- creating a Cloud DNS private zone that uses the namespace as its authoritative source
+- creating two service accounts and assigning them the `roles/servicedirectory.editor` role on the namespace and on one service respectively
+- creating two VMs and setting them to use the two service accounts, so that DNS queries and `gcloud` commands can be used to verify the setup
+
+The resources created in this blueprint are shown in the high level diagram below:
+
+
+
+A [companion Medium article](https://medium.com/google-cloud/fine-grained-cloud-dns-iam-via-service-directory-446058b4362e) has been published for this blueprint, you can refer to it for more details on the context, and the specifics of running the blueprint.
+
+## Running the blueprint
+
+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%2Fdns-fine-grained-iam&cloudshell_open_in_editor=cloudshell_open%2Fcloud-foundation-fabric%2Fblueprints%2Fcloud-operations%2Fdns-fine-grained-iam%2Fvariables.tf), then go through the following steps to create resources:
+
+- `terraform init`
+- `terraform apply -var project_id=my-project-id`
+
+Once done testing, you can clean up resources by running `terraform destroy`. To persist state, check out the `backend.tf.sample` file.
+
+## Testing the blueprint
+
+The terraform outputs generate preset `gcloud compute ssh` commands that you can copy and run in the console to connect to each VM. Remember to adapt the testing commands below if you changed the default values for the `name`, `region`, or `zone_domain` variables.
+
+Connect via SSH to the `ns` VM and query the Service Directory namespace via DNS.
+
+```bash
+gcloud compute ssh dns-sd-test-ns-1 \
+ --zone europe-west1-b \
+ --tunnel-through-iap
+
+dig app1.svc.example.org +short
+# 127.0.0.2
+# 127.0.0.3
+# 127.0.0.7
+dig app2.svc.example.org +short
+# 127.0.0.4
+# 127.0.0.5
+dig _app1._tcp.app1.svc.example.org srv +short
+# 10 10 80 vm1.app1.svc.example.org.
+# 10 10 80 vm2.app1.svc.example.org.
+# 10 10 80 vm3.app1.svc.example.org.
+```
+
+The DNS answers should match the ones in the comments above, after each command. Note the special format used to query `SRV` records.
+
+If the above looks good, let's verify that the `ns` VM service account has edit rights on the namespace by creating a new service, and then verifying it via DNS.
+
+```bash
+gcloud beta service-directory services create app3 \
+ --location europe-west1 \
+ --namespace dns-sd-test
+# Created service [app3].
+
+gcloud beta service-directory endpoints create vm1 \
+ --service app3 \
+ --location europe-west1 \
+ --namespace dns-sd-test \
+ --address 127.0.0.6 \
+ --port 80
+# Created endpoint [vm1].
+
+dig app3.svc.example.org +short
+# 127.0.0.6
+```
+
+Log out from the `ns` VM and log in to the `svc` VM, then verify that its service account has no permissions on the whole namespace.
+
+```bash
+gcloud compute ssh dns-sd-test-svc-1 \
+ --zone europe-west1-b \
+ --tunnel-through-iap
+
+gcloud beta service-directory services delete app3 \
+ --location europe-west1 \
+ --namespace dns-sd-test
+# Deleted service [app3].
+# ERROR: (gcloud.beta.service-directory.services.delete) PERMISSION_DENIED: Permission 'servicedirectory.services.delete' denied on resource 'projects/my-project/locations/europe-west1/namespaces/dns-sd-test/services/app3'.
+```
+
+Ignoring the `deleted` message which is clearly a bug (the service is still in beta after all), the error message shows that this identity has no rights to operate on the namespace. What it can do is operate on the single service we gave it access to.
+
+```bash
+gcloud beta service-directory endpoints create vm3 \
+ --service app1 \
+ --location europe-west1 \
+ --namespace dns-sd-test \
+ --address 127.0.0.7 \
+ --port 80
+# Created endpoint [vm3].
+
+dig app1.svc.example.org +short
+# 127.0.0.2
+# 127.0.0.3
+# 127.0.0.7
+```
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [project_id](variables.tf#L29) | Existing project id. | string
| ✓ | |
+| [name](variables.tf#L17) | Arbitrary string used to name created resources. | string
| | "dns-sd-test"
|
+| [project_create](variables.tf#L23) | Create project instead ofusing an existing one. | bool
| | false
|
+| [region](variables.tf#L34) | Compute region used in the example. | string
| | "europe-west1"
|
+| [zone_domain](variables.tf#L40) | Domain name used for the DNS zone. | string
| | "svc.example.org."
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [gcloud_commands](outputs.tf#L17) | Commands used to SSH to the VMs. | |
+| [vms](outputs.tf#L25) | VM names. | |
+
+
diff --git a/examples/cloud-operations/dns-fine-grained-iam/backend.tf.sample b/blueprints/cloud-operations/dns-fine-grained-iam/backend.tf.sample
similarity index 100%
rename from examples/cloud-operations/dns-fine-grained-iam/backend.tf.sample
rename to blueprints/cloud-operations/dns-fine-grained-iam/backend.tf.sample
diff --git a/examples/cloud-operations/dns-fine-grained-iam/cloud-shell-readme.txt b/blueprints/cloud-operations/dns-fine-grained-iam/cloud-shell-readme.txt
similarity index 100%
rename from examples/cloud-operations/dns-fine-grained-iam/cloud-shell-readme.txt
rename to blueprints/cloud-operations/dns-fine-grained-iam/cloud-shell-readme.txt
diff --git a/examples/cloud-operations/dns-fine-grained-iam/diagram.png b/blueprints/cloud-operations/dns-fine-grained-iam/diagram.png
similarity index 100%
rename from examples/cloud-operations/dns-fine-grained-iam/diagram.png
rename to blueprints/cloud-operations/dns-fine-grained-iam/diagram.png
diff --git a/blueprints/cloud-operations/dns-fine-grained-iam/main.tf b/blueprints/cloud-operations/dns-fine-grained-iam/main.tf
new file mode 100644
index 0000000000..c867749912
--- /dev/null
+++ b/blueprints/cloud-operations/dns-fine-grained-iam/main.tf
@@ -0,0 +1,129 @@
+/**
+ * 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 {
+ startup-script = <string
| ✓ | |
+| [folder_id](variables.tf#L28) | Folder ID in which DNS projects will be created. | string
| ✓ | |
+| [prefix](variables.tf#L33) | Prefix used for resource names. | string
| ✓ | |
+| [shared_vpc_link](variables.tf#L51) | Shared VPC self link, used for DNS peering. | string
| ✓ | |
+| [dns_domain](variables.tf#L22) | DNS domain under which each application team DNS domain will be created. | string
| | "example.org"
|
+| [project_services](variables.tf#L42) | Service APIs enabled by default. | list(string)
| | […]
|
+| [teams](variables.tf#L56) | List of application teams requiring their own Cloud DNS instance. | list(string)
| | […]
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [teams](outputs.tf#L17) | Team resources. | |
+
+
diff --git a/examples/cloud-operations/dns-shared-vpc/diagram.png b/blueprints/cloud-operations/dns-shared-vpc/diagram.png
similarity index 100%
rename from examples/cloud-operations/dns-shared-vpc/diagram.png
rename to blueprints/cloud-operations/dns-shared-vpc/diagram.png
diff --git a/examples/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/README.md b/blueprints/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/README.md
similarity index 100%
rename from examples/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/README.md
rename to blueprints/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/README.md
diff --git a/examples/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/network.tf b/blueprints/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/network.tf
similarity index 87%
rename from examples/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/network.tf
rename to blueprints/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/network.tf
index 725ef63637..f6025d78a3 100644
--- a/examples/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/network.tf
+++ b/blueprints/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/network.tf
@@ -22,10 +22,9 @@ module "shared-vpc" {
subnets = [
{
- name = "subnet-01"
- ip_cidr_range = "10.10.1.0/24"
- region = var.region
- secondary_ip_range = {}
+ name = "subnet-01"
+ ip_cidr_range = "10.10.1.0/24"
+ region = var.region
}
]
}
diff --git a/examples/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/outputs.tf b/blueprints/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/outputs.tf
similarity index 100%
rename from examples/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/outputs.tf
rename to blueprints/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/outputs.tf
diff --git a/examples/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/projects.tf b/blueprints/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/projects.tf
similarity index 96%
rename from examples/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/projects.tf
rename to blueprints/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/projects.tf
index 66602cb167..4c247cdef7 100644
--- a/examples/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/projects.tf
+++ b/blueprints/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/projects.tf
@@ -37,8 +37,7 @@ module "project-host" {
services = var.project_services
shared_vpc_host_config = {
- enabled = true
- service_projects = [] # defined later
+ enabled = true
}
}
diff --git a/examples/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/test.example b/blueprints/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/test.example
similarity index 100%
rename from examples/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/test.example
rename to blueprints/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/test.example
diff --git a/blueprints/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/variables.tf b/blueprints/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/variables.tf
new file mode 100644
index 0000000000..90220e3df8
--- /dev/null
+++ b/blueprints/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/variables.tf
@@ -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.
+ */
+
+variable "host_project" {
+ description = "Host project name."
+ default = "host"
+}
+
+variable "service_projects" {
+ description = "List of service project names."
+ type = list(any)
+ default = [
+ "app-team1",
+ "app-team2",
+ ]
+}
+
+variable "region" {
+ description = "Region in which to create the subnet."
+ default = "europe-west1"
+}
+
+variable "project_services" {
+ description = "Service APIs enabled by default in new projects."
+ default = [
+ "compute.googleapis.com",
+ "dns.googleapis.com",
+ ]
+}
+
+variable "organization_id" {
+ description = "The organization ID."
+}
+
+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 "dns_domain" {
+ description = "DNS domain under which each application team DNS domain will be created."
+ default = "prod.internal"
+}
+
+variable "teams" {
+ description = "List of teams that require their own Cloud DNS instance."
+ default = ["appteam1", "appteam2"]
+}
diff --git a/blueprints/cloud-operations/dns-shared-vpc/main.tf b/blueprints/cloud-operations/dns-shared-vpc/main.tf
new file mode 100644
index 0000000000..4ade9476ac
--- /dev/null
+++ b/blueprints/cloud-operations/dns-shared-vpc/main.tf
@@ -0,0 +1,62 @@
+/**
+ * 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 {
+ projects = {
+ for k, v in module.project : k => v.project_id
+ }
+ svpc_project_id = regex("/projects/(.*?)/.*", var.shared_vpc_link)[0]
+}
+
+module "project" {
+ source = "../../../modules/project"
+ for_each = toset(var.teams)
+ billing_account = var.billing_account_id
+ name = each.value
+ parent = var.folder_id
+ prefix = var.prefix
+ services = var.project_services
+}
+
+module "vpc" {
+ source = "../../../modules/net-vpc"
+ for_each = local.projects
+ project_id = each.value
+ name = "dns-vpc"
+}
+
+module "dns-private" {
+ source = "../../../modules/dns"
+ for_each = local.projects
+ project_id = each.value
+ type = "private"
+ name = each.key
+ domain = "${each.key}.${var.dns_domain}."
+ description = "DNS zone for ${each.key}"
+ client_networks = [module.vpc[each.key].self_link]
+}
+
+module "dns-peering" {
+ source = "../../../modules/dns"
+ for_each = local.projects
+ project_id = local.svpc_project_id
+ name = "peering-${each.key}"
+ domain = "${each.key}.${var.dns_domain}."
+ description = "DNS peering for ${each.key}"
+ type = "peering"
+ peer_network = module.vpc[each.key].self_link
+ client_networks = [var.shared_vpc_link]
+}
diff --git a/examples/cloud-operations/dns-shared-vpc/outputs.tf b/blueprints/cloud-operations/dns-shared-vpc/outputs.tf
similarity index 100%
rename from examples/cloud-operations/dns-shared-vpc/outputs.tf
rename to blueprints/cloud-operations/dns-shared-vpc/outputs.tf
diff --git a/blueprints/cloud-operations/dns-shared-vpc/variables.tf b/blueprints/cloud-operations/dns-shared-vpc/variables.tf
new file mode 100644
index 0000000000..63a0ab9495
--- /dev/null
+++ b/blueprints/cloud-operations/dns-shared-vpc/variables.tf
@@ -0,0 +1,63 @@
+/**
+ * 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_id" {
+ description = "Billing account associated with the GCP Projects that will be created for each team."
+ type = string
+}
+
+variable "dns_domain" {
+ description = "DNS domain under which each application team DNS domain will be created."
+ type = string
+ default = "example.org"
+}
+
+variable "folder_id" {
+ description = "Folder ID in which DNS projects will be created."
+ type = string
+}
+
+variable "prefix" {
+ description = "Prefix used for resource names."
+ type = string
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty."
+ }
+}
+
+variable "project_services" {
+ description = "Service APIs enabled by default."
+ type = list(string)
+ default = [
+ "compute.googleapis.com",
+ "dns.googleapis.com",
+ ]
+}
+
+variable "shared_vpc_link" {
+ description = "Shared VPC self link, used for DNS peering."
+ type = string
+}
+
+variable "teams" {
+ description = "List of application teams requiring their own Cloud DNS instance."
+ type = list(string)
+ default = [
+ "team1",
+ "team2",
+ ]
+}
diff --git a/blueprints/cloud-operations/dns-shared-vpc/versions.tf b/blueprints/cloud-operations/dns-shared-vpc/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/cloud-operations/dns-shared-vpc/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/cloud-operations/iam-delegated-role-grants/README.md b/blueprints/cloud-operations/iam-delegated-role-grants/README.md
new file mode 100644
index 0000000000..4c4d227da6
--- /dev/null
+++ b/blueprints/cloud-operations/iam-delegated-role-grants/README.md
@@ -0,0 +1,78 @@
+# Delegated Role Grants
+
+This blueprint shows two applications of [delegated role grants](https://cloud.google.com/iam/docs/setting-limits-on-granting-roles):
+
+- how to use them to restrict service usage in a GCP project
+- how to use them to allow administrative access to a service via a predefined role, while restricting administrators from minting other admins.
+
+## Restricting service usage
+
+In its default configuration, the blueprint provisions two sets of permissions:
+
+- the roles listed in `direct_role_grants` will be granted unconditionally to the users listed in `project_administrators`.
+- additionally, `project_administrators` will be granted the role `roles/resourcemanager.projectIamAdmin` in a restricted fashion, allowing them to only grant the roles listed in `delegated_role_grants` to other users.
+
+By carefully choosing `direct_role_grants` and `delegated_role_grants`, you can restrict which services can be used within the project while still giving enough freedom to project administrators to still grant permissions to other principals within their projects.
+
+This diagram shows the resources and expected behaviour:
+
+
+
+
+A [Medium article](https://medium.com/@jccb/managing-gcp-service-usage-through-delegated-role-grants-a843610f2226) has been published for this blueprint, refer to it for more details on the context and the specifics of running the blueprint.
+
+## Restricting a predefined role
+
+By changing the `restricted_role_grant`, the blueprint can be used to grant administrators a predefined role like `roles/compute.networkAdmin`, which allows setting IAM policies on service resources like subnetworks, but restrict the roles that those administrators are able to confer to other users.
+
+You can easily configure the blueprint for this use case:
+
+```tfvars
+# terraform.tfvars
+
+delegated_role_grants = ["roles/compute.networkUser"]
+direct_role_grants = []
+restricted_role_grant = "roles/compute.networkAdmin"
+```
+
+This diagram shows the resources and expected behaviour:
+
+
+
+## Running the blueprint
+
+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%2Fiam-delegated-role-grants), then go through the following steps to create resources:
+
+- `terraform init`
+- `terraform apply -var project_id=my-project-id 'project_administrators=["user:project-admin@example.com"]'`
+
+Once done testing, you can clean up resources by running `terraform destroy`.
+
+## Auditing Roles
+
+This blueprint includes a python script that audits a list of roles to ensure you're not granting the `setIamPolicy` permission at the project, folder or organization level. To audit all the predefined compute roles, run it like this:
+
+```bash
+pip3 install -r requirements.txt
+gcloud iam roles list --filter="name:roles/compute. stage=GA" --format="get(name)" > roles.txt
+python3 audit.py roles.txt
+```
+
+If you get any warnings, check the roles and remove any of them granting any of the following permissions:
+- `resourcemanager.projects.setIamPolicy`
+- `resourcemanager.folders.setIamPolicy`
+- `resourcemanager.organizations.setIamPolicy`
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [project_administrators](variables.tf#L62) | List identities granted administrator permissions. | list(string)
| ✓ | |
+| [project_id](variables.tf#L73) | GCP project id where to grant direct and delegated roles to the users listed in project_administrators. | string
| ✓ | |
+| [delegated_role_grants](variables.tf#L17) | List of roles that project administrators will be allowed to grant/revoke. | list(string)
| | […]
|
+| [direct_role_grants](variables.tf#L53) | List of roles granted directly to project administrators. | list(string)
| | […]
|
+| [project_create](variables.tf#L67) | Create project instead of using an existing one. | bool
| | false
|
+| [restricted_role_grant](variables.tf#L78) | Role grant to which the restrictions will apply. | string
| | "roles/resourcemanager.projectIamAdmin"
|
+
+
diff --git a/examples/cloud-operations/iam-delegated-role-grants/audit.py b/blueprints/cloud-operations/iam-delegated-role-grants/audit.py
similarity index 100%
rename from examples/cloud-operations/iam-delegated-role-grants/audit.py
rename to blueprints/cloud-operations/iam-delegated-role-grants/audit.py
diff --git a/examples/cloud-operations/iam-delegated-role-grants/diagram-2.png b/blueprints/cloud-operations/iam-delegated-role-grants/diagram-2.png
similarity index 100%
rename from examples/cloud-operations/iam-delegated-role-grants/diagram-2.png
rename to blueprints/cloud-operations/iam-delegated-role-grants/diagram-2.png
diff --git a/examples/cloud-operations/iam-delegated-role-grants/diagram.png b/blueprints/cloud-operations/iam-delegated-role-grants/diagram.png
similarity index 100%
rename from examples/cloud-operations/iam-delegated-role-grants/diagram.png
rename to blueprints/cloud-operations/iam-delegated-role-grants/diagram.png
diff --git a/examples/cloud-operations/iam-delegated-role-grants/main.tf b/blueprints/cloud-operations/iam-delegated-role-grants/main.tf
similarity index 100%
rename from examples/cloud-operations/iam-delegated-role-grants/main.tf
rename to blueprints/cloud-operations/iam-delegated-role-grants/main.tf
diff --git a/examples/cloud-operations/iam-delegated-role-grants/outputs.tf b/blueprints/cloud-operations/iam-delegated-role-grants/outputs.tf
similarity index 100%
rename from examples/cloud-operations/iam-delegated-role-grants/outputs.tf
rename to blueprints/cloud-operations/iam-delegated-role-grants/outputs.tf
diff --git a/examples/cloud-operations/iam-delegated-role-grants/requirements.txt b/blueprints/cloud-operations/iam-delegated-role-grants/requirements.txt
similarity index 100%
rename from examples/cloud-operations/iam-delegated-role-grants/requirements.txt
rename to blueprints/cloud-operations/iam-delegated-role-grants/requirements.txt
diff --git a/examples/cloud-operations/iam-delegated-role-grants/variables.tf b/blueprints/cloud-operations/iam-delegated-role-grants/variables.tf
similarity index 100%
rename from examples/cloud-operations/iam-delegated-role-grants/variables.tf
rename to blueprints/cloud-operations/iam-delegated-role-grants/variables.tf
diff --git a/blueprints/cloud-operations/iam-delegated-role-grants/versions.tf b/blueprints/cloud-operations/iam-delegated-role-grants/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/cloud-operations/iam-delegated-role-grants/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/cloud-operations/network-dashboard/LICENSE b/blueprints/cloud-operations/network-dashboard/LICENSE
new file mode 100644
index 0000000000..261eeb9e9f
--- /dev/null
+++ b/blueprints/cloud-operations/network-dashboard/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/blueprints/cloud-operations/network-dashboard/README.md b/blueprints/cloud-operations/network-dashboard/README.md
new file mode 100644
index 0000000000..1fe0960f7a
--- /dev/null
+++ b/blueprints/cloud-operations/network-dashboard/README.md
@@ -0,0 +1,89 @@
+# Network Dashboard and Discovery Tool
+
+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.
+
+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:
+
+
+
+One other example is the IP utilization information per subnet, allowing you to monitor the percentage of used IP addresses in your GCP subnets.
+
+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.
+
+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.
+
+## Metrics created
+
+- `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 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.
+
+## TODO
+
+These are some of our ideas for additional features:
+
+- 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/dashboards/quotas-utilization.json b/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json
new file mode 100644
index 0000000000..1c11bdb7af
--- /dev/null
+++ b/blueprints/cloud-operations/network-dashboard/dashboards/quotas-utilization.json
@@ -0,0 +1,555 @@
+{
+ "category": "CUSTOM",
+ "displayName": "quotas_utilization",
+ "mosaicLayout": {
+ "columns": 12,
+ "tiles": [
+ {
+ "height": 4,
+ "widget": {
+ "title": "Internal L4 forwarding rules 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/netmon/network/forwarding_rules_l4_used_ratio\" resource.type=\"global\"",
+ "secondaryAggregation": {
+ "alignmentPeriod": "1800s",
+ "perSeriesAligner": "ALIGN_MEAN"
+ }
+ }
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ "width": 6,
+ "xPos": 0,
+ "yPos": 0
+ },
+ {
+ "height": 4,
+ "widget": {
+ "title": "Internal L7 forwarding rules 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/netmon/network/forwarding_rules_l4_used_ratio\" resource.type=\"global\"",
+ "secondaryAggregation": {
+ "alignmentPeriod": "60s",
+ "perSeriesAligner": "ALIGN_MEAN"
+ }
+ }
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ "width": 6,
+ "xPos": 0,
+ "yPos": 12
+ },
+ {
+ "height": 4,
+ "widget": {
+ "title": "Instance 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/netmon/network/instances_used_ratio\" resource.type=\"global\"",
+ "secondaryAggregation": {
+ "alignmentPeriod": "60s",
+ "perSeriesAligner": "ALIGN_MEAN"
+ }
+ }
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ "width": 6,
+ "xPos": 0,
+ "yPos": 8
+ },
+ {
+ "height": 4,
+ "widget": {
+ "title": "Peering 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/netmon/network/peerings_total_used_ratio\" resource.type=\"global\"",
+ "secondaryAggregation": {
+ "alignmentPeriod": "60s",
+ "perSeriesAligner": "ALIGN_MEAN"
+ }
+ }
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ "width": 6,
+ "xPos": 6,
+ "yPos": 4
+ },
+ {
+ "height": 4,
+ "widget": {
+ "title": "Active peering 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/netmon/network/peerings_active_used_ratio\" resource.type=\"global\"",
+ "secondaryAggregation": {
+ "alignmentPeriod": "60s",
+ "perSeriesAligner": "ALIGN_INTERPOLATE"
+ }
+ }
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ "width": 6,
+ "xPos": 0,
+ "yPos": 4
+ },
+ {
+ "height": 4,
+ "widget": {
+ "title": "Peering group internal L4 forwarding rules 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/netmon/peering_group/forwarding_rules_l4_used_ratio\" 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": "Peering group internal L7 forwarding rules 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/netmon/peering_group/forwarding_rules_l7_used_ratio\" resource.type=\"global\"",
+ "secondaryAggregation": {
+ "alignmentPeriod": "60s",
+ "perSeriesAligner": "ALIGN_MEAN"
+ }
+ }
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ "width": 6,
+ "xPos": 6,
+ "yPos": 12
+ },
+ {
+ "height": 4,
+ "widget": {
+ "title": "Peering group instance 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/netmon/peering_group/instances_used_ratio\" resource.type=\"global\""
+ }
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ "width": 6,
+ "xPos": 6,
+ "yPos": 8
+ },
+ {
+ "height": 4,
+ "widget": {
+ "title": "Peering group dynamic route 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/netmon/peering_group/routes_dynamic_used_ratio\" resource.type=\"global\""
+ }
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ "width": 6,
+ "xPos": 0,
+ "yPos": 20
+ },
+ {
+ "height": 4,
+ "widget": {
+ "title": "Project firewall rules used ratio",
+ "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/netmon/project/firewall_rules_used_ratio\" resource.type=\"global\""
+ }
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ "width": 6,
+ "xPos": 0,
+ "yPos": 32
+ },
+ {
+ "height": 4,
+ "widget": {
+ "title": "Firewall policy tuples used ratio",
+ "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/netmon/firewall_policy/tuples_used_ratio\" resource.type=\"global\""
+ }
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ "width": 6,
+ "xPos": 6,
+ "yPos": 28
+ },
+ {
+ "height": 4,
+ "widget": {
+ "title": "IP addressed per subnetwork used ratio",
+ "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/netmon/subnetwork/addresses_used_ratio\" resource.type=\"global\""
+ }
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ "width": 6,
+ "xPos": 6,
+ "yPos": 16
+ },
+ {
+ "height": 4,
+ "widget": {
+ "title": "Project static routes used",
+ "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/netmon/project/routes_static_used_ratio\" 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": "Peering group static routes used",
+ "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/netmon/peering_group/routes_static_used_ratio\" resource.type=\"global\""
+ }
+ }
+ }
+ ],
+ "thresholds": [],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ "width": 6,
+ "xPos": 0,
+ "yPos": 28
+ }
+ ]
+ }
+}
\ No newline at end of file
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 0000000000..bf1d7b5cf7
--- /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.
+
+
+
+## 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.
+
+
+
+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
| | "0/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 0000000000..6247c1c90d
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 0000000000..d715406722
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 0000000000..abbea80e29
--- /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 0000000000..0c2c50abed
--- /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 0000000000..ab59f91f52
--- /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 = "0/30 * * * *"
+}
diff --git a/blueprints/cloud-operations/network-dashboard/metric.png b/blueprints/cloud-operations/network-dashboard/metric.png
new file mode 100644
index 0000000000..3a6ae4d1f7
Binary files /dev/null and b/blueprints/cloud-operations/network-dashboard/metric.png differ
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 0000000000..a7fad82175
--- /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 0000000000..9f090b3c50
--- /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 0000000000..6db262a669
--- /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 0000000000..1bdc4cb20b
--- /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 0000000000..dc5c53247e
--- /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 0000000000..1d22cd8e99
--- /dev/null
+++ b/blueprints/cloud-operations/network-dashboard/src/plugins/discover-cai.py
@@ -0,0 +1,261 @@
+# 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'
+}
+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'],
+ 'self_link': _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'],
+ }
+
+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 0000000000..9c9e8f9486
--- /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 0000000000..cd2840b771
--- /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 0000000000..350c288b4c
--- /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 0000000000..a9e4090def
--- /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 0000000000..de4eae8971
--- /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 0000000000..defd697532
--- /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 0000000000..5490e6d3bd
--- /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 0000000000..0ce7a4b304
--- /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 0000000000..9f79268500
--- /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 0000000000..0ea088f9c0
--- /dev/null
+++ b/blueprints/cloud-operations/network-dashboard/src/plugins/series-psa.py
@@ -0,0 +1,71 @@
+# 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
+
+
+@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: 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 in _sql_addresses(resources.get('sql_instances', {})):
+ ip_address = ipaddress.ip_address(address)
+ for k, v in psa_nets.items():
+ if ip_address in v:
+ psa_counts[k] = psa_counts.get(k, 0) + ip_count
+ break
+
+ for k, v in psa_counts.items():
+ max_ips = psa_nets[k].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 0000000000..89011215ca
--- /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 0000000000..a9f0a5f302
--- /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 0000000000..5be6599889
--- /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 0000000000..3ca529bc35
--- /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 0000000000..93b1110e46
--- /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/onprem-sa-key-management/README.md b/blueprints/cloud-operations/onprem-sa-key-management/README.md
new file mode 100644
index 0000000000..4d6f3ab280
--- /dev/null
+++ b/blueprints/cloud-operations/onprem-sa-key-management/README.md
@@ -0,0 +1,80 @@
+# Managing on-prem service account keys by uploading public keys
+
+When managing GCP Service Accounts with terraform, it's often a question on **how to avoid Service Account Key in the terraform state?**
+
+This blueprint shows how to manage IAM Service Account Keys by manually generating a key pair and uploading the public part of the key to GCP. It has the following benefits:
+
+ - no [passing keys between users](https://cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys#pass-between-users) or systems
+ - no private keys stored in the terraform state (only public part of the key is in the state)
+ - let keys [expire automatically](https://cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys#key-expiryhaving)
+
+
+## Running the blueprint
+
+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%2Fonprem-sa-key-management&cloudshell_open_in_editor=cloudshell_open%2Fcloud-foundation-fabric%2Fblueprints%2Fcloud-operations%2Fonprem-sa-key-management%2Fvariables.tf), then go through the following steps to create resources:
+
+Cleaning up blueprint keys
+```bash
+rm -f /public-keys/data-uploader/
+rm -f /public-keys/prisma-security/
+```
+
+Generate keys for service accounts
+```bash
+mkdir keys && cd keys
+openssl req -x509 -nodes -newkey rsa:2048 -days 30 \
+ -keyout data_uploader_private_key.pem \
+ -out ../public-keys/data-uploader/public_key.pem \
+ -subj "/CN=unused"
+openssl req -x509 -nodes -newkey rsa:2048 -days 30 \
+ -keyout prisma_security_private_key.pem \
+ -out ../public-keys/prisma-security/public_key.pem \
+ -subj "/CN=unused"
+```
+
+Deploy service accounts and keys
+```bash
+cd ..
+terraform init
+terraform apply -var project_id=$GOOGLE_CLOUD_PROJECT
+
+```
+
+Extract JSON credentials templates from terraform output and put the private part of the keys into templates
+```bash
+terraform show -json | jq '.values.outputs."sa-credentials".value."data-uploader"."public_key.pem" | fromjson' > data-uploader.json
+terraform show -json | jq '.values.outputs."sa-credentials".value."prisma-security"."public_key.pem" | fromjson' > prisma-security.json
+
+contents=$(jq --arg key "$(cat keys/data_uploader_private_key.pem)" '.private_key=$key' data-uploader.json) && echo "$contents" > data-uploader.json
+contents=$(jq --arg key "$(cat keys/prisma_security_private_key.pem)" '.private_key=$key' prisma-security.json) && echo "$contents" > prisma-security.json
+```
+
+## Testing the blueprint
+Validate that service accounts json credentials are valid
+```bash
+gcloud auth activate-service-account --key-file prisma-security.json
+gcloud auth activate-service-account --key-file data-uploader.json
+```
+
+## Cleaning up
+```bash
+terraform destroy -var project_id=$GOOGLE_CLOUD_PROJECT
+```
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [project_id](variables.tf#L23) | Project id. | string
| ✓ | |
+| [project_create](variables.tf#L17) | Create project instead of using an existing one. | bool
| | false
|
+| [service_accounts](variables.tf#L28) | List of service accounts. | list(object({…}))
| | […]
|
+| [services](variables.tf#L56) | Service APIs to enable. | list(string)
| | []
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [sa-credentials](outputs.tf#L17) | SA json key templates. | |
+
+
diff --git a/examples/cloud-operations/onprem-sa-key-management/backend.tf.sample b/blueprints/cloud-operations/onprem-sa-key-management/backend.tf.sample
similarity index 100%
rename from examples/cloud-operations/onprem-sa-key-management/backend.tf.sample
rename to blueprints/cloud-operations/onprem-sa-key-management/backend.tf.sample
diff --git a/examples/cloud-operations/onprem-sa-key-management/cloud-shell-readme.txt b/blueprints/cloud-operations/onprem-sa-key-management/cloud-shell-readme.txt
similarity index 100%
rename from examples/cloud-operations/onprem-sa-key-management/cloud-shell-readme.txt
rename to blueprints/cloud-operations/onprem-sa-key-management/cloud-shell-readme.txt
diff --git a/examples/cloud-operations/onprem-sa-key-management/main.tf b/blueprints/cloud-operations/onprem-sa-key-management/main.tf
similarity index 100%
rename from examples/cloud-operations/onprem-sa-key-management/main.tf
rename to blueprints/cloud-operations/onprem-sa-key-management/main.tf
diff --git a/examples/cloud-operations/onprem-sa-key-management/outputs.tf b/blueprints/cloud-operations/onprem-sa-key-management/outputs.tf
similarity index 100%
rename from examples/cloud-operations/onprem-sa-key-management/outputs.tf
rename to blueprints/cloud-operations/onprem-sa-key-management/outputs.tf
diff --git a/examples/cloud-operations/onprem-sa-key-management/public-keys/data-uploader/public_key.pem b/blueprints/cloud-operations/onprem-sa-key-management/public-keys/data-uploader/public_key.pem
similarity index 100%
rename from examples/cloud-operations/onprem-sa-key-management/public-keys/data-uploader/public_key.pem
rename to blueprints/cloud-operations/onprem-sa-key-management/public-keys/data-uploader/public_key.pem
diff --git a/examples/cloud-operations/onprem-sa-key-management/public-keys/prisma-security/public_key.pem b/blueprints/cloud-operations/onprem-sa-key-management/public-keys/prisma-security/public_key.pem
similarity index 100%
rename from examples/cloud-operations/onprem-sa-key-management/public-keys/prisma-security/public_key.pem
rename to blueprints/cloud-operations/onprem-sa-key-management/public-keys/prisma-security/public_key.pem
diff --git a/examples/cloud-operations/onprem-sa-key-management/variables.tf b/blueprints/cloud-operations/onprem-sa-key-management/variables.tf
similarity index 100%
rename from examples/cloud-operations/onprem-sa-key-management/variables.tf
rename to blueprints/cloud-operations/onprem-sa-key-management/variables.tf
diff --git a/blueprints/cloud-operations/onprem-sa-key-management/versions.tf b/blueprints/cloud-operations/onprem-sa-key-management/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/cloud-operations/onprem-sa-key-management/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/cloud-operations/packer-image-builder/README.md b/blueprints/cloud-operations/packer-image-builder/README.md
new file mode 100644
index 0000000000..94755e6f22
--- /dev/null
+++ b/blueprints/cloud-operations/packer-image-builder/README.md
@@ -0,0 +1,95 @@
+# Compute Image builder with Hashicorp Packer
+
+This blueprint shows how to deploy infrastructure for a Compute Engine image builder based on
+[Hashicorp's Packer tool](https://www.packer.io).
+
+![High-level diagram](diagram.png "High-level diagram")
+
+## Running the blueprint
+
+Prerequisite: [Packer](https://www.packer.io/downloads) version >= v1.7.0
+
+Infrastructure setup (Terraform part):
+
+1. Set Terraform configuration variables
+2. Run `terraform init`
+3. Run `terraform apply`
+
+Building Compute Engine image (Packer part):
+
+1. Enter `packer` directory
+2. Set Packer configuration variables (see [Configuring Packer](#configuring-packer) below)
+3. Run `packer init .`
+4. Run `packer build .`
+
+## Using Packer's service account
+
+The following blueprint leverages [service account impersonation](https://cloud.google.com/iam/docs/impersonating-service-accounts)
+to execute any operations on GCP as a dedicated Packer service account. Depending on how you execute
+the Packer tool, you need to grant your principal rights to impersonate Packer's service account.
+
+Set `packer_account_users` variable in Terraform configuration to grant roles required to impersonate
+Packer's service account to selected IAM principals.
+Blueprint: allow default [Cloud Build](https://cloud.google.com/build) service account to impersonate
+Packer SA: `packer_account_users=["serviceAccount:myProjectNumber@cloudbuild.gserviceaccount.com"]`.
+
+## Configuring Packer
+
+Provided Packer build blueprint uses [HCL2 configuration files](https://www.packer.io/guides/hcl) and
+requires configuration of some input variables *(i.e. service accounts emails)*.
+Values of those variables can be taken from the Terraform outputs.
+
+For your convenience, Terraform can populate Packer's variable file.
+You can enable this behavior by setting `create_packer_vars` configuration variable to `true`.
+Terraform will use template from `packer/build.pkrvars.tpl` file and generate `packer/build.auto.pkrvars.hcl`
+variable file for Packer.
+
+Read [Assigning Variables](https://www.packer.io/guides/hcl/variables#assigning-variables) chapter
+from [Packer's documentation](https://www.packer.io/docs) for more details on setting up Packer variables.
+
+## Accessing temporary VM
+
+Packer creates a temporary Compute Engine VM instance for provisioning. As we recommend using internal
+IP addresses only, communication with this VM has to either:
+
+* originate from the network routable on Packer's VPC *(i.e. peered VPC, over VPN or interconnect)*
+* use [Identity-Aware Proxy](https://cloud.google.com/iap/docs/using-tcp-forwarding) tunnel
+
+By default, this blueprint assumes that IAP tunnel is needed to communicate with the temporary VM.
+This might be changed by setting `use_iap` variable to `false` in Terraform and Packer
+configurations respectively.
+
+**NOTE:** using IAP tunnel with Packer requires gcloud SDK installed on the system running Packer.
+
+## Accessing resources over the Internet
+
+The blueprint assumes that provisioning of a Compute Engine VM requires access to
+the resources over the Internet (i.e. to install OS packages). Since Compute VM has no public IP
+address for security reasons, Internet connectivity is done with [Cloud NAT](https://cloud.google.com/nat/docs/overview).
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [project_id](variables.tf#L55) | Project id that references existing project. | string
| ✓ | |
+| [billing_account](variables.tf#L17) | Billing account id used as default for new projects. | string
| | null
|
+| [cidrs](variables.tf#L23) | CIDR ranges for subnets. | map(string)
| | {…}
|
+| [create_packer_vars](variables.tf#L31) | Create packer variables file using template file and terraform output. | bool
| | false
|
+| [packer_account_users](variables.tf#L37) | List of members that will be allowed to impersonate Packer image builder service account in IAM format, i.e. 'user:{emailid}'. | list(string)
| | []
|
+| [packer_source_cidrs](variables.tf#L43) | List of CIDR ranges allowed to connect to the temporary VM for provisioning. | list(string)
| | ["0.0.0.0/0"]
|
+| [project_create](variables.tf#L49) | Create project instead of using an existing one. | bool
| | true
|
+| [region](variables.tf#L60) | Default region for resources. | string
| | "europe-west1"
|
+| [root_node](variables.tf#L66) | The resource name of the parent folder or organization for project creation, in 'folders/folder_id' or 'organizations/org_id' format. | string
| | null
|
+| [use_iap](variables.tf#L72) | Use IAP tunnel to connect to Compute Engine instance for provisioning. | bool
| | true
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [builder_sa](outputs.tf#L17) | Packer's service account email. | |
+| [compute_sa](outputs.tf#L22) | Packer's temporary VM service account email. | |
+| [compute_subnetwork](outputs.tf#L27) | Name of a subnetwork for Packer's temporary VM. | |
+| [compute_zone](outputs.tf#L32) | Name of a compute engine zone for Packer's temporary VM. | |
+
+
diff --git a/examples/cloud-operations/packer-image-builder/diagram.png b/blueprints/cloud-operations/packer-image-builder/diagram.png
similarity index 100%
rename from examples/cloud-operations/packer-image-builder/diagram.png
rename to blueprints/cloud-operations/packer-image-builder/diagram.png
diff --git a/blueprints/cloud-operations/packer-image-builder/main.tf b/blueprints/cloud-operations/packer-image-builder/main.tf
new file mode 100644
index 0000000000..f6de3af518
--- /dev/null
+++ b/blueprints/cloud-operations/packer-image-builder/main.tf
@@ -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.
+ */
+
+locals {
+ compute_subnet_name = "image-builder"
+ compute_zone = "${var.region}-a"
+ packer_variables_template = "packer/build.pkrvars.tpl"
+ packer_variables_file = "packer/build.auto.pkrvars.hcl"
+}
+
+module "project" {
+ source = "../../../modules/project"
+ name = var.project_id
+ parent = var.root_node
+ billing_account = var.billing_account
+ project_create = var.project_create
+ services = [
+ "compute.googleapis.com"
+ ]
+}
+
+module "service-account-image-builder" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.project.project_id
+ name = "image-builder"
+ iam_project_roles = {
+ (var.project_id) = [
+ "roles/compute.instanceAdmin.v1",
+ "roles/iam.serviceAccountUser"
+ ]
+ }
+}
+
+module "service-account-image-builder-vm" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.project.project_id
+ name = "image-builder-vm"
+}
+
+module "vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = "image-builder"
+ subnets = [
+ {
+ name = local.compute_subnet_name
+ ip_cidr_range = var.cidrs.image-builder
+ region = var.region
+ }
+ ]
+}
+
+module "firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.project.project_id
+ network = module.vpc.name
+ ingress_rules = {
+ image-builder-ingress-builder-vm = {
+ description = "Allow image builder vm ingress traffic"
+ source_ranges = var.packer_source_cidrs
+ targets = [module.service-account-image-builder-vm.email]
+ use_service_accounts = true
+ rules = [{
+ protocol = "tcp"
+ ports = [22, 5985, 5986]
+ }]
+ }
+ }
+}
+
+module "nat" {
+ source = "../../../modules/net-cloudnat"
+ project_id = module.project.project_id
+ region = var.region
+ name = "default"
+ router_network = module.vpc.name
+ config_source_subnets = "LIST_OF_SUBNETWORKS"
+ subnetworks = [
+ {
+ self_link = module.vpc.subnet_self_links["${var.region}/${local.compute_subnet_name}"]
+ config_source_ranges = ["ALL_IP_RANGES"]
+ secondary_ranges = null
+ }
+ ]
+}
+
+resource "google_service_account_iam_binding" "sa-image-builder-token-creators" {
+ count = length(var.packer_account_users) > 0 ? 1 : 0
+ service_account_id = module.service-account-image-builder.service_account.name
+ role = "roles/iam.serviceAccountTokenCreator"
+ members = var.packer_account_users
+}
+
+resource "google_project_iam_member" "project-iap-sa-image-builder" {
+ count = var.use_iap ? 1 : 0
+ project = var.project_id
+ member = module.service-account-image-builder.iam_email
+ role = "roles/iap.tunnelResourceAccessor"
+}
+
+resource "local_file" "packer-vars" {
+ count = var.create_packer_vars ? 1 : 0
+ content = templatefile(local.packer_variables_template, {
+ PROJECT_ID = "${var.project_id}"
+ COMPUTE_ZONE = "${local.compute_zone}"
+ BUILDER_SA = "${module.service-account-image-builder.email}"
+ COMPUTE_SA = "${module.service-account-image-builder-vm.email}"
+ COMPUTE_SUBNETWORK = "${local.compute_subnet_name}"
+ USE_IAP = "${var.use_iap}"
+ })
+ filename = local.packer_variables_file
+}
diff --git a/examples/cloud-operations/packer-image-builder/outputs.tf b/blueprints/cloud-operations/packer-image-builder/outputs.tf
similarity index 100%
rename from examples/cloud-operations/packer-image-builder/outputs.tf
rename to blueprints/cloud-operations/packer-image-builder/outputs.tf
diff --git a/examples/cloud-operations/packer-image-builder/packer/README.md b/blueprints/cloud-operations/packer-image-builder/packer/README.md
similarity index 100%
rename from examples/cloud-operations/packer-image-builder/packer/README.md
rename to blueprints/cloud-operations/packer-image-builder/packer/README.md
diff --git a/examples/cloud-operations/packer-image-builder/packer/build.pkr.hcl b/blueprints/cloud-operations/packer-image-builder/packer/build.pkr.hcl
similarity index 100%
rename from examples/cloud-operations/packer-image-builder/packer/build.pkr.hcl
rename to blueprints/cloud-operations/packer-image-builder/packer/build.pkr.hcl
diff --git a/examples/cloud-operations/packer-image-builder/packer/build.pkrvars.tpl b/blueprints/cloud-operations/packer-image-builder/packer/build.pkrvars.tpl
similarity index 100%
rename from examples/cloud-operations/packer-image-builder/packer/build.pkrvars.tpl
rename to blueprints/cloud-operations/packer-image-builder/packer/build.pkrvars.tpl
diff --git a/examples/cloud-operations/packer-image-builder/packer/install_httpd.sh b/blueprints/cloud-operations/packer-image-builder/packer/install_httpd.sh
similarity index 100%
rename from examples/cloud-operations/packer-image-builder/packer/install_httpd.sh
rename to blueprints/cloud-operations/packer-image-builder/packer/install_httpd.sh
diff --git a/examples/cloud-operations/packer-image-builder/packer/variables.pkr.hcl b/blueprints/cloud-operations/packer-image-builder/packer/variables.pkr.hcl
similarity index 100%
rename from examples/cloud-operations/packer-image-builder/packer/variables.pkr.hcl
rename to blueprints/cloud-operations/packer-image-builder/packer/variables.pkr.hcl
diff --git a/examples/cloud-operations/packer-image-builder/variables.tf b/blueprints/cloud-operations/packer-image-builder/variables.tf
similarity index 100%
rename from examples/cloud-operations/packer-image-builder/variables.tf
rename to blueprints/cloud-operations/packer-image-builder/variables.tf
diff --git a/blueprints/cloud-operations/packer-image-builder/versions.tf b/blueprints/cloud-operations/packer-image-builder/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/cloud-operations/packer-image-builder/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/cloud-operations/quota-monitoring/README.md b/blueprints/cloud-operations/quota-monitoring/README.md
new file mode 100644
index 0000000000..adcd89d643
--- /dev/null
+++ b/blueprints/cloud-operations/quota-monitoring/README.md
@@ -0,0 +1,54 @@
+# Compute Engine quota monitoring
+
+This blueprint improves on the [GCE quota exporter tool](https://github.com/GoogleCloudPlatform/professional-services/tree/master/tools/gce-quota-sync) (by the same author of this blueprint), and shows a practical way of collecting and monitoring [Compute Engine resource quotas](https://cloud.google.com/compute/quotas) via Cloud Monitoring metrics as an alternative to the recently released [built-in quota metrics](https://cloud.google.com/monitoring/alerts/using-quota-metrics).
+
+Compared to the built-in metrics, it offers a simpler representation of quotas and quota ratios which is especially useful in charts, it allows filtering or combining quotas between different projects regardless of their monitoring workspace, and it creates a default alerting policy without the need to interact directly with the monitoring API.
+
+Regardless of its specific purpose, this blueprint is also useful in showing how to manipulate and write time series to cloud monitoring. The resources it creates are shown in the high level diagram below:
+
+
+
+The solution is designed so that the Cloud Function arguments that control function execution (eg to set which project quotas to monitor) are defined in the Cloud Scheduler payload set in the PubSub message, so that a single function can be used for different configurations by creating more schedules.
+
+Quota time series are stored using [custom metrics](https://cloud.google.com/monitoring/custom-metrics) with metric type for usage, limit and utilization; metric types are named using a common prefix and two tokens joined by a `-` character:
+
+- `prefix` (custom.googleapis.com/quota/)
+- `quota name`
+- `{usage,limit,utilization}`
+
+e.g:
+
+- `custom.googleapis.com/quota/firewalls_usage`
+- `custom.googleapis.com/quota/firewalls_limit`
+- `custom.googleapis.com/quota/firewalls_utilization`
+
+All custom metrics are associated to the `global` resource type and use [gauge kind](https://cloud.google.com/monitoring/api/v3/kinds-and-types#metric-kinds)
+
+Labels are set with project id (which may differ from the monitoring workspace projects) and region (quotas that are not region specific are labelled `global`), this is how a usage/limit/utilization triplet looks in in Metrics Explorer
+
+
+
+The solution can also create a basic monitoring alert policy, to demonstrate how to raise alerts when quotas utilization goes over a predefined threshold, to enable it, set variable `alert_create` to true and reapply main.tf after main.py has run at least one and quota monitoring metrics have been creaed.
+
+## Running the blueprint
+
+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%2Fquota-monitoring), then go through the following steps to create resources:
+
+- `terraform init`
+- `terraform apply -var project_id=my-project-id`
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [project_id](variables.tf#L41) | Project id that references existing project. | string
| ✓ | |
+| [alert_create](variables.tf#L17) | Enables the creation of a sample monitoring alert, false by default. | bool
| | false
|
+| [bundle_path](variables.tf#L23) | Path used to write the intermediate Cloud Function code bundle. | string
| | "./bundle.zip"
|
+| [name](variables.tf#L29) | Arbitrary string used to name created resources. | string
| | "quota-monitor"
|
+| [project_create](variables.tf#L35) | Create project instead of using an existing one. | bool
| | false
|
+| [quota_config](variables.tf#L46) | Cloud function configuration. | object({…})
| | {…}
|
+| [region](variables.tf#L60) | Compute region used in the example. | string
| | "europe-west1"
|
+| [schedule_config](variables.tf#L66) | Schedule timer configuration in crontab format. | string
| | "0 * * * *"
|
+
+
diff --git a/examples/cloud-operations/quota-monitoring/backend.tf.sample b/blueprints/cloud-operations/quota-monitoring/backend.tf.sample
similarity index 100%
rename from examples/cloud-operations/quota-monitoring/backend.tf.sample
rename to blueprints/cloud-operations/quota-monitoring/backend.tf.sample
diff --git a/blueprints/cloud-operations/quota-monitoring/cf/main.py b/blueprints/cloud-operations/quota-monitoring/cf/main.py
new file mode 100755
index 0000000000..6d42b58149
--- /dev/null
+++ b/blueprints/cloud-operations/quota-monitoring/cf/main.py
@@ -0,0 +1,226 @@
+#! /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.
+"""Sync GCE quota usage to Stackdriver for multiple projects.
+
+This tool fetches global and/or regional quotas from the GCE API for
+multiple projects, and sends them to Stackdriver as custom metrics, where they
+can be used to set alert policies or create charts.
+"""
+
+import base64
+import datetime
+import json
+import logging
+import os
+import time
+import warnings
+
+import click
+
+from google.api_core.exceptions import GoogleAPIError
+from google.api import label_pb2 as ga_label
+from google.api import metric_pb2 as ga_metric
+from google.cloud import monitoring_v3
+
+import googleapiclient.discovery
+import googleapiclient.errors
+
+_BATCH_SIZE = 5
+_METRIC_KIND = ga_metric.MetricDescriptor.MetricKind.GAUGE
+_METRIC_TYPE_STEM = 'custom.googleapis.com/quota/'
+
+_USAGE = "usage"
+_LIMIT = "limit"
+_UTILIZATION = "utilization"
+
+
+def _add_series(project_id, series, client=None):
+ """Write metrics series to Stackdriver.
+
+ Args:
+ project_id: series will be written to this project id's account
+ series: the time series to be written, as a list of
+ monitoring_v3.types.TimeSeries instances
+ client: optional monitoring_v3.MetricServiceClient will be used
+ instead of obtaining a new one
+ """
+ client = client or monitoring_v3.MetricServiceClient()
+ project_name = client.common_project_path(project_id)
+ if isinstance(series, monitoring_v3.types.TimeSeries):
+ series = [series]
+ try:
+ client.create_time_series(name=project_name, time_series=series)
+ except GoogleAPIError as e:
+ raise RuntimeError('Error from monitoring API: %s' % e)
+
+
+def _configure_logging(verbose=True):
+ """Basic logging configuration.
+
+ Args:
+ verbose: enable verbose logging
+ """
+ level = logging.DEBUG if verbose else logging.INFO
+ logging.basicConfig(level=level)
+ warnings.filterwarnings('ignore', r'.*end user credentials.*', UserWarning)
+
+
+def _fetch_quotas(project, region='global', compute=None):
+ """Fetch GCE per - project or per - region quotas from the API.
+
+ Args:
+ project: fetch global or regional quotas for this project id
+ region: which quotas to fetch, 'global' or region name
+ compute: optional instance of googleapiclient.discovery.build will be used
+ instead of obtaining a new one
+ """
+ compute = compute or googleapiclient.discovery.build('compute', 'v1')
+ try:
+ if region != 'global':
+ req = compute.regions().get(project=project, region=region)
+ else:
+ req = compute.projects().get(project=project)
+ resp = req.execute()
+ return resp['quotas']
+ except (GoogleAPIError, googleapiclient.errors.HttpError) as e:
+ logging.debug('API Error: %s', e, exc_info=True)
+ raise RuntimeError('Error fetching quota (project: %s, region: %s)' %
+ (project, region))
+
+
+def _get_series(metric_labels, value, metric_type, timestamp, dt=None):
+ """Create a Stackdriver monitoring time series from value and labels.
+
+ Args:
+ metric_labels: dict with labels that will be used in the time series
+ value: time series value
+ metric_type: which metric is this series for
+ dt: datetime.datetime instance used for the series end time
+ """
+ series = monitoring_v3.types.TimeSeries()
+ series.metric.type = metric_type
+ series.resource.type = 'global'
+ for label in metric_labels:
+ series.metric.labels[label] = metric_labels[label]
+ point = monitoring_v3.types.Point()
+ point.value.double_value = value
+
+ seconds = int(timestamp)
+ nanos = int((timestamp - seconds) * 10**9)
+ interval = monitoring_v3.TimeInterval(
+ {"end_time": {
+ "seconds": seconds,
+ "nanos": nanos
+ }})
+ point.interval = interval
+
+ series.points.append(point)
+ return series
+
+
+def _quota_to_series_triplet(project, region, quota):
+ """Convert API quota objects to three Stackdriver monitoring time series: usage, limit and utilization
+
+ Args:
+ project: set in converted time series labels
+ region: set in converted time series labels
+ quota: quota object received from the GCE API
+ """
+ labels = dict()
+ labels['project'] = project
+ labels['region'] = region
+
+ try:
+ utilization = quota['usage'] / float(quota['limit'])
+ except ZeroDivisionError:
+ utilization = 0
+ now = time.time()
+ metric_type_prefix = _METRIC_TYPE_STEM + quota['metric'].lower() + '_'
+ return [
+ _get_series(labels, quota['usage'], metric_type_prefix + _USAGE, now),
+ _get_series(labels, quota['limit'], metric_type_prefix + _LIMIT, now),
+ _get_series(labels, utilization, metric_type_prefix + _UTILIZATION, now),
+ ]
+
+
+@click.command()
+@click.option('--monitoring-project', required=True,
+ help='monitoring project id')
+@click.option('--gce-project', multiple=True,
+ help='project ids (multiple), defaults to monitoring project')
+@click.option('--gce-region', multiple=True,
+ help='regions (multiple), defaults to "global"')
+@click.option('--verbose', is_flag=True, help='Verbose output')
+@click.argument('keywords', nargs=-1)
+def main_cli(monitoring_project=None, gce_project=None, gce_region=None,
+ verbose=False, keywords=None):
+ """Fetch GCE quotas and writes them as custom metrics to Stackdriver.
+
+ If KEYWORDS are specified as arguments, only quotas matching one of the
+ keywords will be stored in Stackdriver.
+ """
+ try:
+ _main(monitoring_project, gce_project, gce_region, verbose, keywords)
+ except RuntimeError:
+ logging.exception('exception raised')
+
+
+def main(event, context):
+ """Cloud Function entry point."""
+ try:
+ data = json.loads(base64.b64decode(event['data']).decode('utf-8'))
+ _main(os.environ.get('GCP_PROJECT'), **data)
+ # uncomment once https://issuetracker.google.com/issues/155215191 is fixed
+ # except RuntimeError:
+ # raise
+ except Exception:
+ logging.exception('exception in cloud function entry point')
+
+
+def _main(monitoring_project, gce_project=None, gce_region=None, verbose=False,
+ keywords=None):
+ """Module entry point used by cli and cloud function wrappers."""
+ _configure_logging(verbose=verbose)
+ gce_projects = gce_project or [monitoring_project]
+ gce_regions = gce_region or ['global']
+ keywords = set(keywords or [])
+ logging.debug('monitoring project %s', monitoring_project)
+ logging.debug('projects %s regions %s', gce_projects, gce_regions)
+ logging.debug('keywords %s', keywords)
+ quotas = []
+ compute = googleapiclient.discovery.build('compute', 'v1',
+ cache_discovery=False)
+ for project in gce_projects:
+ logging.debug('project %s', project)
+ for region in gce_regions:
+ logging.debug('region %s', region)
+ for quota in _fetch_quotas(project, region, compute=compute):
+ if keywords and not any(k in quota['metric'] for k in keywords):
+ # logging.debug('skipping %s', quota)
+ continue
+ logging.debug('quota %s', quota)
+ quotas.append((project, region, quota))
+ client, i = monitoring_v3.MetricServiceClient(), 0
+
+ x = len(quotas)
+ while i < len(quotas):
+ series = sum(
+ [_quota_to_series_triplet(*q) for q in quotas[i:i + _BATCH_SIZE]], [])
+ _add_series(monitoring_project, series, client)
+ i += _BATCH_SIZE
+
+
+if __name__ == '__main__':
+ main_cli()
diff --git a/examples/cloud-operations/quota-monitoring/cf/requirements.txt b/blueprints/cloud-operations/quota-monitoring/cf/requirements.txt
similarity index 100%
rename from examples/cloud-operations/quota-monitoring/cf/requirements.txt
rename to blueprints/cloud-operations/quota-monitoring/cf/requirements.txt
diff --git a/examples/cloud-operations/quota-monitoring/cloud-shell-readme.txt b/blueprints/cloud-operations/quota-monitoring/cloud-shell-readme.txt
similarity index 100%
rename from examples/cloud-operations/quota-monitoring/cloud-shell-readme.txt
rename to blueprints/cloud-operations/quota-monitoring/cloud-shell-readme.txt
diff --git a/examples/cloud-operations/quota-monitoring/diagram.png b/blueprints/cloud-operations/quota-monitoring/diagram.png
similarity index 100%
rename from examples/cloud-operations/quota-monitoring/diagram.png
rename to blueprints/cloud-operations/quota-monitoring/diagram.png
diff --git a/blueprints/cloud-operations/quota-monitoring/explorer.png b/blueprints/cloud-operations/quota-monitoring/explorer.png
new file mode 100644
index 0000000000..3e158563ae
Binary files /dev/null and b/blueprints/cloud-operations/quota-monitoring/explorer.png differ
diff --git a/blueprints/cloud-operations/quota-monitoring/main.tf b/blueprints/cloud-operations/quota-monitoring/main.tf
new file mode 100644
index 0000000000..fae9af514c
--- /dev/null
+++ b/blueprints/cloud-operations/quota-monitoring/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 {
+ projects = (
+ var.quota_config.projects == null
+ ? [var.project_id]
+ : var.quota_config.projects
+ )
+}
+
+module "project" {
+ source = "../../../modules/project"
+ name = var.project_id
+ project_create = var.project_create
+ services = [
+ "compute.googleapis.com",
+ "cloudfunctions.googleapis.com"
+ ]
+ iam = {
+ "roles/monitoring.metricWriter" = [module.cf.service_account_iam_email]
+ }
+}
+
+module "pubsub" {
+ source = "../../../modules/pubsub"
+ project_id = module.project.project_id
+ name = var.name
+ subscriptions = {
+ "${var.name}-default" = null
+ }
+ # the Cloud Scheduler robot service account already has pubsub.topics.publish
+ # at the project level via roles/cloudscheduler.serviceAgent
+}
+
+module "cf" {
+ source = "../../../modules/cloud-function"
+ project_id = module.project.project_id
+ name = var.name
+ bucket_name = "${var.name}-${random_pet.random.id}"
+ bucket_config = {
+ location = var.region
+ }
+ bundle_config = {
+ source_dir = "cf"
+ output_path = var.bundle_path
+ }
+ # https://github.com/hashicorp/terraform-provider-archive/issues/40
+ # https://issuetracker.google.com/issues/155215191
+ environment_variables = {
+ USE_WORKER_V2 = "true"
+ PYTHON37_DRAIN_LOGS_ON_CRASH_WAIT_SEC = "5"
+ }
+ service_account_create = true
+ trigger_config = {
+ v1 = {
+ event = "google.pubsub.topic.publish"
+ resource = module.pubsub.topic.id
+ }
+ }
+}
+
+resource "google_cloud_scheduler_job" "job" {
+ 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({
+ gce_project = var.quota_config.projects
+ gce_region = var.quota_config.regions
+ keywords = var.quota_config.filters
+ }))
+ }
+}
+
+resource "google_project_iam_member" "network_viewer" {
+ for_each = toset(local.projects)
+ project = each.key
+ role = "roles/compute.networkViewer"
+ member = module.cf.service_account_iam_email
+}
+
+resource "google_project_iam_member" "quota_viewer" {
+ for_each = toset(local.projects)
+ project = each.key
+ role = "roles/servicemanagement.quotaViewer"
+ member = module.cf.service_account_iam_email
+}
+
+
+resource "google_monitoring_alert_policy" "alert_policy" {
+ count = var.alert_create ? 1 : 0
+ project = module.project.project_id
+ display_name = "Quota monitor"
+ combiner = "OR"
+ conditions {
+ display_name = "simple quota threshold for cpus utilization"
+ condition_threshold {
+ filter = "metric.type=\"custom.googleapis.com/quota/cpus_utilization\" resource.type=\"global\""
+ threshold_value = 0.75
+ comparison = "COMPARISON_GT"
+ duration = "0s"
+ aggregations {
+ alignment_period = "60s"
+ group_by_fields = []
+ per_series_aligner = "ALIGN_MEAN"
+ }
+ trigger {
+ count = 1
+ percent = 0
+ }
+ }
+ }
+ enabled = false
+ user_labels = {
+ name = var.name
+ }
+ documentation {
+ content = "GCE cpus quota over threshold."
+ }
+}
+
+
+resource "random_pet" "random" {
+ length = 1
+}
diff --git a/examples/cloud-operations/quota-monitoring/outputs.tf b/blueprints/cloud-operations/quota-monitoring/outputs.tf
similarity index 100%
rename from examples/cloud-operations/quota-monitoring/outputs.tf
rename to blueprints/cloud-operations/quota-monitoring/outputs.tf
diff --git a/blueprints/cloud-operations/quota-monitoring/variables.tf b/blueprints/cloud-operations/quota-monitoring/variables.tf
new file mode 100644
index 0000000000..53ca0f6361
--- /dev/null
+++ b/blueprints/cloud-operations/quota-monitoring/variables.tf
@@ -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.
+ */
+
+variable "alert_create" {
+ description = "Enables the creation of a sample monitoring alert, false by default."
+ type = bool
+ default = false
+}
+
+variable "bundle_path" {
+ description = "Path used to write the intermediate Cloud Function code bundle."
+ type = string
+ default = "./bundle.zip"
+}
+
+variable "name" {
+ description = "Arbitrary string used to name created resources."
+ type = string
+ default = "quota-monitor"
+}
+
+variable "project_create" {
+ description = "Create project instead of using an existing one."
+ type = bool
+ default = false
+}
+
+variable "project_id" {
+ description = "Project id that references existing project."
+ type = string
+}
+
+variable "quota_config" {
+ description = "Cloud function configuration."
+ type = object({
+ filters = list(string)
+ projects = list(string)
+ regions = list(string)
+ })
+ default = {
+ filters = null
+ projects = null
+ regions = null
+ }
+}
+
+variable "region" {
+ description = "Compute region used in the example."
+ type = string
+ default = "europe-west1"
+}
+
+variable "schedule_config" {
+ description = "Schedule timer configuration in crontab format."
+ type = string
+ default = "0 * * * *"
+}
diff --git a/blueprints/cloud-operations/quota-monitoring/versions.tf b/blueprints/cloud-operations/quota-monitoring/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/cloud-operations/quota-monitoring/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/README.md b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/README.md
new file mode 100644
index 0000000000..9e66ba391e
--- /dev/null
+++ b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/README.md
@@ -0,0 +1,79 @@
+# Scheduled Cloud Asset Inventory Export to Bigquery
+
+This blueprint shows how to leverage [Cloud Asset Inventory Exporting to Bigquery](https://cloud.google.com/asset-inventory/docs/exporting-to-bigquery) feature to keep track of your project wide assets over time storing information in Bigquery.
+
+The data stored in Bigquery can then be used for different purposes:
+
+- dashboarding
+- analysis
+
+The blueprint uses export resources at the project level for ease of testing, in actual use a few changes are needed to operate at the resource hierarchy level:
+
+- the export should be set at the folder or organization level
+- the `roles/cloudasset.viewer` on the service account should be set at the folder or organization level
+
+The resources created in this blueprint are shown in the high level diagram below:
+
+
+
+## Prerequisites
+
+Ensure that you grant your account one of the following roles on your project, folder, or organization:
+
+- Cloud Asset Viewer role (`roles/cloudasset.viewer`)
+- Owner primitive role (`roles/owner`)
+
+## Running the blueprint
+
+Clone this repository, specify your variables in a `terraform.tvars` and then go through the following steps to create resources:
+
+- `terraform init`
+- `terraform apply`
+
+Once done testing, you can clean up resources by running `terraform destroy`. To persist state, check out the `backend.tf.sample` file.
+
+## Testing the blueprint
+
+Once resources are created, you can run queries on the data you exported on Bigquery. [Here](https://cloud.google.com/asset-inventory/docs/exporting-to-bigquery#querying_an_asset_snapshot) you can find some blueprint of queries you can run.
+
+You can also create a dashboard connecting [Datalab](https://datastudio.google.com/) or any other BI tools of your choice to your Bigquery dataset.
+
+## File exporter for JSON, CSV (optional).
+
+This is an optional part.
+
+Regular file-based exports of data from Cloud Asset Inventory may be useful for e.g. scale-out network dependencies discovery tools like [Planet Exporter](https://github.com/williamchanrico/planet-exporter), or to update legacy workloads tracking or configuration management systems. Bigquery supports multiple [export formats](https://cloud.google.com/bigquery/docs/exporting-data#export_formats_and_compression_types) and one may upload objects to Storage Bucket using provided Cloud Function. Specify `job.DestinationFormat` as defined in [documentation](https://googleapis.dev/python/bigquery/latest/generated/google.cloud.bigquery.job.DestinationFormat.html), e.g. `NEWLINE_DELIMITED_JSON`.
+
+It helps to create custom [scheduled query](https://cloud.google.com/bigquery/docs/scheduling-queries#console) from CAI export tables, and to write out results in to dedicated table (with overwrites). Define such query's output columns to comply with downstream systems' fields requirements, and time query execution after CAI export into BQ for freshness. See [sample queries](https://cloud.google.com/asset-inventory/docs/exporting-to-bigquery-sample-queries).
+
+This is an optional part, created if `cai_gcs_export` is set to `true`. The high level diagram extends to the following:
+
+
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [cai_config](variables.tf#L36) | Cloud Asset Inventory export config. | object({…})
| ✓ | |
+| [project_id](variables.tf#L101) | Project id that references existing project. | string
| ✓ | |
+| [billing_account](variables.tf#L17) | Billing account id used as default for new projects. | string
| | null
|
+| [bundle_path](variables.tf#L23) | Path used to write the intermediate Cloud Function code bundle. | string
| | "./bundle.zip"
|
+| [bundle_path_cffile](variables.tf#L30) | Path used to write the intermediate Cloud Function code bundle. | string
| | "./bundle_cffile.zip"
|
+| [cai_gcs_export](variables.tf#L47) | Enable optional part to export tables to GCS. | bool
| | false
|
+| [file_config](variables.tf#L54) | Optional BQ table as a file export function config. | object({…})
| | {…}
|
+| [location](variables.tf#L73) | Appe Engine location used in the example. | string
| | "europe-west"
|
+| [name](variables.tf#L80) | Arbitrary string used to name created resources. | string
| | "asset-inventory"
|
+| [name_cffile](variables.tf#L88) | Arbitrary string used to name created resources. | string
| | "cffile-exporter"
|
+| [project_create](variables.tf#L95) | Create project instead ofusing an existing one. | bool
| | true
|
+| [region](variables.tf#L106) | Compute region used in the example. | string
| | "europe-west1"
|
+| [root_node](variables.tf#L112) | The resource name of the parent folder or organization for project creation, in 'folders/folder_id' or 'organizations/org_id' format. | string
| | null
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [bq-dataset](outputs.tf#L17) | Bigquery instance details. | |
+| [cloud-function](outputs.tf#L22) | Cloud Function instance details. | |
+
+
diff --git a/examples/cloud-operations/scheduled-asset-inventory-export-bq/backend.tf.sample b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/backend.tf.sample
similarity index 100%
rename from examples/cloud-operations/scheduled-asset-inventory-export-bq/backend.tf.sample
rename to blueprints/cloud-operations/scheduled-asset-inventory-export-bq/backend.tf.sample
diff --git a/examples/cloud-operations/scheduled-asset-inventory-export-bq/cf/main.py b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/cf/main.py
similarity index 100%
rename from examples/cloud-operations/scheduled-asset-inventory-export-bq/cf/main.py
rename to blueprints/cloud-operations/scheduled-asset-inventory-export-bq/cf/main.py
diff --git a/examples/cloud-operations/scheduled-asset-inventory-export-bq/cf/requirements.txt b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/cf/requirements.txt
similarity index 100%
rename from examples/cloud-operations/scheduled-asset-inventory-export-bq/cf/requirements.txt
rename to blueprints/cloud-operations/scheduled-asset-inventory-export-bq/cf/requirements.txt
diff --git a/examples/cloud-operations/scheduled-asset-inventory-export-bq/cffile/main.py b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/cffile/main.py
similarity index 100%
rename from examples/cloud-operations/scheduled-asset-inventory-export-bq/cffile/main.py
rename to blueprints/cloud-operations/scheduled-asset-inventory-export-bq/cffile/main.py
diff --git a/examples/cloud-operations/scheduled-asset-inventory-export-bq/cffile/requirements.txt b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/cffile/requirements.txt
similarity index 100%
rename from examples/cloud-operations/scheduled-asset-inventory-export-bq/cffile/requirements.txt
rename to blueprints/cloud-operations/scheduled-asset-inventory-export-bq/cffile/requirements.txt
diff --git a/examples/cloud-operations/scheduled-asset-inventory-export-bq/diagram.png b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/diagram.png
similarity index 100%
rename from examples/cloud-operations/scheduled-asset-inventory-export-bq/diagram.png
rename to blueprints/cloud-operations/scheduled-asset-inventory-export-bq/diagram.png
diff --git a/examples/cloud-operations/scheduled-asset-inventory-export-bq/diagram_optional.png b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/diagram_optional.png
similarity index 100%
rename from examples/cloud-operations/scheduled-asset-inventory-export-bq/diagram_optional.png
rename to blueprints/cloud-operations/scheduled-asset-inventory-export-bq/diagram_optional.png
diff --git a/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/main.tf b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/main.tf
new file mode 100644
index 0000000000..1be2d1a983
--- /dev/null
+++ b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/main.tf
@@ -0,0 +1,212 @@
+/**
+ * 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.
+ */
+
+
+
+###############################################################################
+# Projects #
+###############################################################################
+
+module "project" {
+ source = "../../../modules/project"
+ name = var.project_id
+ parent = var.root_node
+ billing_account = try(var.billing_account, null)
+ project_create = var.project_create
+ services = [
+ "bigquery.googleapis.com",
+ "cloudasset.googleapis.com",
+ "compute.googleapis.com",
+ "cloudfunctions.googleapis.com",
+ "cloudbuild.googleapis.com",
+ "cloudscheduler.googleapis.com",
+ "pubsub.googleapis.com"
+ ]
+ iam = {
+ "roles/resourcemanager.projectIamAdmin" = ["serviceAccount:${module.project.service_accounts.robots.cloudasset}"]
+ "roles/bigquery.dataEditor" = ["serviceAccount:${module.project.service_accounts.robots.cloudasset}"]
+ "roles/bigquery.user" = ["serviceAccount:${module.project.service_accounts.robots.cloudasset}"]
+ }
+}
+
+module "service-account" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.project.project_id
+ name = "${var.name}-cf"
+ iam_project_roles = {
+ (var.project_id) = [
+ "roles/cloudasset.owner",
+ "roles/bigquery.jobUser"
+ ]
+ }
+}
+
+###############################################################################
+# Pub/Sub #
+###############################################################################
+
+module "pubsub" {
+ source = "../../../modules/pubsub"
+ project_id = module.project.project_id
+ name = var.name
+ subscriptions = {
+ "${var.name}-default" = null
+ }
+ # the Cloud Scheduler robot service account already has pubsub.topics.publish
+ # at the project level via roles/cloudscheduler.serviceAgent
+}
+
+module "pubsub_file" {
+ source = "../../../modules/pubsub"
+ project_id = module.project.project_id
+ name = var.name_cffile
+ subscriptions = {
+ "${var.name_cffile}-default" = null
+ }
+ # the Cloud Scheduler robot service account already has pubsub.topics.publish
+ # at the project level via roles/cloudscheduler.serviceAgent
+}
+
+###############################################################################
+# Cloud Function #
+###############################################################################
+
+module "cf" {
+ source = "../../../modules/cloud-function"
+ project_id = module.project.project_id
+ region = var.region
+ name = var.name
+ bucket_name = "${var.name}-${random_pet.random.id}"
+ bucket_config = {
+ location = var.region
+ }
+ bundle_config = {
+ source_dir = "cf"
+ output_path = var.bundle_path
+ }
+ service_account = module.service-account.email
+ trigger_config = {
+ v1 = {
+ event = "google.pubsub.topic.publish"
+ resource = module.pubsub.topic.id
+ }
+ }
+}
+
+module "cffile" {
+ count = var.cai_gcs_export ? 1 : 0
+ source = "../../../modules/cloud-function"
+ project_id = module.project.project_id
+ region = var.region
+ name = var.name_cffile
+ bucket_name = "${var.name_cffile}-${random_pet.random.id}"
+ bucket_config = {
+ location = var.region
+ lifecycle_delete_age_days = null
+ }
+ bundle_config = {
+ source_dir = "cffile"
+ output_path = var.bundle_path_cffile
+ excludes = null
+ }
+ service_account = module.service-account.email
+ trigger_config = {
+ v1 = {
+ event = "google.pubsub.topic.publish"
+ resource = module.pubsub_file.topic.id
+ retry = null
+ }
+ }
+}
+
+resource "random_pet" "random" {
+ length = 1
+}
+
+###############################################################################
+# Cloud Scheduler #
+###############################################################################
+
+resource "google_app_engine_application" "app" {
+ project = module.project.project_id
+ location_id = var.location
+}
+
+resource "google_cloud_scheduler_job" "job" {
+ project = google_app_engine_application.app.project
+ region = var.region
+ name = "cai-export-job"
+ description = "CAI Export Job."
+ schedule = "* 9 * * 1"
+ time_zone = "Etc/UTC"
+
+ pubsub_target {
+ attributes = {}
+ topic_name = module.pubsub.topic.id
+ data = base64encode(jsonencode({
+ project = module.project.project_id
+ bq_project = module.project.project_id
+ bq_dataset = var.cai_config.bq_dataset
+ bq_table = var.cai_config.bq_table
+ bq_table_overwrite = var.cai_config.bq_table_overwrite
+ target_node = var.cai_config.target_node
+ }))
+ }
+}
+
+resource "google_cloud_scheduler_job" "job_file" {
+ count = var.cai_gcs_export ? 1 : 0
+ project = google_app_engine_application.app.project
+ region = var.region
+ name = "file-export-job"
+ description = "File export from BQ Job."
+ schedule = "* 9 * * 1"
+ time_zone = "Etc/UTC"
+
+ pubsub_target {
+ attributes = {}
+ topic_name = module.pubsub_file.topic.id
+ data = base64encode(jsonencode({
+ bucket = var.file_config.bucket
+ filename = var.file_config.filename
+ format = var.file_config.format
+ bq_dataset = var.file_config.bq_dataset
+ bq_table = var.file_config.bq_table
+ }))
+ }
+}
+
+###############################################################################
+# Bigquery #
+###############################################################################
+
+module "bq" {
+ source = "../../../modules/bigquery-dataset"
+ project_id = module.project.project_id
+ id = var.cai_config.bq_dataset
+ location = var.region
+ access = {
+ owner = { role = "OWNER", type = "user" }
+ }
+ access_identities = {
+ owner = module.service-account.email
+ }
+ options = {
+ default_table_expiration_ms = null
+ default_partition_expiration_ms = null
+ delete_contents_on_destroy = true
+ }
+}
diff --git a/examples/cloud-operations/scheduled-asset-inventory-export-bq/outputs.tf b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/outputs.tf
similarity index 100%
rename from examples/cloud-operations/scheduled-asset-inventory-export-bq/outputs.tf
rename to blueprints/cloud-operations/scheduled-asset-inventory-export-bq/outputs.tf
diff --git a/examples/cloud-operations/scheduled-asset-inventory-export-bq/variables.tf b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/variables.tf
similarity index 100%
rename from examples/cloud-operations/scheduled-asset-inventory-export-bq/variables.tf
rename to blueprints/cloud-operations/scheduled-asset-inventory-export-bq/variables.tf
diff --git a/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/versions.tf b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/cloud-operations/scheduled-asset-inventory-export-bq/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/README.md b/blueprints/cloud-operations/terraform-enterprise-wif/README.md
new file mode 100644
index 0000000000..4bb282c560
--- /dev/null
+++ b/blueprints/cloud-operations/terraform-enterprise-wif/README.md
@@ -0,0 +1,115 @@
+# Configuring workload identity federation for Terraform Cloud/Enterprise workflow
+
+The most common way to use Terraform Cloud for GCP deployments is to store a GCP Service Account Key as a part of TFE Workflow configuration, as we all know there are security risks due to the fact that keys are long term credentials that could be compromised.
+
+Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account.
+
+This blueprint shows how to set up [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) between [Terraform Cloud/Enterprise](https://developer.hashicorp.com/terraform/enterprise) instance and Google Cloud. This will be possible by configuring workload identity federation to trust oidc tokens generated for a specific workflow in a Terraform Enterprise organization.
+
+The following diagram illustrates how the VM will get a short-lived access token and use it to access a resource:
+
+ ![Sequence diagram](diagram.png)
+
+## Running the blueprint
+
+### Create Terraform Enterprise Workflow
+If you don't have an existing Terraform Enterprise organization you can sign up for a [free trial](https://app.terraform.io/public/signup/account) account.
+
+Create a new Workspace for a `CLI-driven workflow` (Identity Federation will work for any workflow type, but for simplicity of the blueprint we use CLI driven workflow).
+
+Note workspace name and id (id starts with `ws-`), we will use them on a later stage.
+
+Go to the organization settings and note the org name and id (id starts with `org-`).
+
+### Deploy GCP Workload Identity Pool Provider for Terraform Enterprise
+
+> **_NOTE:_** This is a preparation part and should be executed on behalf of a user with enough permissions.
+
+Required permissions when new project is created:
+ - Project Creator on the parent folder/org.
+
+ Required permissions when an existing project is used:
+ - Workload Identity Admin on the project level
+ - Project IAM Admin on the project level
+
+Fill out required variables, use TFE Org and Workspace IDs from the previous steps (IDs are not the names).
+```bash
+cd gcp-workload-identity-provider
+
+mv terraform.auto.tfvars.template terraform.auto.tfvars
+
+vi terraform.auto.tfvars
+```
+
+Authenticate using application default credentials, execute terraform code and deploy resources
+```
+gcloud auth application-default login
+
+terraform init
+
+terraform apply
+```
+
+As a result a set of outputs will be provided (your values will be different), note the output since we will use it on the next steps.
+
+```
+impersonate_service_account_email = "sa-tfe@fe-test-oidc.iam.gserviceaccount.com"
+project_id = "tfe-test-oidc"
+workload_identity_audience = "//iam.googleapis.com/projects/476538149566/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider"
+workload_identity_pool_provider_id = "projects/476538149566/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider"
+```
+
+### Configure OIDC provider for your TFE Workflow
+
+To enable OIDC for a TFE workflow it's enough to setup an environment variable `TFC_WORKLOAD_IDENTITY_AUDIENCE`.
+
+Go the the Workflow -> Variables and add a new variable `TFC_WORKLOAD_IDENTITY_AUDIENCE` equal to the value of `workload_identity_audience` output, in our example it's:
+
+```
+TFC_WORKLOAD_IDENTITY_AUDIENCE = "//iam.googleapis.com/projects/476538149566/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider"
+```
+
+At that point we setup GCP Identity Federation to trust TFE generated OIDC tokens, so the TFE workflow can use the token to impersonate a GCP Service Account.
+
+## Testing the blueprint
+
+In order to test the setup we will deploy a GCS bucket from TFE Workflow using OIDC token for Service Account Impersonation.
+
+### Configure backend and variables
+
+First, we need to configure TFE Remote backend for our testing terraform code, use TFE Organization name and workspace name (names are not the same as ids)
+
+```
+cd ../tfc-workflow-using-wif
+
+mv backend.tf.template backend.tf
+
+
+vi backend.tf
+
+```
+
+Fill out variables based on the output from the preparation steps:
+
+```
+mv terraform.auto.tfvars.template terraform.auto.tfvars
+
+vi terraform.auto.tfvars
+
+```
+
+### Authenticate terraform for triggering CLI-driven workflow
+
+Follow this [documentation](https://learn.hashicorp.com/tutorials/terraform/cloud-login) to login ti terraform cloud from the CLI.
+
+### Trigger the workflow
+
+```
+terraform init
+
+terraform apply
+```
+
+As a result we have a successfully deployed GCS bucket from Terraform Enterprise workflow using Workload Identity Federation.
+
+Once done testing, you can clean up resources by running `terraform destroy` first in the `tfc-workflow-using-wif` and then `gcp-workload-identity-provider` folders.
diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/diagram.png b/blueprints/cloud-operations/terraform-enterprise-wif/diagram.png
new file mode 100644
index 0000000000..d4e6f82e9d
Binary files /dev/null and b/blueprints/cloud-operations/terraform-enterprise-wif/diagram.png differ
diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/README.md b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/README.md
new file mode 100644
index 0000000000..35198e8d1c
--- /dev/null
+++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/README.md
@@ -0,0 +1,33 @@
+# GCP Workload Identity Provider for Terraform Enterprise
+
+This terraform code is a part of [GCP Workload Identity Federation for Terraform Enterprise](../) blueprint.
+
+The codebase provisions the following list of resources:
+
+- GCS Bucket
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [billing_account](variables.tf#L16) | Billing account id used as default for new projects. | string
| ✓ | |
+| [project_id](variables.tf#L43) | Existing project id. | string
| ✓ | |
+| [tfe_organization_id](variables.tf#L48) | TFE organization id. | string
| ✓ | |
+| [tfe_workspace_id](variables.tf#L53) | TFE workspace id. | string
| ✓ | |
+| [issuer_uri](variables.tf#L21) | Terraform Enterprise uri. Replace the uri if a self hosted instance is used. | string
| | "https://app.terraform.io/"
|
+| [parent](variables.tf#L27) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string
| | null
|
+| [project_create](variables.tf#L37) | Create project instead of using an existing one. | bool
| | true
|
+| [workload_identity_pool_id](variables.tf#L58) | Workload identity pool id. | string
| | "tfe-pool"
|
+| [workload_identity_pool_provider_id](variables.tf#L64) | Workload identity pool provider id. | string
| | "tfe-provider"
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [impersonate_service_account_email](outputs.tf#L16) | Service account to be impersonated by workload identity. | |
+| [project_id](outputs.tf#L21) | GCP Project ID. | |
+| [workload_identity_audience](outputs.tf#L26) | TFC Workload Identity Audience. | |
+| [workload_identity_pool_provider_id](outputs.tf#L31) | GCP workload identity pool provider ID. | |
+
+
diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf
new file mode 100644
index 0000000000..5ced2e3c57
--- /dev/null
+++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf
@@ -0,0 +1,91 @@
+# 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.
+
+
+###############################################################################
+# GCP PROJECT #
+###############################################################################
+
+module "project" {
+ source = "../../../../modules/project"
+ name = var.project_id
+ project_create = var.project_create
+ parent = var.parent
+ billing_account = var.billing_account
+ services = [
+ "iam.googleapis.com",
+ "cloudresourcemanager.googleapis.com",
+ "iamcredentials.googleapis.com",
+ "sts.googleapis.com",
+ "storage.googleapis.com"
+ ]
+}
+
+###############################################################################
+# Workload Identity Pool and Provider #
+###############################################################################
+
+resource "google_iam_workload_identity_pool" "tfe-pool" {
+ project = module.project.project_id
+ workload_identity_pool_id = var.workload_identity_pool_id
+ display_name = "TFE Pool"
+ description = "Identity pool for Terraform Enterprise OIDC integration"
+}
+
+resource "google_iam_workload_identity_pool_provider" "tfe-pool-provider" {
+ project = module.project.project_id
+ workload_identity_pool_id = google_iam_workload_identity_pool.tfe-pool.workload_identity_pool_id
+ workload_identity_pool_provider_id = var.workload_identity_pool_provider_id
+ display_name = "TFE Pool Provider"
+ description = "OIDC identity pool provider for TFE Integration"
+ # Use condition to make sure only token generated for a specific TFE Org can be used across org workspaces
+ attribute_condition = "attribute.terraform_organization_id == \"${var.tfe_organization_id}\""
+ attribute_mapping = {
+ "google.subject" = "assertion.sub"
+ "attribute.aud" = "assertion.aud"
+ "attribute.terraform_run_phase" = "assertion.terraform_run_phase"
+ "attribute.terraform_workspace_id" = "assertion.terraform_workspace_id"
+ "attribute.terraform_workspace_name" = "assertion.terraform_workspace_name"
+ "attribute.terraform_organization_id" = "assertion.terraform_organization_id"
+ "attribute.terraform_organization_name" = "assertion.terraform_organization_name"
+ "attribute.terraform_run_id" = "assertion.terraform_run_id"
+ "attribute.terraform_full_workspace" = "assertion.terraform_full_workspace"
+ }
+ oidc {
+ # Should be different if self hosted TFE instance is used
+ issuer_uri = var.issuer_uri
+ }
+}
+
+###############################################################################
+# Service Account and IAM bindings #
+###############################################################################
+
+module "sa-tfe" {
+ source = "../../../../modules/iam-service-account"
+ project_id = module.project.project_id
+ name = "sa-tfe"
+
+ iam = {
+ # We allow only tokens generated by a specific TFE workspace impersonation of the service account,
+ # that way one identity pool can be used for a TFE Organization, but every workspace will be able to impersonate only a specifc SA
+ "roles/iam.workloadIdentityUser" = ["principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.tfe-pool.name}/attribute.terraform_workspace_id/${var.tfe_workspace_id}"]
+ }
+
+ iam_project_roles = {
+ "${module.project.project_id}" = [
+ "roles/storage.admin"
+ ]
+ }
+}
diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf
new file mode 100644
index 0000000000..46d7f6b0f2
--- /dev/null
+++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf
@@ -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
+#
+# 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 "impersonate_service_account_email" {
+ description = "Service account to be impersonated by workload identity."
+ value = module.sa-tfe.email
+}
+
+output "project_id" {
+ description = "GCP Project ID."
+ value = module.project.project_id
+}
+
+output "workload_identity_audience" {
+ description = "TFC Workload Identity Audience."
+ value = "//iam.googleapis.com/${google_iam_workload_identity_pool_provider.tfe-pool-provider.name}"
+}
+
+output "workload_identity_pool_provider_id" {
+ description = "GCP workload identity pool provider ID."
+ value = google_iam_workload_identity_pool_provider.tfe-pool-provider.name
+}
diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/terraform.auto.tfvars.template b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/terraform.auto.tfvars.template
new file mode 100644
index 0000000000..645eea0b9c
--- /dev/null
+++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/terraform.auto.tfvars.template
@@ -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
+#
+# 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.
+
+
+parent = "folders/437102807785"
+project_id = "my-project-id"
+tfe_organization_id = "org-W3bz9neazHrZz99U"
+tfe_workspace_id = "ws-DFxEE3NmeMdaAvoK"
+billing_account = "015617-1B8CBC-AF10D9"
diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf
new file mode 100644
index 0000000000..3719b1839e
--- /dev/null
+++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.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
+#
+# 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.
+
+
+variable "billing_account" {
+ description = "Billing account id used as default for new projects."
+ type = string
+}
+
+variable "issuer_uri" {
+ description = "Terraform Enterprise uri. Replace the uri if a self hosted instance is used."
+ type = string
+ default = "https://app.terraform.io/"
+}
+
+variable "parent" {
+ description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format."
+ type = string
+ default = null
+ validation {
+ condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent))
+ error_message = "Parent must be of the form folders/folder_id or organizations/organization_id."
+ }
+}
+
+variable "project_create" {
+ description = "Create project instead of using an existing one."
+ type = bool
+ default = true
+}
+
+variable "project_id" {
+ description = "Existing project id."
+ type = string
+}
+
+variable "tfe_organization_id" {
+ description = "TFE organization id."
+ type = string
+}
+
+variable "tfe_workspace_id" {
+ description = "TFE workspace id."
+ type = string
+}
+
+variable "workload_identity_pool_id" {
+ description = "Workload identity pool id."
+ type = string
+ default = "tfe-pool"
+}
+
+variable "workload_identity_pool_provider_id" {
+ description = "Workload identity pool provider id."
+ type = string
+ default = "tfe-provider"
+}
diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md
new file mode 100644
index 0000000000..9b292de62a
--- /dev/null
+++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md
@@ -0,0 +1,17 @@
+# GCP Workload Identity Provider for Terraform Enterprise
+
+This terraform code is a part of [GCP Workload Identity Federation for Terraform Enterprise](../) blueprint. For instructions please refer to the blueprint [readme](../README.md).
+
+The codebase provisions the following list of resources:
+
+- GCS Bucket
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [impersonate_service_account_email](variables.tf#L16) | Service account to be impersonated by workload identity. | string
| ✓ | |
+| [project_id](variables.tf#L21) | GCP project ID. | string
| ✓ | |
+
+
diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/backend.tf.template b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/backend.tf.template
new file mode 100644
index 0000000000..87d4737dfb
--- /dev/null
+++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/backend.tf.template
@@ -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.
+
+
+# The block below configures Terraform to use the 'remote' backend with Terraform Cloud.
+# For more information, see https://www.terraform.io/docs/backends/types/remote.html
+
+terraform {
+ backend "remote" {
+ organization = "string
| ✓ | |
+| [tmp_oidc_token_path](variables.tf#L22) | Name of the temporary file where TFC OIDC token will be stored to authentificate terraform provider google. | string
| | ".oidc_token"
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [credentials](outputs.tf#L17) | Credentials in format to pass the to gcp provider. | |
+
+
diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/get_audience.sh b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/get_audience.sh
new file mode 100644
index 0000000000..251fe321d0
--- /dev/null
+++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/get_audience.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+# 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.
+
+# Exit if any of the intermediate steps fail
+set -e
+
+cat <string
| ✓ | |
+| [project_id](variables.tf#L39) | Project id to create a project when `project_create` is `true`, or to be used when `false`. | string
| ✓ | |
+| [grace_period](variables.tf#L21) | Grace period for an instance startup. | string
| | "180s"
|
+| [location](variables.tf#L27) | App Engine location used in the example (required for CloudFunctions). | string
| | "europe-west"
|
+| [project_create](variables.tf#L33) | Create project instead of using an existing one. | bool
| | false
|
+| [region](variables.tf#L44) | Compute region used in the example. | string
| | "europe-west1"
|
+| [root_node](variables.tf#L50) | The resource name of the parent folder or organization for project creation, in 'folders/folder_id' or 'organizations/org_id' format. | string
| | null
|
+| [schedule](variables.tf#L56) | Cron schedule for executing compute instances healthcheck. | string
| | "*/5 * * * *" # every five minutes"
|
+| [tcp_port](variables.tf#L62) | TCP port to run healthcheck against. | string
| | "80" #http"
|
+| [timeout](variables.tf#L68) | TCP probe timeout. | string
| | "1000ms"
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [cloud-function-healthchecker](outputs.tf#L16) | Cloud Function Healthchecker instance details. | |
+| [cloud-function-restarter](outputs.tf#L21) | Cloud Function Healthchecker instance details. | |
+| [pubsub-topic](outputs.tf#L26) | Restarter PubSub topic. | |
+
+
diff --git a/examples/cloud-operations/unmanaged-instances-healthcheck/backend.tf.sample b/blueprints/cloud-operations/unmanaged-instances-healthcheck/backend.tf.sample
similarity index 100%
rename from examples/cloud-operations/unmanaged-instances-healthcheck/backend.tf.sample
rename to blueprints/cloud-operations/unmanaged-instances-healthcheck/backend.tf.sample
diff --git a/examples/cloud-operations/unmanaged-instances-healthcheck/cloud-shell-readme.txt b/blueprints/cloud-operations/unmanaged-instances-healthcheck/cloud-shell-readme.txt
similarity index 100%
rename from examples/cloud-operations/unmanaged-instances-healthcheck/cloud-shell-readme.txt
rename to blueprints/cloud-operations/unmanaged-instances-healthcheck/cloud-shell-readme.txt
diff --git a/examples/cloud-operations/unmanaged-instances-healthcheck/diagram.png b/blueprints/cloud-operations/unmanaged-instances-healthcheck/diagram.png
similarity index 100%
rename from examples/cloud-operations/unmanaged-instances-healthcheck/diagram.png
rename to blueprints/cloud-operations/unmanaged-instances-healthcheck/diagram.png
diff --git a/examples/cloud-operations/unmanaged-instances-healthcheck/function/healthchecker/configuration.go b/blueprints/cloud-operations/unmanaged-instances-healthcheck/function/healthchecker/configuration.go
similarity index 100%
rename from examples/cloud-operations/unmanaged-instances-healthcheck/function/healthchecker/configuration.go
rename to blueprints/cloud-operations/unmanaged-instances-healthcheck/function/healthchecker/configuration.go
diff --git a/examples/cloud-operations/unmanaged-instances-healthcheck/function/healthchecker/go.mod b/blueprints/cloud-operations/unmanaged-instances-healthcheck/function/healthchecker/go.mod
similarity index 100%
rename from examples/cloud-operations/unmanaged-instances-healthcheck/function/healthchecker/go.mod
rename to blueprints/cloud-operations/unmanaged-instances-healthcheck/function/healthchecker/go.mod
diff --git a/examples/cloud-operations/unmanaged-instances-healthcheck/function/healthchecker/go.sum b/blueprints/cloud-operations/unmanaged-instances-healthcheck/function/healthchecker/go.sum
similarity index 100%
rename from examples/cloud-operations/unmanaged-instances-healthcheck/function/healthchecker/go.sum
rename to blueprints/cloud-operations/unmanaged-instances-healthcheck/function/healthchecker/go.sum
diff --git a/examples/cloud-operations/unmanaged-instances-healthcheck/function/healthchecker/healthchecker.go b/blueprints/cloud-operations/unmanaged-instances-healthcheck/function/healthchecker/healthchecker.go
similarity index 100%
rename from examples/cloud-operations/unmanaged-instances-healthcheck/function/healthchecker/healthchecker.go
rename to blueprints/cloud-operations/unmanaged-instances-healthcheck/function/healthchecker/healthchecker.go
diff --git a/examples/cloud-operations/unmanaged-instances-healthcheck/function/restarter/go.mod b/blueprints/cloud-operations/unmanaged-instances-healthcheck/function/restarter/go.mod
similarity index 100%
rename from examples/cloud-operations/unmanaged-instances-healthcheck/function/restarter/go.mod
rename to blueprints/cloud-operations/unmanaged-instances-healthcheck/function/restarter/go.mod
diff --git a/examples/cloud-operations/unmanaged-instances-healthcheck/function/restarter/go.sum b/blueprints/cloud-operations/unmanaged-instances-healthcheck/function/restarter/go.sum
similarity index 100%
rename from examples/cloud-operations/unmanaged-instances-healthcheck/function/restarter/go.sum
rename to blueprints/cloud-operations/unmanaged-instances-healthcheck/function/restarter/go.sum
diff --git a/examples/cloud-operations/unmanaged-instances-healthcheck/function/restarter/restarter.go b/blueprints/cloud-operations/unmanaged-instances-healthcheck/function/restarter/restarter.go
similarity index 100%
rename from examples/cloud-operations/unmanaged-instances-healthcheck/function/restarter/restarter.go
rename to blueprints/cloud-operations/unmanaged-instances-healthcheck/function/restarter/restarter.go
diff --git a/blueprints/cloud-operations/unmanaged-instances-healthcheck/main.tf b/blueprints/cloud-operations/unmanaged-instances-healthcheck/main.tf
new file mode 100644
index 0000000000..19f2d467bd
--- /dev/null
+++ b/blueprints/cloud-operations/unmanaged-instances-healthcheck/main.tf
@@ -0,0 +1,250 @@
+# 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.
+
+
+###############################################################################
+# Project #
+###############################################################################
+
+module "project" {
+ source = "../../../modules/project"
+ name = var.project_id
+ parent = var.root_node
+ billing_account = var.billing_account
+ project_create = var.project_create
+ services = [
+ "vpcaccess.googleapis.com",
+ "compute.googleapis.com",
+ "cloudfunctions.googleapis.com",
+ "cloudbuild.googleapis.com",
+ "cloudscheduler.googleapis.com",
+ "pubsub.googleapis.com"
+ ]
+}
+
+###############################################################################
+# Network #
+###############################################################################
+
+module "vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = "vpc"
+ subnets = [
+ {
+ name = "apps"
+ ip_cidr_range = "10.8.32.0/24"
+ region = var.region
+ }
+ ]
+}
+
+module "firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.project.project_id
+ network = module.vpc.name
+}
+
+###############################################################################
+# Service Accounts #
+###############################################################################
+
+module "service-account-healthchecker" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.project.project_id
+ name = "healthckecker-cf"
+ iam_project_roles = {
+ (var.project_id) = [
+ "roles/compute.viewer",
+ "roles/logging.logWriter"
+ ]
+ }
+}
+
+module "service-account-restarter" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.project.project_id
+ name = "restarter-cf"
+ iam_project_roles = {
+ (var.project_id) = [
+ "roles/compute.instanceAdmin",
+ "roles/logging.logWriter"
+ ]
+ }
+}
+
+module "service-account-scheduler" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.project.project_id
+ name = "cloud-scheduler"
+}
+
+###############################################################################
+# Pub/Sub #
+###############################################################################
+
+module "pubsub" {
+ source = "../../../modules/pubsub"
+ project_id = module.project.project_id
+ name = "restarter"
+ iam = {
+ "roles/pubsub.publisher" = [module.service-account-healthchecker.iam_email]
+ }
+}
+
+###############################################################################
+# Cloud Function #
+###############################################################################
+
+module "cf-restarter" {
+ source = "../../../modules/cloud-function"
+ project_id = module.project.project_id
+ name = "cf-restarter"
+ region = var.region
+ bucket_name = "cf-bundle-bucket-${random_pet.random.id}"
+ bucket_config = {
+ location = var.region
+ }
+ bundle_config = {
+ source_dir = "${path.module}/function/restarter"
+ output_path = "restarter.zip"
+ }
+ service_account = module.service-account-restarter.email
+
+ function_config = {
+ entry_point = "RestartInstance"
+ ingress_settings = null
+ instance_count = 1
+ memory_mb = 256
+ runtime = "go116"
+ timeout = 300
+ }
+
+ trigger_config = {
+ v1 = {
+ event = "google.pubsub.topic.publish"
+ resource = module.pubsub.topic.id
+ }
+ }
+
+}
+
+module "cf-healthchecker" {
+ source = "../../../modules/cloud-function"
+ project_id = module.project.project_id
+ 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
+ instance_count = 1
+ memory_mb = 256
+ runtime = "go116"
+ timeout = 300
+ }
+ environment_variables = {
+ FILTER = "name = nginx-*"
+ GRACE_PERIOD = var.grace_period
+ PROJECT = module.project.project_id
+ PUBSUB_TOPIC = module.pubsub.topic.name
+ REGION = var.region
+ TCP_PORT = var.tcp_port
+ TIMEOUT = var.timeout
+ }
+ vpc_connector = {
+ create = true
+ name = "hc-connector"
+ egress_settings = "PRIVATE_RANGES_ONLY"
+
+ }
+
+ vpc_connector_config = {
+ ip_cidr_range = "10.132.0.0/28"
+ network = "vpc"
+ }
+
+ iam = {
+ "roles/cloudfunctions.invoker" = [module.service-account-scheduler.iam_email]
+ }
+
+ depends_on = [
+ module.vpc
+ ]
+}
+
+resource "random_pet" "random" {
+ length = 1
+}
+
+###############################################################################
+# Cloud Scheduler #
+###############################################################################
+
+resource "google_app_engine_application" "app" {
+ project = module.project.project_id
+ location_id = var.location
+}
+
+resource "google_cloud_scheduler_job" "healthcheck-job" {
+ project = google_app_engine_application.app.project
+ region = var.region
+ name = "healthchecker-schedule"
+ description = "Execute Compute Instance Healthcheck CF"
+ schedule = var.schedule
+ time_zone = "Etc/UTC"
+
+ http_target {
+ http_method = "GET"
+ uri = module.cf-healthchecker.function.https_trigger_url
+
+ oidc_token {
+ service_account_email = module.service-account-scheduler.email
+ }
+ }
+}
+
+###############################################################################
+# Test Nginx Instance #
+###############################################################################
+
+module "cos-nginx" {
+ source = "../../../modules/cloud-config-container/nginx"
+}
+
+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"]
+ }]
+ tags = ["ssh"]
+}
diff --git a/examples/cloud-operations/unmanaged-instances-healthcheck/outputs.tf b/blueprints/cloud-operations/unmanaged-instances-healthcheck/outputs.tf
similarity index 100%
rename from examples/cloud-operations/unmanaged-instances-healthcheck/outputs.tf
rename to blueprints/cloud-operations/unmanaged-instances-healthcheck/outputs.tf
diff --git a/blueprints/cloud-operations/unmanaged-instances-healthcheck/variables.tf b/blueprints/cloud-operations/unmanaged-instances-healthcheck/variables.tf
new file mode 100644
index 0000000000..14409a6643
--- /dev/null
+++ b/blueprints/cloud-operations/unmanaged-instances-healthcheck/variables.tf
@@ -0,0 +1,72 @@
+# 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.
+
+
+variable "billing_account" {
+ description = "Billing account id used as default for new projects."
+ type = string
+}
+
+variable "grace_period" {
+ description = "Grace period for an instance startup."
+ type = string
+ default = "180s"
+}
+
+variable "location" {
+ description = "App Engine location used in the example (required for CloudFunctions)."
+ type = string
+ default = "europe-west"
+}
+
+variable "project_create" {
+ description = "Create project instead of using an existing one."
+ type = bool
+ default = false
+}
+
+variable "project_id" {
+ description = "Project id to create a project when `project_create` is `true`, or to be used when `false`."
+ type = string
+}
+
+variable "region" {
+ description = "Compute region used in the example."
+ type = string
+ default = "europe-west1"
+}
+
+variable "root_node" {
+ description = "The resource name of the parent folder or organization for project creation, in 'folders/folder_id' or 'organizations/org_id' format."
+ type = string
+ default = null
+}
+
+variable "schedule" {
+ description = "Cron schedule for executing compute instances healthcheck."
+ type = string
+ default = "*/5 * * * *" # every five minutes
+}
+
+variable "tcp_port" {
+ description = "TCP port to run healthcheck against."
+ type = string
+ default = "80" #http
+}
+
+variable "timeout" {
+ description = "TCP probe timeout."
+ type = string
+ default = "1000ms"
+}
diff --git a/blueprints/cloud-operations/vm-migration/README.md b/blueprints/cloud-operations/vm-migration/README.md
new file mode 100644
index 0000000000..6aa44375e0
--- /dev/null
+++ b/blueprints/cloud-operations/vm-migration/README.md
@@ -0,0 +1,34 @@
+# Migrate for Compute Engine (v5) blueprints
+
+The blueprints in this folder implement **Migrate for Compute Engine (v5)** environments for the main migration scenarios like the ones with host and target project, or with shared VPC.
+
+They are meant to be used as minimal but complete starting points to create migration environment **on top of existing cloud foundations**, and as playgrounds to experiment with specific Google Cloud features.
+
+## Blueprints
+
+### M4CE on a single project
+
+ This [blueprint](./single-project/) implements a simple environment for Migrate for Compute Engine (v5) where both the API backend and the migration target environment are deployed on a single GCP project.
+
+This blueprint represents the easiest scenario to implement a Migrate for Compute Engine (v5) environment suitable for small migrations on simple environments or for product showcases.
+string
| ✓ | |
+| [vcenter_password](variables.tf#L48) | VCenter user password. | string
| ✓ | |
+| [vsphere_environment](variables.tf#L53) | VMVware VSphere connection parameters. | object({…})
| ✓ | |
+| [m4ce_appliance_properties](variables.tf#L15) | M4CE connector OVA image configuration parameters. | object({…})
| | {…}
|
+| [m4ce_connector_ovf_url](variables.tf#L37) | http URL to the public M4CE connector OVA image. | string
| | "https://storage.googleapis.com/vmmigration-public-artifacts/migrate-connector-2-0-1663.ova"
|
+
+
+## Manual Steps
+Once this blueprint is deployed a VCenter user has to be created and binded to the M4CE role in order to allow the connector access the VMWare resources.
+The user can be created manually through the VCenter web interface or through GOV commandline if it is available:
+```bash
+export GOVC_URL=list(string)
| ✓ | |
+| [migration_target_projects](variables.tf#L20) | List of target projects for m4ce workload migrations. | list(string)
| ✓ | |
+| [migration_viewer_users](variables.tf#L25) | List of users authorized to retrive information about M4CE in the Google Cloud Console, in IAM format. | list(string)
| | []
|
+| [project_create](variables.tf#L31) | Parameters for the creation of the new project to host the M4CE backend. | object({…})
| | null
|
+| [project_name](variables.tf#L40) | Name of an existing project or of the new project assigned as M4CE host project. | string
| | "m4ce-host-project-000"
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [m4ce_gmanaged_service_account](outputs.tf#L15) | Google managed service account created automatically during the migrate connector registration.. It is used by M4CE to perform activities on target projects. | |
+
+
diff --git a/examples/cloud-operations/vm-migration/host-target-projects/backend.tf.sample b/blueprints/cloud-operations/vm-migration/host-target-projects/backend.tf.sample
similarity index 100%
rename from examples/cloud-operations/vm-migration/host-target-projects/backend.tf.sample
rename to blueprints/cloud-operations/vm-migration/host-target-projects/backend.tf.sample
diff --git a/examples/cloud-operations/vm-migration/host-target-projects/diagram.png b/blueprints/cloud-operations/vm-migration/host-target-projects/diagram.png
similarity index 100%
rename from examples/cloud-operations/vm-migration/host-target-projects/diagram.png
rename to blueprints/cloud-operations/vm-migration/host-target-projects/diagram.png
diff --git a/examples/cloud-operations/vm-migration/host-target-projects/main.tf b/blueprints/cloud-operations/vm-migration/host-target-projects/main.tf
similarity index 100%
rename from examples/cloud-operations/vm-migration/host-target-projects/main.tf
rename to blueprints/cloud-operations/vm-migration/host-target-projects/main.tf
diff --git a/blueprints/cloud-operations/vm-migration/host-target-projects/outputs.tf b/blueprints/cloud-operations/vm-migration/host-target-projects/outputs.tf
new file mode 100644
index 0000000000..2db8f1ae9e
--- /dev/null
+++ b/blueprints/cloud-operations/vm-migration/host-target-projects/outputs.tf
@@ -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
+#
+# 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 "m4ce_gmanaged_service_account" {
+ description = "Google managed service account created automatically during the migrate connector registration.. It is used by M4CE to perform activities on target projects."
+ value = "serviceAccount:service-${module.host-project.number}@gcp-sa-vmmigration.iam.gserviceaccount.com"
+}
diff --git a/blueprints/cloud-operations/vm-migration/host-target-projects/variables.tf b/blueprints/cloud-operations/vm-migration/host-target-projects/variables.tf
new file mode 100644
index 0000000000..c210fa3159
--- /dev/null
+++ b/blueprints/cloud-operations/vm-migration/host-target-projects/variables.tf
@@ -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
+#
+# 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.
+
+variable "migration_admin_users" {
+ description = "List of users authorized to create a new M4CE sources and perform all other migration operations, in IAM format."
+ type = list(string)
+}
+
+variable "migration_target_projects" {
+ description = "List of target projects for m4ce workload migrations."
+ type = list(string)
+}
+
+variable "migration_viewer_users" {
+ description = "List of users authorized to retrive information about M4CE in the Google Cloud Console, in IAM format."
+ type = list(string)
+ default = []
+}
+
+variable "project_create" {
+ description = "Parameters for the creation of the new project to host the M4CE backend."
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = null
+}
+
+variable "project_name" {
+ description = "Name of an existing project or of the new project assigned as M4CE host project."
+ type = string
+ default = "m4ce-host-project-000"
+}
diff --git a/blueprints/cloud-operations/vm-migration/host-target-sharedvpc/README.md b/blueprints/cloud-operations/vm-migration/host-target-sharedvpc/README.md
new file mode 100644
index 0000000000..bb34cf8ff3
--- /dev/null
+++ b/blueprints/cloud-operations/vm-migration/host-target-sharedvpc/README.md
@@ -0,0 +1,44 @@
+# M4CE(v5) - Host and Target Projects with Shared VPC
+
+This blueprint creates a Migrate for Compute Engine (v5) environment deployed on an host project with multiple [target projects](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#identifying_your_host_project) and shared VPCs.
+
+The blueprint is designed to implement a M4CE (v5) environment on-top of complex migration landing environment where VMs have to be migrated to multiple target projects. In this blueprint targets are also service projects for a shared VPC. It also includes the IAM wiring needed to make such scenarios work.
+
+This is the high level diagram:
+
+![High-level diagram](diagram.png "High-level diagram")
+
+## Managed resources and services
+
+This sample creates\update several distinct groups of resources:
+
+- projects
+ - M4CE host project with [required services](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#enabling_required_services_on_the_host_project) deployed on a new or existing project.
+ - M4CE target project prerequisites deployed on existing projects.
+- IAM
+ - Create a [service account](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/migrate-connector#step-3) used at runtime by the M4CE connector for data replication
+ - Grant [migration admin roles](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#using_predefined_roles) to provided user accounts.
+ - Grant [migration viewer role](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#using_predefined_roles) to provided user accounts.
+ - Grant [roles on shared VPC](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/target-project#configure-permissions) to migration admins
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [migration_admin_users](variables.tf#L15) | List of users authorized to create a new M4CE sources and perform all other migration operations, in IAM format. | list(string)
| ✓ | |
+| [migration_target_projects](variables.tf#L20) | List of target projects for m4ce workload migrations. | list(string)
| ✓ | |
+| [sharedvpc_host_projects](variables.tf#L45) | List of host projects that share a VPC with the selected target projects. | list(string)
| ✓ | |
+| [migration_viewer_users](variables.tf#L25) | List of users authorized to retrive information about M4CE in the Google Cloud Console, in IAM format. | list(string)
| | []
|
+| [project_create](variables.tf#L30) | Parameters for the creation of the new project to host the M4CE backend. | object({…})
| | null
|
+| [project_name](variables.tf#L39) | Name of an existing project or of the new project assigned as M4CE host project. | string
| | "m4ce-host-project-000"
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [m4ce_gmanaged_service_account](outputs.tf#L15) | Google managed service account created automatically during the migrate connector registration. It is used by M4CE to perform activities on target projects. | |
+
+
+## Manual Steps
+Once this blueprint is deployed the M4CE [m4ce_gmanaged_service_account](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/target-sa-compute-engine#configuring_the_default_service_account) has to be configured to grant the access to the shared VPC and allow the deploy of Compute Engine instances as the result of the migration.
diff --git a/examples/cloud-operations/vm-migration/host-target-sharedvpc/backend.tf.sample b/blueprints/cloud-operations/vm-migration/host-target-sharedvpc/backend.tf.sample
similarity index 100%
rename from examples/cloud-operations/vm-migration/host-target-sharedvpc/backend.tf.sample
rename to blueprints/cloud-operations/vm-migration/host-target-sharedvpc/backend.tf.sample
diff --git a/examples/cloud-operations/vm-migration/host-target-sharedvpc/diagram.png b/blueprints/cloud-operations/vm-migration/host-target-sharedvpc/diagram.png
similarity index 100%
rename from examples/cloud-operations/vm-migration/host-target-sharedvpc/diagram.png
rename to blueprints/cloud-operations/vm-migration/host-target-sharedvpc/diagram.png
diff --git a/examples/cloud-operations/vm-migration/host-target-sharedvpc/main.tf b/blueprints/cloud-operations/vm-migration/host-target-sharedvpc/main.tf
similarity index 100%
rename from examples/cloud-operations/vm-migration/host-target-sharedvpc/main.tf
rename to blueprints/cloud-operations/vm-migration/host-target-sharedvpc/main.tf
diff --git a/blueprints/cloud-operations/vm-migration/host-target-sharedvpc/outputs.tf b/blueprints/cloud-operations/vm-migration/host-target-sharedvpc/outputs.tf
new file mode 100644
index 0000000000..c772de5f6e
--- /dev/null
+++ b/blueprints/cloud-operations/vm-migration/host-target-sharedvpc/outputs.tf
@@ -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
+#
+# 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 "m4ce_gmanaged_service_account" {
+ description = "Google managed service account created automatically during the migrate connector registration. It is used by M4CE to perform activities on target projects."
+ value = "serviceAccount:service-${module.host-project.number}@gcp-sa-vmmigration.iam.gserviceaccount.com"
+}
diff --git a/blueprints/cloud-operations/vm-migration/host-target-sharedvpc/variables.tf b/blueprints/cloud-operations/vm-migration/host-target-sharedvpc/variables.tf
new file mode 100644
index 0000000000..c01740dc47
--- /dev/null
+++ b/blueprints/cloud-operations/vm-migration/host-target-sharedvpc/variables.tf
@@ -0,0 +1,48 @@
+# 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.
+
+variable "migration_admin_users" {
+ description = "List of users authorized to create a new M4CE sources and perform all other migration operations, in IAM format."
+ type = list(string)
+}
+
+variable "migration_target_projects" {
+ description = "List of target projects for m4ce workload migrations."
+ type = list(string)
+}
+
+variable "migration_viewer_users" {
+ description = "List of users authorized to retrive information about M4CE in the Google Cloud Console, in IAM format."
+ type = list(string)
+ default = []
+}
+variable "project_create" {
+ description = "Parameters for the creation of the new project to host the M4CE backend."
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = null
+}
+
+variable "project_name" {
+ description = "Name of an existing project or of the new project assigned as M4CE host project."
+ type = string
+ default = "m4ce-host-project-000"
+}
+
+variable "sharedvpc_host_projects" {
+ description = "List of host projects that share a VPC with the selected target projects."
+ type = list(string)
+}
diff --git a/blueprints/cloud-operations/vm-migration/single-project/README.md b/blueprints/cloud-operations/vm-migration/single-project/README.md
new file mode 100644
index 0000000000..20afd4a9b4
--- /dev/null
+++ b/blueprints/cloud-operations/vm-migration/single-project/README.md
@@ -0,0 +1,41 @@
+# M4CE(v5) - Single Project
+
+This blueprint creates a simple M4CE (v5) environment deployed on a single [host project](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#identifying_your_host_project).
+
+The blueprint is designed for quick tests or product demos where it is required to setup a simple and minimal M4CE (v5) environment. It also includes the IAM wiring needed to make such scenarios work.
+
+This is the high level diagram:
+
+![High-level diagram](diagram.png "High-level diagram")
+
+## Managed resources and services
+
+This sample creates several distinct groups of resources:
+
+- projects
+ - M4CE host project with [required services](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#enabling_required_services_on_the_host_project) deployed on a new or existing project.
+- networking
+ - Default VPC network
+- IAM
+ - One [service account](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/migrate-connector#step-3) used at runtime by the M4CE connector for data replication
+ - Grant [migration admin roles](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#using_predefined_roles) to admin user accounts
+ - Grant [migration viewer role](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#using_predefined_roles) to viewer user accounts
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [migration_admin_users](variables.tf#L15) | List of users authorized to create a new M4CE sources and perform all other migration operations, in IAM format. | list(string)
| ✓ | |
+| [migration_viewer_users](variables.tf#L20) | List of users authorized to retrive information about M4CE in the Google Cloud Console, in IAM format. | list(string)
| | []
|
+| [project_create](variables.tf#L26) | Parameters for the creation of the new project to host the M4CE backend. | object({…})
| | null
|
+| [project_name](variables.tf#L35) | Name of an existing project or of the new project assigned as M4CE host an target project. | string
| | "m4ce-host-project-000"
|
+| [vpc_config](variables.tf#L41) | Parameters to create a simple VPC on the M4CE project. | object({…})
| | {…}
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [m4ce_gmanaged_service_account](outputs.tf#L15) | Google managed service account created automatically during the migrate connector registration. It is used by M4CE to perform activities on target projects. | |
+
+
diff --git a/examples/cloud-operations/vm-migration/single-project/backend.tf.sample b/blueprints/cloud-operations/vm-migration/single-project/backend.tf.sample
similarity index 100%
rename from examples/cloud-operations/vm-migration/single-project/backend.tf.sample
rename to blueprints/cloud-operations/vm-migration/single-project/backend.tf.sample
diff --git a/examples/cloud-operations/vm-migration/single-project/diagram.png b/blueprints/cloud-operations/vm-migration/single-project/diagram.png
similarity index 100%
rename from examples/cloud-operations/vm-migration/single-project/diagram.png
rename to blueprints/cloud-operations/vm-migration/single-project/diagram.png
diff --git a/blueprints/cloud-operations/vm-migration/single-project/main.tf b/blueprints/cloud-operations/vm-migration/single-project/main.tf
new file mode 100644
index 0000000000..a6fee84308
--- /dev/null
+++ b/blueprints/cloud-operations/vm-migration/single-project/main.tf
@@ -0,0 +1,72 @@
+# 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.
+
+module "landing-project" {
+ source = "../../../../modules/project"
+ billing_account = (var.project_create != null
+ ? var.project_create.billing_account_id
+ : null
+ )
+ name = var.project_name
+ parent = (var.project_create != null
+ ? var.project_create.parent
+ : null
+ )
+
+ services = [
+ "cloudresourcemanager.googleapis.com",
+ "compute.googleapis.com",
+ "iam.googleapis.com",
+ "logging.googleapis.com",
+ "networkconnectivity.googleapis.com",
+ "servicemanagement.googleapis.com",
+ "servicecontrol.googleapis.com",
+ "vmmigration.googleapis.com"
+ ]
+
+ project_create = var.project_create != null
+
+ iam_additive = {
+ "roles/iam.serviceAccountKeyAdmin" = var.migration_admin_users,
+ "roles/iam.serviceAccountCreator" = var.migration_admin_users,
+ "roles/vmmigration.admin" = var.migration_admin_users,
+ "roles/vmmigration.viewer" = var.migration_viewer_users
+ }
+}
+
+module "m4ce-service-account" {
+ source = "../../../../modules/iam-service-account"
+ project_id = module.landing-project.project_id
+ name = "m4ce-sa"
+ generate_key = true
+}
+
+module "landing-vpc" {
+ source = "../../../../modules/net-vpc"
+ project_id = module.landing-project.project_id
+ name = "landing-vpc"
+ subnets = [
+ {
+ ip_cidr_range = var.vpc_config.ip_cidr_range
+ name = "landing-vpc-${var.vpc_config.region}"
+ region = var.vpc_config.region
+ }
+ ]
+}
+
+module "landing-vpc-firewall" {
+ source = "../../../../modules/net-vpc-firewall"
+ project_id = module.landing-project.project_id
+ network = module.landing-vpc.name
+}
diff --git a/blueprints/cloud-operations/vm-migration/single-project/outputs.tf b/blueprints/cloud-operations/vm-migration/single-project/outputs.tf
new file mode 100644
index 0000000000..269bb2bd52
--- /dev/null
+++ b/blueprints/cloud-operations/vm-migration/single-project/outputs.tf
@@ -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
+#
+# 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 "m4ce_gmanaged_service_account" {
+ description = "Google managed service account created automatically during the migrate connector registration. It is used by M4CE to perform activities on target projects."
+ value = "serviceAccount:service-${module.landing-project.number}@gcp-sa-vmmigration.iam.gserviceaccount.com"
+}
diff --git a/blueprints/cloud-operations/vm-migration/single-project/variables.tf b/blueprints/cloud-operations/vm-migration/single-project/variables.tf
new file mode 100644
index 0000000000..3335254f23
--- /dev/null
+++ b/blueprints/cloud-operations/vm-migration/single-project/variables.tf
@@ -0,0 +1,51 @@
+# 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.
+
+variable "migration_admin_users" {
+ description = "List of users authorized to create a new M4CE sources and perform all other migration operations, in IAM format."
+ type = list(string)
+}
+
+variable "migration_viewer_users" {
+ description = "List of users authorized to retrive information about M4CE in the Google Cloud Console, in IAM format."
+ type = list(string)
+ default = []
+}
+
+variable "project_create" {
+ description = "Parameters for the creation of the new project to host the M4CE backend."
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = null
+}
+
+variable "project_name" {
+ description = "Name of an existing project or of the new project assigned as M4CE host an target project."
+ type = string
+ default = "m4ce-host-project-000"
+}
+
+variable "vpc_config" {
+ description = "Parameters to create a simple VPC on the M4CE project."
+ type = object({
+ ip_cidr_range = string,
+ region = string
+ })
+ default = {
+ ip_cidr_range = "10.200.0.0/20",
+ region = "us-west2"
+ }
+}
diff --git a/blueprints/cloud-operations/workload-identity-federation/README.md b/blueprints/cloud-operations/workload-identity-federation/README.md
new file mode 100644
index 0000000000..ad6feaede1
--- /dev/null
+++ b/blueprints/cloud-operations/workload-identity-federation/README.md
@@ -0,0 +1,95 @@
+# Configuring Workload Identity Federation to access Google Cloud resources from apps running on Azure
+
+The most straightforward way for workloads running outside of Google Cloud to call Google Cloud APIs is by using a downloaded service account key. However, this approach has 2 major pain points:
+
+* A management hassle, keys need to be stored securely and rotated often.
+* A security risk, keys are long term credentials that could be compromised.
+
+Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account.
+
+This blueprint shows how to set up everything, both in Azure and Google Cloud, so a workload in Azure can access Google Cloud resources without a service account key. This will be possible by configuring workload identity federation to trust access tokens generated for a specific application in an Azure Active Directory (AAD) tenant.
+
+The following diagram illustrates how the VM will get a short-lived access token and use it to access a resource:
+
+ ![Sequence diagram](sequence_diagram.png)
+
+The provided terraform configuration will set up the following architecture:
+
+ ![Architecture](architecture.png)
+
+* On Azure:
+
+ * An Azure Active Directory application and a service principal. By default, the new application grants all users in the Azure AD tenant permission to obtain access tokens. So an app role assignment will be required to restrict which identities can obtain access tokens for the application.
+
+ * Optionally, all the resources required to have a VM configured to run with a system-assigned managed identity and accessible via SSH on a public IP using public key authentication, so we can log in to the machine and run the `gcloud` command to verify that everything works as expected.
+
+* On Google Cloud:
+
+ * A Google Cloud project with:
+
+ * A workload identity pool and provider configured to trust the AAD application
+
+ * A service account with the Viewer role granted on the project. The external identities in the workload identity pool would be assigned the Workload Identity User role on that service account.
+
+## Running the blueprint
+
+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%2Fworkload-identity-federation), then go through the following steps to create resources:
+
+* `terraform init`
+* `terraform apply -var project_id=my-project-id`
+
+## Testing the blueprint
+
+Once the resources have been created, do the following to verify that everything works as expected:
+
+1. Log in to the VM.
+
+ If you have created the VM using this terraform configuration proceed the following way:
+
+ * Copy the public IP address of the Azure VM and the username required to log in to the VM via SSH from the output.
+
+ * Save the private key to a file
+
+ `terraform state pull | jq -r '.outputs.tls_private_key.value' > private_key.pem`
+
+ * Change the permissions on the private key file to 600
+
+ `chmod 600 private_key.pem`
+
+ * Login to the Azure VM using the following command:
+
+ `ssh -i private_key.pem azureuser@VM_PUBLIC_IP`
+
+ If you already had an existing VM with the gcloud CLI installed that you want to use, you will have assign its managed identity an application role as explained [here](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-assign-app-role-managed-identity-powershell#assign-a-managed-identity-access-to-another-applications-app-role).
+
+2. Create a file called credential.json in the VM with the contents of the `credential` output.
+
+3. Authorize gcloud to access Google Cloud with the credentials file created in the step before.
+
+ `gcloud auth login --cred-file credential.json
+
+4. Get the Google Cloud project details
+
+ `gcloud projects describe PROJECT_ID`
+
+Once done testing, you can clean up resources by running `terraform destroy`.
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [project_id](variables.tf#L26) | Identifier of the project that will contain the Pub/Sub topic that will be created from Azure and the service account that will be impersonated. | string
| ✓ | |
+| [project_create](variables.tf#L17) | Parameters for the creation of the new project. | object({…})
| | null
|
+| [vm_test](variables.tf#L31) | Flag indicating whether the infrastructure required to test that everything works should be created in Azure. | bool
| | false
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [credential](outputs.tf#L17) | Credential configuration file contents. | |
+| [tls_private_key](outputs.tf#L28) | Private key required to log in to the Azure VM via SSH. | ✓ |
+| [username](outputs.tf#L34) | Username required to log in to the Azure VM via SSH. | |
+| [vm_public_ip_address](outputs.tf#L39) | Azure VM public IP address. | |
+
+
diff --git a/examples/cloud-operations/workload-identity-federation/architecture.png b/blueprints/cloud-operations/workload-identity-federation/architecture.png
similarity index 100%
rename from examples/cloud-operations/workload-identity-federation/architecture.png
rename to blueprints/cloud-operations/workload-identity-federation/architecture.png
diff --git a/examples/cloud-operations/workload-identity-federation/azure.tf b/blueprints/cloud-operations/workload-identity-federation/azure.tf
similarity index 100%
rename from examples/cloud-operations/workload-identity-federation/azure.tf
rename to blueprints/cloud-operations/workload-identity-federation/azure.tf
diff --git a/examples/cloud-operations/workload-identity-federation/credential.json b/blueprints/cloud-operations/workload-identity-federation/credential.json
similarity index 100%
rename from examples/cloud-operations/workload-identity-federation/credential.json
rename to blueprints/cloud-operations/workload-identity-federation/credential.json
diff --git a/examples/cloud-operations/workload-identity-federation/google-cloud.tf b/blueprints/cloud-operations/workload-identity-federation/google-cloud.tf
similarity index 100%
rename from examples/cloud-operations/workload-identity-federation/google-cloud.tf
rename to blueprints/cloud-operations/workload-identity-federation/google-cloud.tf
diff --git a/examples/cloud-operations/workload-identity-federation/outputs.tf b/blueprints/cloud-operations/workload-identity-federation/outputs.tf
similarity index 100%
rename from examples/cloud-operations/workload-identity-federation/outputs.tf
rename to blueprints/cloud-operations/workload-identity-federation/outputs.tf
diff --git a/examples/cloud-operations/workload-identity-federation/sequence_diagram.png b/blueprints/cloud-operations/workload-identity-federation/sequence_diagram.png
similarity index 100%
rename from examples/cloud-operations/workload-identity-federation/sequence_diagram.png
rename to blueprints/cloud-operations/workload-identity-federation/sequence_diagram.png
diff --git a/examples/cloud-operations/workload-identity-federation/setup.sh b/blueprints/cloud-operations/workload-identity-federation/setup.sh
similarity index 100%
rename from examples/cloud-operations/workload-identity-federation/setup.sh
rename to blueprints/cloud-operations/workload-identity-federation/setup.sh
diff --git a/examples/cloud-operations/workload-identity-federation/variables.tf b/blueprints/cloud-operations/workload-identity-federation/variables.tf
similarity index 100%
rename from examples/cloud-operations/workload-identity-federation/variables.tf
rename to blueprints/cloud-operations/workload-identity-federation/variables.tf
diff --git a/blueprints/data-solutions/README.md b/blueprints/data-solutions/README.md
new file mode 100644
index 0000000000..4919f29a42
--- /dev/null
+++ b/blueprints/data-solutions/README.md
@@ -0,0 +1,57 @@
+# GCP Data Services blueprints
+
+The blueprints in this folder implement **typical data service topologies** and **end-to-end scenarios**, that allow testing specific features like Cloud KMS to encrypt your data, or VPC-SC to mitigate data exfiltration.
+
+They are meant to be used as minimal but complete starting points to create actual infrastructure, and as playgrounds to experiment with specific Google Cloud features.
+
+## Blueprints
+
+### Cloud SQL instance with multi-region read replicas
+
+
+This [blueprint](./cloudsql-multiregion/) creates a [Cloud SQL instance](https://cloud.google.com/sql) with multi-region read replicas as described in the [Cloud SQL for PostgreSQL disaster recovery](https://cloud.google.com/architecture/cloud-sql-postgres-disaster-recovery-complete-failover-fallback) article.
+
+string
| ✓ | |
+| [prefix](variables.tf#L45) | Prefix used for resource names. | string
| ✓ | |
+| [project_id](variables.tf#L63) | Project id, references existing project if `project_create` is null. | string
| ✓ | |
+| [data_eng_principals](variables.tf#L17) | Groups with Service Account Token creator role on service accounts in IAM format, only user supported on CloudSQL, eg 'user@domain.com'. | list(string)
| | []
|
+| [network_config](variables.tf#L23) | Shared VPC network configurations to use. If null networks will be created in projects with preconfigured values. | object({…})
| | null
|
+| [postgres_database](variables.tf#L34) | `postgres` database. | string
| | "guestbook"
|
+| [project_create](variables.tf#L54) | Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…})
| | null
|
+| [regions](variables.tf#L68) | Map of instance_name => location where instances will be deployed. | map(string)
| | {…}
|
+| [service_encryption_keys](variables.tf#L81) | Cloud KMS keys to use to encrypt resources. Provide a key for each reagion configured. | map(string)
| | null
|
+| [sql_configuration](variables.tf#L87) | Cloud SQL configuration. | object({…})
| | {…}
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [bucket](outputs.tf#L17) | Cloud storage bucket to import/export data from Cloud SQL. | |
+| [connection_names](outputs.tf#L22) | Connection name of each instance. | |
+| [demo_commands](outputs.tf#L27) | Demo commands. | |
+| [ips](outputs.tf#L36) | IP address of each instance. | |
+| [project_id](outputs.tf#L41) | ID of the project containing all the instances. | |
+| [service_accounts](outputs.tf#L46) | Service Accounts. | |
+
+
diff --git a/examples/data-solutions/data-platform-foundations/backend.tf.sample b/blueprints/data-solutions/cloudsql-multiregion/backend.tf.sample
similarity index 100%
rename from examples/data-solutions/data-platform-foundations/backend.tf.sample
rename to blueprints/data-solutions/cloudsql-multiregion/backend.tf.sample
diff --git a/blueprints/data-solutions/cloudsql-multiregion/cloudsql.tf b/blueprints/data-solutions/cloudsql-multiregion/cloudsql.tf
new file mode 100644
index 0000000000..796cc1b6c3
--- /dev/null
+++ b/blueprints/data-solutions/cloudsql-multiregion/cloudsql.tf
@@ -0,0 +1,62 @@
+# 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.
+
+module "db" {
+ source = "../../../modules/cloudsql-instance"
+ project_id = module.project.project_id
+ availability_type = var.sql_configuration.availability_type
+ encryption_key_name = var.service_encryption_keys != null ? try(var.service_encryption_keys[var.regions.primary], null) : null
+ network = local.vpc_self_link
+ name = "${var.prefix}-db"
+ region = var.regions.primary
+ database_version = var.sql_configuration.database_version
+ tier = var.sql_configuration.tier
+ flags = {
+ "cloudsql.iam_authentication" = "on"
+ }
+ replicas = {
+ for k, v in var.regions :
+ k => {
+ region = v,
+ encryption_key_name = var.service_encryption_keys != null ? try(var.service_encryption_keys[v], null) : null
+ } if k != "primary"
+ }
+ databases = [var.postgres_database]
+ users = {
+ postgres = var.postgres_user_password
+ }
+}
+
+resource "google_sql_user" "users" {
+ for_each = toset(var.data_eng_principals)
+ project = module.project.project_id
+ name = each.value
+ instance = module.db.name
+ type = "CLOUD_IAM_USER"
+}
+
+resource "google_sql_user" "service-account" {
+ for_each = toset(var.data_eng_principals)
+ project = module.project.project_id
+ # Omit the .gserviceaccount.com suffix in the email
+ name = regex("(.+)(.gserviceaccount)", module.service-account-sql.email)[0]
+ instance = module.db.name
+ type = "CLOUD_IAM_SERVICE_ACCOUNT"
+}
+
+module "service-account-sql" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.project.project_id
+ name = "${var.prefix}-sql"
+}
diff --git a/blueprints/data-solutions/cloudsql-multiregion/datastorage.tf b/blueprints/data-solutions/cloudsql-multiregion/datastorage.tf
new file mode 100644
index 0000000000..1b45a97bf9
--- /dev/null
+++ b/blueprints/data-solutions/cloudsql-multiregion/datastorage.tf
@@ -0,0 +1,30 @@
+# 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.
+
+module "gcs" {
+ source = "../../../modules/gcs"
+ project_id = module.project.project_id
+ prefix = var.prefix
+ name = "data"
+ location = var.regions.primary
+ storage_class = "REGIONAL"
+ encryption_key = var.service_encryption_keys != null ? try(var.service_encryption_keys[var.regions.primary], null) : null
+ force_destroy = true
+}
+
+module "service-account-gcs" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.project.project_id
+ name = "${var.prefix}-gcs"
+}
diff --git a/blueprints/data-solutions/cloudsql-multiregion/gce.tf b/blueprints/data-solutions/cloudsql-multiregion/gce.tf
new file mode 100644
index 0000000000..17048aa887
--- /dev/null
+++ b/blueprints/data-solutions/cloudsql-multiregion/gce.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
+#
+# 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.
+
+locals {
+ startup-script = <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"
|
+| [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"
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [bucket](outputs.tf#L15) | GCS Bucket URL. | |
+| [bucket_keys](outputs.tf#L20) | GCS Bucket Cloud KMS crypto keys. | |
+| [projects](outputs.tf#L25) | Project ids. | |
+| [vm](outputs.tf#L33) | GCE VM. | |
+| [vm_keys](outputs.tf#L41) | GCE VM Cloud KMS crypto keys. | |
+
+
diff --git a/examples/data-solutions/cmek-via-centralized-kms/backend.tf.sample b/blueprints/data-solutions/cmek-via-centralized-kms/backend.tf.sample
similarity index 100%
rename from examples/data-solutions/cmek-via-centralized-kms/backend.tf.sample
rename to blueprints/data-solutions/cmek-via-centralized-kms/backend.tf.sample
diff --git a/examples/data-solutions/cmek-via-centralized-kms/diagram.png b/blueprints/data-solutions/cmek-via-centralized-kms/diagram.png
similarity index 100%
rename from examples/data-solutions/cmek-via-centralized-kms/diagram.png
rename to blueprints/data-solutions/cmek-via-centralized-kms/diagram.png
diff --git a/blueprints/data-solutions/cmek-via-centralized-kms/main.tf b/blueprints/data-solutions/cmek-via-centralized-kms/main.tf
new file mode 100644
index 0000000000..fb7f9fdd16
--- /dev/null
+++ b/blueprints/data-solutions/cmek-via-centralized-kms/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
+#
+# 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.
+
+###############################################################################
+# Projects #
+###############################################################################
+
+module "project-service" {
+ source = "../../../modules/project"
+ name = var.project_service_name
+ parent = var.root_node
+ billing_account = var.billing_account
+ services = [
+ "compute.googleapis.com",
+ "servicenetworking.googleapis.com",
+ "storage-component.googleapis.com"
+ ]
+ oslogin = true
+}
+
+module "project-kms" {
+ source = "../../../modules/project"
+ name = var.project_kms_name
+ parent = var.root_node
+ billing_account = var.billing_account
+ services = [
+ "cloudkms.googleapis.com",
+ "servicenetworking.googleapis.com"
+ ]
+ oslogin = true
+}
+
+###############################################################################
+# Networking #
+###############################################################################
+
+module "vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project-service.project_id
+ name = var.vpc_name
+ subnets = [
+ {
+ ip_cidr_range = var.vpc_ip_cidr_range
+ name = var.vpc_subnet_name
+ region = var.region
+ }
+ ]
+}
+
+module "vpc-firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.project-service.project_id
+ network = module.vpc.name
+ default_rules_config = {
+ admin_ranges = [var.vpc_ip_cidr_range]
+ }
+}
+
+###############################################################################
+# KMS #
+###############################################################################
+
+module "kms" {
+ source = "../../../modules/kms"
+ project_id = module.project-kms.project_id
+ keyring = {
+ name = "my-keyring",
+ location = var.location
+ }
+ 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}",
+ ]
+ }
+ }
+}
+
+###############################################################################
+# GCE #
+###############################################################################
+
+module "vm_example" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project-service.project_id
+ zone = "${var.region}-b"
+ name = "kms-vm"
+ network_interfaces = [{
+ network = module.vpc.self_link,
+ subnetwork = module.vpc.subnet_self_links["${var.region}/subnet"],
+ nat = false,
+ addresses = null
+ }]
+ attached_disks = [
+ {
+ name = "data"
+ size = 10
+ source = null
+ source_type = null
+ options = null
+ }
+ ]
+ boot_disk = {
+ image = "projects/debian-cloud/global/images/family/debian-10"
+ type = "pd-ssd"
+ size = 10
+ encrypt_disk = true
+ }
+ tags = ["ssh"]
+ encryption = {
+ encrypt_boot = true
+ disk_encryption_key_raw = null
+ kms_key_self_link = module.kms.key_ids.key-gce
+ }
+}
+
+###############################################################################
+# GCS #
+###############################################################################
+
+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
+}
diff --git a/examples/data-solutions/cmek-via-centralized-kms/outputs.tf b/blueprints/data-solutions/cmek-via-centralized-kms/outputs.tf
similarity index 100%
rename from examples/data-solutions/cmek-via-centralized-kms/outputs.tf
rename to blueprints/data-solutions/cmek-via-centralized-kms/outputs.tf
diff --git a/examples/data-solutions/cmek-via-centralized-kms/variables.tf b/blueprints/data-solutions/cmek-via-centralized-kms/variables.tf
similarity index 100%
rename from examples/data-solutions/cmek-via-centralized-kms/variables.tf
rename to blueprints/data-solutions/cmek-via-centralized-kms/variables.tf
diff --git a/blueprints/data-solutions/cmek-via-centralized-kms/versions.tf b/blueprints/data-solutions/cmek-via-centralized-kms/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/data-solutions/cmek-via-centralized-kms/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/data-solutions/composer-2/README.md b/blueprints/data-solutions/composer-2/README.md
new file mode 100644
index 0000000000..bc51aaa4fb
--- /dev/null
+++ b/blueprints/data-solutions/composer-2/README.md
@@ -0,0 +1,115 @@
+# Cloud Composer version 2 private instance, supporting Shared VPC and external CMEK key
+
+This blueprint creates a Private instance of [Cloud Composer version 2](https://cloud.google.com/composer/docs/composer-2/composer-versioning-overview) on a VPC with a dedicated service account. Cloud Composer 2 is the new major version for Cloud Composer that supports:
+ - environment autoscaling
+ - workloads configuration: CPU, memory, and storage parameters for Airflow workers, schedulers, web server, and database.
+
+Please consult the [documentation page](https://cloud.google.com/composer/docs/composer-2/composer-versioning-overview) for an exhaustive comparison between Composer Version 1 and Version 2.
+
+The solution will use:
+ - Cloud Composer
+ - VPC with Private Service Access to deploy resources, if no Shared VPC configuration provided.
+ - Google Cloud NAT to access internet resources, if no Shared VPC configuration provided.
+
+The solution supports as inputs:
+ - Shared VPC
+ - Cloud KMS CMEK keys
+
+This is the high level diagram:
+
+![Cloud Composer 2 architecture overview](./diagram.png "Cloud Composer 2 architecture overview")
+
+# Requirements
+This blueprint will deploy all its resources into the project defined by the project_id variable. Please note that we assume this project already exists. However, if you provide the appropriate values to the `project_create` variable, the project will be created as part of the deployment.
+
+If `project_create` is left to null, the identity performing the deployment needs the owner role on the project defined by the `project_id` variable. Otherwise, the identity performing the deployment needs `resourcemanager.projectCreator` on the resource hierarchy node specified by `project_create.parent` and `billing.user` on the billing account specified by `project_create.billing_account_id`.
+
+# Deployment
+Run Terraform init:
+
+```bash
+$ terraform init
+```
+
+Configure the Terraform variable in your terraform.tfvars file. You need to specify at least the following variables:
+
+```tfvars
+project_id = "lcaggioni-sandbox"
+prefix = "lc"
+```
+
+You can run now:
+
+```bash
+$ terraform apply
+```
+
+You can now connect to your instance.
+
+# Customizations
+
+## VPC
+If a shared VPC is not configured, a VPC will be created within the project. The following IP ranges will be used:
+- Cloudsql: `10.20.10.0/24`
+- GKE: `10.20.11.0/28`
+
+Change the code as needed to match your needed configuration, remember that these addresses should not overlap with any other range used in network.
+## Shared VPC
+As is often the case in real-world configurations, this blueprint accepts as input an existing [`Shared-VPC`](https://cloud.google.com/vpc/docs/shared-vpc) via the `network_config` variable.
+
+Example:
+```tfvars
+network_config = {
+ host_project = "PROJECT"
+ network_self_link = "projects/PROJECT/global/networks/VPC_NAME"
+ subnet_self_link = "projects/PROJECT/regions/REGION/subnetworks/VPC_NAME"
+ composer_secondary_ranges = {
+ pods = "pods"
+ services = "services"
+ }
+}
+```
+
+Make sure that:
+- The GKE API (`container.googleapis.com`) is enabled in the VPC host project.
+- The subnet has secondary ranges configured with 2 ranges:
+ - pods: `/22` example: `10.10.8.0/22`
+ - services = `/24` example: 10.10.12.0/24`
+- Firewall rules are set, as described in the [documentation](https://cloud.google.com/composer/docs/composer-2/configure-private-ip#step_3_configure_firewall_rules)
+
+In order to run the example and deploy Cloud Composer on a shared VPC the identity running Terraform must have the following IAM role on the Shared VPC Host project.
+ - Compute Network Admin (roles/compute.networkAdmin)
+ - Compute Shared VPC Admin (roles/compute.xpnAdmin)
+
+## Encryption
+As is often the case in real-world configurations, this blueprint accepts as input an existing [`Cloud KMS keys`](https://cloud.google.com/kms/docs/cmek) via the `service_encryption_keys` variable.
+
+Example:
+```tfvars
+service_encryption_keys = {
+ `europe/west1` = `projects/PROJECT/locations/REGION/keyRings/KR_NAME/cryptoKeys/KEY_NAME`
+}
+```
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [prefix](variables.tf#L78) | Prefix used for resource names. | string
| ✓ | |
+| [project_id](variables.tf#L96) | Project id, references existing project if `project_create` is null. | string
| ✓ | |
+| [composer_config](variables.tf#L17) | Composer environment configuration. It accepts only following attributes: `environment_size`, `software_config` and `workloads_config`. See [attribute reference](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/composer_environment#argument-reference---cloud-composer-2) for details on settings variables. | object({…})
| | {…}
|
+| [iam_groups_map](variables.tf#L58) | Map of Role => groups to be added on the project. Example: { \"roles/composer.admin\" = [\"group:gcp-data-engineers@example.com\"]}. | map(list(string))
| | null
|
+| [network_config](variables.tf#L64) | Shared VPC network configurations to use. If null networks will be created in projects with preconfigured values. | object({…})
| | null
|
+| [project_create](variables.tf#L87) | Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…})
| | null
|
+| [region](variables.tf#L101) | Reagion where instances will be deployed. | string
| | "europe-west1"
|
+| [service_encryption_keys](variables.tf#L107) | Cloud KMS keys to use to encrypt resources. Provide a key for each reagion in use. | map(string)
| | null
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [composer_airflow_uri](outputs.tf#L17) | The URI of the Apache Airflow Web UI hosted within the Cloud Composer environment.. | |
+| [composer_dag_gcs](outputs.tf#L22) | The Cloud Storage prefix of the DAGs for the Cloud Composer environment. | |
+
+
diff --git a/examples/data-solutions/gcs-to-bq-with-least-privileges/backend.tf.sample b/blueprints/data-solutions/composer-2/backend.tf.sample
similarity index 100%
rename from examples/data-solutions/gcs-to-bq-with-least-privileges/backend.tf.sample
rename to blueprints/data-solutions/composer-2/backend.tf.sample
diff --git a/blueprints/data-solutions/composer-2/composer.tf b/blueprints/data-solutions/composer-2/composer.tf
new file mode 100644
index 0000000000..1217e0d416
--- /dev/null
+++ b/blueprints/data-solutions/composer-2/composer.tf
@@ -0,0 +1,111 @@
+/**
+ * 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 "comp-sa" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.project.project_id
+ prefix = var.prefix
+ name = "cmp"
+ display_name = "Composer service account"
+}
+
+resource "google_composer_environment" "env" {
+ name = "${var.prefix}-composer"
+ project = module.project.project_id
+ region = var.region
+ config {
+ dynamic "software_config" {
+ for_each = (
+ try(var.composer_config.software_config, null) != null
+ ? { 1 = 1 }
+ : {}
+ )
+ content {
+ 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 = try(var.composer_config.software_config.env_variables, null)
+ image_version = try(var.composer_config.software_config.image_version, null)
+ python_version = try(var.composer_config.software_config.python_version, null)
+ scheduler_count = try(var.composer_config.software_config.scheduler_count, 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 {
+ network = local.orch_vpc
+ subnetwork = local.orch_subnet
+ service_account = module.comp-sa.email
+ enable_ip_masq_agent = "true"
+ tags = ["composer-worker"]
+ ip_allocation_policy {
+ cluster_secondary_range_name = try(
+ var.network_config.composer_secondary_ranges.pods, "pods"
+ )
+ services_secondary_range_name = try(
+ var.network_config.composer_secondary_ranges.services, "services"
+ )
+ }
+ }
+ private_environment_config {
+ enable_private_endpoint = "true"
+ cloud_sql_ipv4_cidr_block = try(
+ var.network_config.composer_ip_ranges.cloudsql, "10.20.10.0/24"
+ )
+ master_ipv4_cidr_block = try(
+ var.network_config.composer_ip_ranges.gke_master, "10.20.11.0/28"
+ )
+ }
+ dynamic "encryption_config" {
+ for_each = (
+ try(var.service_encryption_keys[var.region], null) != null
+ ? { 1 = 1 }
+ : {}
+ )
+ content {
+ kms_key_name = try(var.service_encryption_keys[var.region], null)
+ }
+ }
+ }
+ depends_on = [
+ google_project_iam_member.shared_vpc,
+ module.project
+ ]
+}
diff --git a/blueprints/data-solutions/composer-2/diagram.png b/blueprints/data-solutions/composer-2/diagram.png
new file mode 100644
index 0000000000..b8ffc12efd
Binary files /dev/null and b/blueprints/data-solutions/composer-2/diagram.png differ
diff --git a/blueprints/data-solutions/composer-2/main.tf b/blueprints/data-solutions/composer-2/main.tf
new file mode 100644
index 0000000000..a9ee619c6c
--- /dev/null
+++ b/blueprints/data-solutions/composer-2/main.tf
@@ -0,0 +1,146 @@
+/**
+ * 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 {
+ iam = merge(
+ {
+ "roles/composer.worker" = [module.comp-sa.iam_email]
+ "roles/composer.ServiceAgentV2Ext" = ["serviceAccount:${module.project.service_accounts.robots.composer}"]
+ },
+ var.iam_groups_map
+ )
+ # Adding Roles on Service Identities Service account as per documentation: https://cloud.google.com/composer/docs/composer-2/configure-shared-vpc#edit_permissions_for_the_google_apis_service_account
+ _shared_vpc_bindings = {
+ "roles/compute.networkUser" = [
+ "prj-cloudservices", "prj-robot-gke"
+ ]
+ "roles/composer.sharedVpcAgent" = [
+ "prj-robot-cs"
+ ]
+ "roles/container.hostServiceAgentUser" = [
+ "prj-robot-gke"
+ ]
+ }
+ shared_vpc_role_members = {
+ prj-cloudservices = "serviceAccount:${module.project.service_accounts.cloud_services}"
+ prj-robot-gke = "serviceAccount:${module.project.service_accounts.robots.container-engine}"
+ prj-robot-cs = "serviceAccount:${module.project.service_accounts.robots.composer}"
+ }
+ # 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
+ }
+
+ shared_vpc_project = try(var.network_config.host_project, null)
+ use_shared_vpc = var.network_config != null
+
+ vpc_self_link = (
+ local.use_shared_vpc
+ ? var.network_config.network_self_link
+ : module.vpc.0.self_link
+ )
+
+ orch_subnet = (
+ local.use_shared_vpc
+ ? var.network_config.subnet_self_link
+ : values(module.vpc.0.subnet_self_links)[0]
+ )
+
+ orch_vpc = (
+ local.use_shared_vpc
+ ? var.network_config.network_self_link
+ : module.vpc.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.project_create == null ? null : var.prefix
+ iam = var.project_create != null ? local.iam : {}
+ iam_additive = var.project_create == null ? local.iam : {}
+ services = [
+ "artifactregistry.googleapis.com",
+ "cloudkms.googleapis.com",
+ "container.googleapis.com",
+ "containerregistry.googleapis.com",
+ "composer.googleapis.com",
+ "compute.googleapis.com",
+ "iap.googleapis.com",
+ "logging.googleapis.com",
+ "monitoring.googleapis.com",
+ "networkmanagement.googleapis.com",
+ "servicenetworking.googleapis.com",
+ "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
+ }
+
+ service_encryption_key_ids = {
+ composer = [try(lookup(var.service_encryption_keys, var.region, null), null)]
+ }
+
+ service_config = {
+ disable_on_destroy = false, disable_dependent_services = false
+ }
+}
+
+module "vpc" {
+ source = "../../../modules/net-vpc"
+ count = local.use_shared_vpc ? 0 : 1
+ project_id = module.project.project_id
+ name = "vpc"
+ subnets = [
+ {
+ ip_cidr_range = "10.0.0.0/20"
+ name = "subnet"
+ region = var.region
+ secondary_ip_ranges = {
+ pods = "10.10.8.0/22"
+ services = "10.10.12.0/24"
+ }
+ }
+ ]
+}
+
+# No explicit firewall rules set, created automatically by GKE autopilot
+
+module "nat" {
+ source = "../../../modules/net-cloudnat"
+ count = local.use_shared_vpc ? 0 : 1
+ project_id = module.project.project_id
+ region = var.region
+ name = "${var.prefix}-default"
+ router_network = module.vpc.0.name
+}
+
+resource "google_project_iam_member" "shared_vpc" {
+ for_each = local.use_shared_vpc ? local.shared_vpc_bindings_map : {}
+ project = var.network_config.host_project
+ role = each.value.role
+ member = lookup(local.shared_vpc_role_members, each.value.member)
+}
diff --git a/blueprints/data-solutions/composer-2/outputs.tf b/blueprints/data-solutions/composer-2/outputs.tf
new file mode 100644
index 0000000000..4e09a04985
--- /dev/null
+++ b/blueprints/data-solutions/composer-2/outputs.tf
@@ -0,0 +1,25 @@
+/**
+ * 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 "composer_airflow_uri" {
+ description = "The URI of the Apache Airflow Web UI hosted within the Cloud Composer environment.."
+ value = google_composer_environment.env.config[0].airflow_uri
+}
+
+output "composer_dag_gcs" {
+ description = "The Cloud Storage prefix of the DAGs for the Cloud Composer environment."
+ value = google_composer_environment.env.config[0].dag_gcs_prefix
+}
diff --git a/blueprints/data-solutions/composer-2/variables.tf b/blueprints/data-solutions/composer-2/variables.tf
new file mode 100644
index 0000000000..6ff0ff4615
--- /dev/null
+++ b/blueprints/data-solutions/composer-2/variables.tf
@@ -0,0 +1,111 @@
+/**
+ * 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 "composer_config" {
+ description = "Composer environment configuration. It accepts only following attributes: `environment_size`, `software_config` and `workloads_config`. See [attribute reference](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/composer_environment#argument-reference---cloud-composer-2) for details on settings variables."
+ type = object({
+ environment_size = string
+ software_config = any
+ 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 = {
+ environment_size = "ENVIRONMENT_SIZE_SMALL"
+ software_config = {
+ image_version = "composer-2-airflow-2"
+ }
+ workloads_config = null
+ }
+}
+
+variable "iam_groups_map" {
+ description = "Map of Role => groups to be added on the project. Example: { \"roles/composer.admin\" = [\"group:gcp-data-engineers@example.com\"]}."
+ type = map(list(string))
+ default = null
+}
+
+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
+ composer_secondary_ranges = object({
+ pods = string
+ services = string
+ })
+ })
+ default = null
+}
+
+variable "prefix" {
+ description = "Prefix used for resource names."
+ type = string
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty."
+ }
+}
+
+variable "project_create" {
+ description = "Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format."
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = null
+}
+
+variable "project_id" {
+ description = "Project id, references existing project if `project_create` is null."
+ type = string
+}
+
+variable "region" {
+ description = "Reagion where instances will be deployed."
+ type = string
+ default = "europe-west1"
+}
+
+variable "service_encryption_keys" {
+ description = "Cloud KMS keys to use to encrypt resources. Provide a key for each reagion in use."
+ type = map(string)
+ default = null
+}
diff --git a/blueprints/data-solutions/data-platform-foundations/01-dropoff.tf b/blueprints/data-solutions/data-platform-foundations/01-dropoff.tf
new file mode 100644
index 0000000000..177f940a86
--- /dev/null
+++ b/blueprints/data-solutions/data-platform-foundations/01-dropoff.tf
@@ -0,0 +1,135 @@
+# 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.
+
+# tfdoc:file:description drop off project and resources.
+
+locals {
+ drop_orch_service_accounts = [
+ module.load-sa-df-0.iam_email, module.orch-sa-cmp-0.iam_email
+ ]
+}
+
+module "drop-project" {
+ source = "../../../modules/project"
+ parent = var.folder_id
+ billing_account = var.billing_account_id
+ prefix = var.prefix
+ name = "drp${local.project_suffix}"
+ group_iam = {
+ (local.groups.data-engineers) = [
+ "roles/bigquery.dataEditor",
+ "roles/pubsub.editor",
+ "roles/storage.admin",
+ ]
+ }
+ iam = {
+ "roles/bigquery.dataEditor" = [module.drop-sa-bq-0.iam_email]
+ "roles/bigquery.user" = [module.load-sa-df-0.iam_email]
+ "roles/pubsub.publisher" = [module.drop-sa-ps-0.iam_email]
+ "roles/pubsub.subscriber" = concat(
+ local.drop_orch_service_accounts, [module.load-sa-df-0.iam_email]
+ )
+ "roles/storage.objectAdmin" = [module.load-sa-df-0.iam_email]
+ "roles/storage.objectCreator" = [module.drop-sa-cs-0.iam_email]
+ "roles/storage.objectViewer" = [module.orch-sa-cmp-0.iam_email]
+ "roles/storage.admin" = [module.load-sa-df-0.iam_email]
+ }
+ services = concat(var.project_services, [
+ "bigquery.googleapis.com",
+ "bigqueryreservation.googleapis.com",
+ "bigquerystorage.googleapis.com",
+ "cloudkms.googleapis.com",
+ "pubsub.googleapis.com",
+ "storage.googleapis.com",
+ "storage-component.googleapis.com",
+ ])
+ service_encryption_key_ids = {
+ bq = [try(local.service_encryption_keys.bq, null)]
+ pubsub = [try(local.service_encryption_keys.pubsub, null)]
+ storage = [try(local.service_encryption_keys.storage, null)]
+ }
+}
+
+# Cloud Storage
+
+module "drop-sa-cs-0" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.drop-project.project_id
+ prefix = var.prefix
+ name = "drp-cs-0"
+ display_name = "Data platform GCS drop off service account."
+ iam = {
+ "roles/iam.serviceAccountTokenCreator" = [
+ local.groups_iam.data-engineers
+ ]
+ }
+}
+
+module "drop-cs-0" {
+ source = "../../../modules/gcs"
+ project_id = module.drop-project.project_id
+ prefix = var.prefix
+ name = "drp-cs-0"
+ location = var.location
+ storage_class = "MULTI_REGIONAL"
+ encryption_key = try(local.service_encryption_keys.storage, null)
+ force_destroy = var.data_force_destroy
+ # retention_policy = {
+ # retention_period = 7776000 # 90 * 24 * 60 * 60
+ # is_locked = false
+ # }
+}
+
+# PubSub
+
+module "drop-sa-ps-0" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.drop-project.project_id
+ prefix = var.prefix
+ name = "drp-ps-0"
+ display_name = "Data platform PubSub drop off service account"
+ iam = {
+ "roles/iam.serviceAccountTokenCreator" = [
+ local.groups_iam.data-engineers
+ ]
+ }
+}
+
+module "drop-ps-0" {
+ source = "../../../modules/pubsub"
+ project_id = module.drop-project.project_id
+ name = "${var.prefix}-drp-ps-0"
+ kms_key = try(local.service_encryption_keys.pubsub, null)
+}
+
+# BigQuery
+
+module "drop-sa-bq-0" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.drop-project.project_id
+ prefix = var.prefix
+ name = "drp-bq-0"
+ display_name = "Data platform BigQuery drop off service account"
+ iam = {
+ "roles/iam.serviceAccountTokenCreator" = [local.groups_iam.data-engineers]
+ }
+}
+
+module "drop-bq-0" {
+ source = "../../../modules/bigquery-dataset"
+ project_id = module.drop-project.project_id
+ id = "${replace(var.prefix, "-", "_")}_drp_bq_0"
+ location = var.location
+ encryption_key = try(local.service_encryption_keys.bq, null)
+}
diff --git a/examples/data-solutions/data-platform-foundations/02-load.tf b/blueprints/data-solutions/data-platform-foundations/02-load.tf
similarity index 88%
rename from examples/data-solutions/data-platform-foundations/02-load.tf
rename to blueprints/data-solutions/data-platform-foundations/02-load.tf
index 8fdbe21532..74cb9f8b0c 100644
--- a/examples/data-solutions/data-platform-foundations/02-load.tf
+++ b/blueprints/data-solutions/data-platform-foundations/02-load.tf
@@ -74,9 +74,8 @@ module "load-project" {
storage = [try(local.service_encryption_keys.storage, null)]
}
shared_vpc_service_config = local.shared_vpc_project == null ? null : {
- attach = true
- host_project = local.shared_vpc_project
- service_identity_iam = {}
+ attach = true
+ host_project = local.shared_vpc_project
}
}
@@ -111,20 +110,21 @@ module "load-vpc" {
name = "${var.prefix}-default"
subnets = [
{
- ip_cidr_range = "10.10.0.0/24"
- name = "default"
- region = var.region
- secondary_ip_range = {}
+ ip_cidr_range = "10.10.0.0/24"
+ name = "default"
+ region = var.region
}
]
}
module "load-vpc-firewall" {
- source = "../../../modules/net-vpc-firewall"
- count = local.use_shared_vpc ? 0 : 1
- project_id = module.load-project.project_id
- network = module.load-vpc.0.name
- admin_ranges = ["10.10.0.0/24"]
+ source = "../../../modules/net-vpc-firewall"
+ count = local.use_shared_vpc ? 0 : 1
+ project_id = module.load-project.project_id
+ network = module.load-vpc.0.name
+ default_rules_config = {
+ admin_ranges = ["10.10.0.0/24"]
+ }
}
module "load-nat" {
diff --git a/blueprints/data-solutions/data-platform-foundations/03-composer.tf b/blueprints/data-solutions/data-platform-foundations/03-composer.tf
new file mode 100644
index 0000000000..2622ffa207
--- /dev/null
+++ b/blueprints/data-solutions/data-platform-foundations/03-composer.tf
@@ -0,0 +1,137 @@
+# 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.
+
+# tfdoc:file:description Orchestration Cloud Composer definition.
+
+module "orch-sa-cmp-0" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.orch-project.project_id
+ prefix = var.prefix
+ name = "orc-cmp-0"
+ display_name = "Data platform Composer service account"
+ iam = {
+ "roles/iam.serviceAccountTokenCreator" = [local.groups_iam.data-engineers]
+ "roles/iam.serviceAccountUser" = [module.orch-sa-cmp-0.iam_email]
+ }
+}
+
+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
+ config {
+ node_count = var.composer_config.node_count
+ 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
+ ip_allocation_policy {
+ use_ip_aliases = "true"
+ cluster_secondary_range_name = try(
+ var.network_config.composer_secondary_ranges.pods, "pods"
+ )
+ services_secondary_range_name = try(
+ var.network_config.composer_secondary_ranges.services, "services"
+ )
+ }
+ }
+ private_environment_config {
+ enable_private_endpoint = "true"
+ cloud_sql_ipv4_cidr_block = try(
+ var.network_config.composer_ip_ranges.cloudsql, "10.20.10.0/24"
+ )
+ 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)
+ ? { 1 = 1 }
+ : {}
+ )
+ content {
+ kms_key_name = try(local.service_encryption_keys.composer, 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,
+ ]
+}
diff --git a/examples/data-solutions/data-platform-foundations/03-orchestration.tf b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf
similarity index 89%
rename from examples/data-solutions/data-platform-foundations/03-orchestration.tf
rename to blueprints/data-solutions/data-platform-foundations/03-orchestration.tf
index 137d4e93b3..2974c12270 100644
--- a/examples/data-solutions/data-platform-foundations/03-orchestration.tf
+++ b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf
@@ -67,8 +67,10 @@ module "orch-project" {
"roles/storage.objectViewer" = [module.load-sa-df-0.iam_email]
}
oslogin = false
- policy_boolean = {
- "constraints/compute.requireOsLogin" = false
+ org_policies = {
+ "constraints/compute.requireOsLogin" = {
+ enforce = false
+ }
}
services = concat(var.project_services, [
"artifactregistry.googleapis.com",
@@ -82,6 +84,7 @@ module "orch-project" {
"container.googleapis.com",
"containerregistry.googleapis.com",
"dataflow.googleapis.com",
+ "orgpolicy.googleapis.com",
"pubsub.googleapis.com",
"servicenetworking.googleapis.com",
"storage.googleapis.com",
@@ -92,9 +95,8 @@ module "orch-project" {
storage = [try(local.service_encryption_keys.storage, null)]
}
shared_vpc_service_config = local.shared_vpc_project == null ? null : {
- attach = true
- host_project = local.shared_vpc_project
- service_identity_iam = {}
+ attach = true
+ host_project = local.shared_vpc_project
}
}
@@ -122,7 +124,7 @@ module "orch-vpc" {
ip_cidr_range = "10.10.0.0/24"
name = "default"
region = var.region
- secondary_ip_range = {
+ secondary_ip_ranges = {
pods = "10.10.8.0/22"
services = "10.10.12.0/24"
}
@@ -131,11 +133,13 @@ module "orch-vpc" {
}
module "orch-vpc-firewall" {
- source = "../../../modules/net-vpc-firewall"
- count = local.use_shared_vpc ? 0 : 1
- project_id = module.orch-project.project_id
- network = module.orch-vpc.0.name
- admin_ranges = ["10.10.0.0/24"]
+ source = "../../../modules/net-vpc-firewall"
+ count = local.use_shared_vpc ? 0 : 1
+ project_id = module.orch-project.project_id
+ network = module.orch-vpc.0.name
+ default_rules_config = {
+ admin_ranges = ["10.10.0.0/24"]
+ }
}
module "orch-nat" {
diff --git a/examples/data-solutions/data-platform-foundations/04-transformation.tf b/blueprints/data-solutions/data-platform-foundations/04-transformation.tf
similarity index 89%
rename from examples/data-solutions/data-platform-foundations/04-transformation.tf
rename to blueprints/data-solutions/data-platform-foundations/04-transformation.tf
index 6f2aacad37..3d3a818c57 100644
--- a/examples/data-solutions/data-platform-foundations/04-transformation.tf
+++ b/blueprints/data-solutions/data-platform-foundations/04-transformation.tf
@@ -72,9 +72,8 @@ module "transf-project" {
storage = [try(local.service_encryption_keys.storage, null)]
}
shared_vpc_service_config = local.shared_vpc_project == null ? null : {
- attach = true
- host_project = local.shared_vpc_project
- service_identity_iam = {}
+ attach = true
+ host_project = local.shared_vpc_project
}
}
@@ -135,20 +134,21 @@ module "transf-vpc" {
name = "${var.prefix}-default"
subnets = [
{
- ip_cidr_range = "10.10.0.0/24"
- name = "default"
- region = var.region
- secondary_ip_range = {}
+ ip_cidr_range = "10.10.0.0/24"
+ name = "default"
+ region = var.region
}
]
}
module "transf-vpc-firewall" {
- source = "../../../modules/net-vpc-firewall"
- count = local.use_shared_vpc ? 0 : 1
- project_id = module.transf-project.project_id
- network = module.transf-vpc.0.name
- admin_ranges = ["10.10.0.0/24"]
+ source = "../../../modules/net-vpc-firewall"
+ count = local.use_shared_vpc ? 0 : 1
+ project_id = module.transf-project.project_id
+ network = module.transf-vpc.0.name
+ default_rules_config = {
+ admin_ranges = ["10.10.0.0/24"]
+ }
}
module "transf-nat" {
diff --git a/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf b/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf
new file mode 100644
index 0000000000..879a0e0b16
--- /dev/null
+++ b/blueprints/data-solutions/data-platform-foundations/05-datawarehouse.tf
@@ -0,0 +1,236 @@
+# 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.
+
+# tfdoc:file:description Data Warehouse projects.
+
+locals {
+ dwh_group_iam = {
+ (local.groups.data-engineers) = [
+ "roles/bigquery.dataEditor",
+ "roles/storage.admin",
+ ],
+ (local.groups.data-analysts) = [
+ "roles/bigquery.dataViewer",
+ "roles/bigquery.jobUser",
+ "roles/bigquery.metadataViewer",
+ "roles/bigquery.user",
+ "roles/datacatalog.viewer",
+ "roles/datacatalog.tagTemplateViewer",
+ "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,
+ module.transf-sa-df-0.iam_email,
+ module.transf-sa-bq-0.iam_email,
+ ]
+ "roles/bigquery.jobUser" = [
+ module.load-sa-df-0.iam_email,
+ ]
+ "roles/datacatalog.categoryAdmin" = [
+ module.transf-sa-bq-0.iam_email
+ ]
+ "roles/storage.objectCreator" = [
+ module.load-sa-df-0.iam_email,
+ ]
+ }
+ dwh_iam = {
+ "roles/bigquery.dataOwner" = [
+ module.transf-sa-df-0.iam_email,
+ module.transf-sa-bq-0.iam_email,
+ ]
+ "roles/bigquery.jobUser" = [
+ module.transf-sa-bq-0.iam_email,
+ ]
+ "roles/datacatalog.categoryAdmin" = [
+ module.load-sa-df-0.iam_email
+ ]
+ "roles/storage.objectCreator" = [
+ module.transf-sa-df-0.iam_email,
+ ]
+ "roles/storage.objectViewer" = [
+ module.transf-sa-df-0.iam_email,
+ ]
+ }
+ dwh_services = concat(var.project_services, [
+ "bigquery.googleapis.com",
+ "bigqueryreservation.googleapis.com",
+ "bigquerystorage.googleapis.com",
+ "cloudkms.googleapis.com",
+ "compute.googleapis.com",
+ "dataflow.googleapis.com",
+ "pubsub.googleapis.com",
+ "servicenetworking.googleapis.com",
+ "storage.googleapis.com",
+ "storage-component.googleapis.com"
+ ])
+}
+
+# Project
+
+module "dwh-lnd-project" {
+ source = "../../../modules/project"
+ parent = var.folder_id
+ billing_account = var.billing_account_id
+ prefix = var.prefix
+ name = "dwh-lnd${local.project_suffix}"
+ group_iam = local.dwh_group_iam
+ iam = local.dwh_lnd_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)]
+ }
+}
+
+module "dwh-cur-project" {
+ source = "../../../modules/project"
+ parent = var.folder_id
+ billing_account = var.billing_account_id
+ prefix = var.prefix
+ name = "dwh-cur${local.project_suffix}"
+ group_iam = local.dwh_group_iam
+ iam = local.dwh_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)]
+ }
+}
+
+module "dwh-conf-project" {
+ source = "../../../modules/project"
+ parent = var.folder_id
+ billing_account = var.billing_account_id
+ prefix = var.prefix
+ name = "dwh-conf${local.project_suffix}"
+ group_iam = local.dwh_group_iam
+ iam = local.dwh_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)]
+ }
+}
+
+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" {
+ source = "../../../modules/bigquery-dataset"
+ project_id = module.dwh-lnd-project.project_id
+ id = "${replace(var.prefix, "-", "_")}_dwh_lnd_bq_0"
+ location = var.location
+ encryption_key = try(local.service_encryption_keys.bq, null)
+}
+
+module "dwh-cur-bq-0" {
+ source = "../../../modules/bigquery-dataset"
+ project_id = module.dwh-cur-project.project_id
+ id = "${replace(var.prefix, "-", "_")}_dwh_lnd_bq_0"
+ location = var.location
+ encryption_key = try(local.service_encryption_keys.bq, null)
+}
+
+module "dwh-conf-bq-0" {
+ source = "../../../modules/bigquery-dataset"
+ project_id = module.dwh-conf-project.project_id
+ id = "${replace(var.prefix, "-", "_")}_dwh_conf_bq_0"
+ location = var.location
+ 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" {
+ source = "../../../modules/gcs"
+ project_id = module.dwh-lnd-project.project_id
+ prefix = var.prefix
+ name = "dwh-lnd-cs-0"
+ location = var.location
+ storage_class = "MULTI_REGIONAL"
+ encryption_key = try(local.service_encryption_keys.storage, null)
+ force_destroy = var.data_force_destroy
+}
+
+module "dwh-cur-cs-0" {
+ source = "../../../modules/gcs"
+ project_id = module.dwh-cur-project.project_id
+ prefix = var.prefix
+ name = "dwh-cur-cs-0"
+ location = var.location
+ storage_class = "MULTI_REGIONAL"
+ encryption_key = try(local.service_encryption_keys.storage, null)
+ force_destroy = var.data_force_destroy
+}
+
+module "dwh-conf-cs-0" {
+ source = "../../../modules/gcs"
+ project_id = module.dwh-conf-project.project_id
+ prefix = var.prefix
+ name = "dwh-conf-cs-0"
+ location = var.location
+ storage_class = "MULTI_REGIONAL"
+ 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/06-common.tf b/blueprints/data-solutions/data-platform-foundations/06-common.tf
new file mode 100644
index 0000000000..80451500c2
--- /dev/null
+++ b/blueprints/data-solutions/data-platform-foundations/06-common.tf
@@ -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
+#
+# 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 common project.
+
+module "common-project" {
+ source = "../../../modules/project"
+ parent = var.folder_id
+ billing_account = var.billing_account_id
+ prefix = var.prefix
+ name = "cmn${local.project_suffix}"
+ group_iam = {
+ (local.groups.data-analysts) = [
+ "roles/datacatalog.viewer",
+ ]
+ (local.groups.data-engineers) = [
+ "roles/dlp.reader",
+ "roles/dlp.user",
+ "roles/dlp.estimatesAdmin",
+ ]
+ (local.groups.data-security) = [
+ "roles/dlp.admin",
+ "roles/datacatalog.admin"
+ ]
+ }
+ iam = {
+ "roles/dlp.user" = [
+ module.load-sa-df-0.iam_email,
+ module.transf-sa-df-0.iam_email
+ ]
+ "roles/datacatalog.viewer" = [
+ module.load-sa-df-0.iam_email,
+ module.transf-sa-df-0.iam_email,
+ module.transf-sa-bq-0.iam_email
+ ]
+ "roles/datacatalog.categoryFineGrainedReader" = [
+ module.transf-sa-df-0.iam_email,
+ module.transf-sa-bq-0.iam_email,
+ # Uncomment if you want to grant access to `data-analyst` to all columns tagged.
+ # local.groups_iam.data-analysts
+ ]
+ }
+ services = concat(var.project_services, [
+ "datacatalog.googleapis.com",
+ "dlp.googleapis.com",
+ ])
+}
+
+# Data Catalog Policy tag
+
+module "common-datacatalog" {
+ source = "../../../modules/data-catalog-policy-tag"
+ project_id = module.common-project.project_id
+ name = "${var.prefix}-datacatalog-policy-tags"
+ location = var.location
+ tags = var.data_catalog_tags
+}
+
+# To create KMS keys in the common projet: uncomment this section and assigne key links accondingly in local.service_encryption_keys variable
+
+# module "cmn-kms-0" {
+# source = "../../../modules/kms"
+# project_id = module.common-project.project_id
+# keyring = {
+# name = "${var.prefix}-kr-global",
+# location = "global"
+# }
+# keys = {
+# pubsub = null
+# }
+# }
+
+# module "cmn-kms-1" {
+# source = "../../../modules/kms"
+# project_id = module.common-project.project_id
+# keyring = {
+# name = "${var.prefix}-kr-mregional",
+# location = var.location
+# }
+# keys = {
+# bq = null
+# storage = null
+# }
+# }
+
+# module "cmn-kms-2" {
+# source = "../../../modules/kms"
+# project_id = module.cmn-prj.project_id
+# keyring = {
+# name = "${var.prefix}-kr-regional",
+# location = var.region
+# }
+# keys = {
+# composer = null
+# dataflow = null
+# }
+# }
diff --git a/examples/data-solutions/data-platform-foundations/07-exposure.tf b/blueprints/data-solutions/data-platform-foundations/07-exposure.tf
similarity index 100%
rename from examples/data-solutions/data-platform-foundations/07-exposure.tf
rename to blueprints/data-solutions/data-platform-foundations/07-exposure.tf
diff --git a/blueprints/data-solutions/data-platform-foundations/IAM.md b/blueprints/data-solutions/data-platform-foundations/IAM.md
new file mode 100644
index 0000000000..54d35939b4
--- /dev/null
+++ b/blueprints/data-solutions/data-platform-foundations/IAM.md
@@ -0,0 +1,98 @@
+# IAM bindings reference
+
+Legend: +
additive, •
conditional.
+
+## Project cmn
+
+| members | roles |
+|---|---|
+|gcp-data-analysts+
|
+|load-df-0+
|
+|load-df-0+
|
+|load-df-0+
|
+
+## Project lod
+
+| members | roles |
+|---|---|
+|gcp-data-engineers+
|
+|load-df-0+
|
+|load-df-0+
|
+|orc-cmp-0string
| ✓ | |
+| [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
|
+
+## 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. | |
+
+
+## TODOs
+
+Features to add in future releases:
+
+- Add example on how to use Cloud Data Loss Prevention
+- Add solution to handle Tables, Views, and Authorized Views lifecycle
+- Add solution to handle Metadata lifecycle
diff --git a/blueprints/data-solutions/data-platform-foundations/backend.tf.sample b/blueprints/data-solutions/data-platform-foundations/backend.tf.sample
new file mode 100644
index 0000000000..49a0883db6
--- /dev/null
+++ b/blueprints/data-solutions/data-platform-foundations/backend.tf.sample
@@ -0,0 +1,30 @@
+# 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.
+
+# The `impersonate_service_account` option require the identity launching terraform
+# role `roles/iam.serviceAccountTokenCreator` on the Service Account specified.
+
+terraform {
+ backend "gcs" {
+ bucket = "BUCKET_NAME"
+ prefix = "PREFIX"
+ impersonate_service_account = "SERVICE_ACCOUNT@PROJECT_ID.iam.gserviceaccount.com"
+ }
+}
+provider "google" {
+ impersonate_service_account = "SERVICE_ACCOUNT@PROJECT_ID.iam.gserviceaccount.com"
+}
+provider "google-beta" {
+ impersonate_service_account = "SERVICE_ACCOUNT@PROJECT_ID.iam.gserviceaccount.com"
+}
\ No newline at end of file
diff --git a/blueprints/data-solutions/data-platform-foundations/demo/README.md b/blueprints/data-solutions/data-platform-foundations/demo/README.md
new file mode 100644
index 0000000000..97add086ad
--- /dev/null
+++ b/blueprints/data-solutions/data-platform-foundations/demo/README.md
@@ -0,0 +1,32 @@
+# Data ingestion Demo
+
+In this folder, you can find an example to ingest data on the `data platform` instantiated [here](../).
+
+The example is not intended to be a production-ready code.
+
+## Demo use case
+The demo imports purchase data generated by a store.
+
+## Input files
+Data are uploaded to the `drop off` GCS bucket. File structure:
+ - `customers.csv`: Comma separate value with customer information in the following format: Customer ID, Name, Surname, Registration Timestamp
+ - `purchases.csv`: Comma separate value with customer information in the following format: Item ID, Customer ID, Item, Item price, Purchase Timestamp
+
+## Data processing pipelines
+Different data pipelines are provided to highlight different features and patterns. For the purpose of the example, a single pipeline handle all data lifecycles. When adapting them to your real use case, you may want to evaluate the option to handle each functional step on a separate pipeline or a dedicated tool. For example, you may want to use `Dataform` to handle data schemas lifecycle.
+
+Below you can find a description of each example:
+ - Simple import data: [`datapipeline.py`](./datapipeline.py) is a simple pipeline to import provided data from the `drop off` Google Cloud Storage bucket to the Data Hub Confidential layer joining `customers` and `purchases` tables into `customerpurchase` table.
+ - Import data with Policy Tags: [`datapipeline_dc_tags.py`](./datapipeline.py) imports provided data from `drop off` bucket to the Data Hub Confidential layer protecting sensitive data using Data Catalog policy Tags.
+ - Delete tables: [`delete_table.py`](./delete_table.py) deletes BigQuery tables created by import pipelines.
+
+## 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.
+
+You can find pre-computed commands in the `demo_commands` output variable of the deployed terraform [data pipeline](../).
diff --git a/examples/data-solutions/data-platform-foundations/demo/data/customer_purchase.json b/blueprints/data-solutions/data-platform-foundations/demo/data/customer_purchase.json
similarity index 100%
rename from examples/data-solutions/data-platform-foundations/demo/data/customer_purchase.json
rename to blueprints/data-solutions/data-platform-foundations/demo/data/customer_purchase.json
diff --git a/examples/data-solutions/data-platform-foundations/demo/data/customers.csv b/blueprints/data-solutions/data-platform-foundations/demo/data/customers.csv
similarity index 100%
rename from examples/data-solutions/data-platform-foundations/demo/data/customers.csv
rename to blueprints/data-solutions/data-platform-foundations/demo/data/customers.csv
diff --git a/examples/data-solutions/data-platform-foundations/demo/data/customers.json b/blueprints/data-solutions/data-platform-foundations/demo/data/customers.json
similarity index 100%
rename from examples/data-solutions/data-platform-foundations/demo/data/customers.json
rename to blueprints/data-solutions/data-platform-foundations/demo/data/customers.json
diff --git a/examples/data-solutions/data-platform-foundations/demo/data/customers_schema.json b/blueprints/data-solutions/data-platform-foundations/demo/data/customers_schema.json
similarity index 100%
rename from examples/data-solutions/data-platform-foundations/demo/data/customers_schema.json
rename to blueprints/data-solutions/data-platform-foundations/demo/data/customers_schema.json
diff --git a/examples/data-solutions/data-platform-foundations/demo/data/customers_udf.js b/blueprints/data-solutions/data-platform-foundations/demo/data/customers_udf.js
similarity index 100%
rename from examples/data-solutions/data-platform-foundations/demo/data/customers_udf.js
rename to blueprints/data-solutions/data-platform-foundations/demo/data/customers_udf.js
diff --git a/examples/data-solutions/data-platform-foundations/demo/data/purchases.csv b/blueprints/data-solutions/data-platform-foundations/demo/data/purchases.csv
similarity index 100%
rename from examples/data-solutions/data-platform-foundations/demo/data/purchases.csv
rename to blueprints/data-solutions/data-platform-foundations/demo/data/purchases.csv
diff --git a/examples/data-solutions/data-platform-foundations/demo/data/purchases.json b/blueprints/data-solutions/data-platform-foundations/demo/data/purchases.json
similarity index 100%
rename from examples/data-solutions/data-platform-foundations/demo/data/purchases.json
rename to blueprints/data-solutions/data-platform-foundations/demo/data/purchases.json
diff --git a/examples/data-solutions/data-platform-foundations/demo/data/purchases_schema.json b/blueprints/data-solutions/data-platform-foundations/demo/data/purchases_schema.json
similarity index 100%
rename from examples/data-solutions/data-platform-foundations/demo/data/purchases_schema.json
rename to blueprints/data-solutions/data-platform-foundations/demo/data/purchases_schema.json
diff --git a/examples/data-solutions/data-platform-foundations/demo/data/purchases_udf.js b/blueprints/data-solutions/data-platform-foundations/demo/data/purchases_udf.js
similarity index 100%
rename from examples/data-solutions/data-platform-foundations/demo/data/purchases_udf.js
rename to blueprints/data-solutions/data-platform-foundations/demo/data/purchases_udf.js
diff --git a/blueprints/data-solutions/data-platform-foundations/demo/datapipeline.py b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline.py
new file mode 100644
index 0000000000..a682d346a0
--- /dev/null
+++ b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline.py
@@ -0,0 +1,211 @@
+# 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 csv
+import datetime
+import io
+import json
+import logging
+import os
+
+from airflow import models
+from airflow.providers.google.cloud.operators.dataflow import DataflowTemplatedJobStartOperator
+from airflow.operators import dummy
+from airflow.providers.google.cloud.operators.bigquery import BigQueryInsertJobOperator, BigQueryUpsertTableOperator, BigQueryUpdateTableSchemaOperator
+from airflow.utils.task_group import TaskGroup
+
+# --------------------------------------------------------------------------------
+# Set variables - Needed for the DEMO
+# --------------------------------------------------------------------------------
+BQ_LOCATION = os.environ.get("BQ_LOCATION")
+DATA_CAT_TAGS = json.loads(os.environ.get("DATA_CAT_TAGS"))
+DWH_LAND_PRJ = os.environ.get("DWH_LAND_PRJ")
+DWH_LAND_BQ_DATASET = os.environ.get("DWH_LAND_BQ_DATASET")
+DWH_LAND_GCS = os.environ.get("DWH_LAND_GCS")
+DWH_CURATED_PRJ = os.environ.get("DWH_CURATED_PRJ")
+DWH_CURATED_BQ_DATASET = os.environ.get("DWH_CURATED_BQ_DATASET")
+DWH_CURATED_GCS = os.environ.get("DWH_CURATED_GCS")
+DWH_CONFIDENTIAL_PRJ = os.environ.get("DWH_CONFIDENTIAL_PRJ")
+DWH_CONFIDENTIAL_BQ_DATASET = os.environ.get("DWH_CONFIDENTIAL_BQ_DATASET")
+DWH_CONFIDENTIAL_GCS = os.environ.get("DWH_CONFIDENTIAL_GCS")
+DWH_PLG_PRJ = os.environ.get("DWH_PLG_PRJ")
+DWH_PLG_BQ_DATASET = os.environ.get("DWH_PLG_BQ_DATASET")
+DWH_PLG_GCS = os.environ.get("DWH_PLG_GCS")
+GCP_REGION = os.environ.get("GCP_REGION")
+DRP_PRJ = os.environ.get("DRP_PRJ")
+DRP_BQ = os.environ.get("DRP_BQ")
+DRP_GCS = os.environ.get("DRP_GCS")
+DRP_PS = os.environ.get("DRP_PS")
+LOD_PRJ = os.environ.get("LOD_PRJ")
+LOD_GCS_STAGING = os.environ.get("LOD_GCS_STAGING")
+LOD_NET_VPC = os.environ.get("LOD_NET_VPC")
+LOD_NET_SUBNET = os.environ.get("LOD_NET_SUBNET")
+LOD_SA_DF = os.environ.get("LOD_SA_DF")
+ORC_PRJ = os.environ.get("ORC_PRJ")
+ORC_GCS = os.environ.get("ORC_GCS")
+TRF_PRJ = os.environ.get("TRF_PRJ")
+TRF_GCS_STAGING = os.environ.get("TRF_GCS_STAGING")
+TRF_NET_VPC = os.environ.get("TRF_NET_VPC")
+TRF_NET_SUBNET = os.environ.get("TRF_NET_SUBNET")
+TRF_SA_DF = os.environ.get("TRF_SA_DF")
+TRF_SA_BQ = os.environ.get("TRF_SA_BQ")
+DF_KMS_KEY = os.environ.get("DF_KMS_KEY", "")
+DF_REGION = os.environ.get("GCP_REGION")
+DF_ZONE = os.environ.get("GCP_REGION") + "-b"
+
+# --------------------------------------------------------------------------------
+# 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_default_options': {
+ 'location': DF_REGION,
+ 'zone': DF_ZONE,
+ 'stagingLocation': LOD_GCS_STAGING,
+ 'tempLocation': LOD_GCS_STAGING + "/tmp",
+ 'serviceAccountEmail': LOD_SA_DF,
+ 'subnetwork': LOD_NET_SUBNET,
+ 'ipConfiguration': "WORKER_IP_PRIVATE",
+ 'kmsKeyName' : DF_KMS_KEY
+ },
+}
+
+# --------------------------------------------------------------------------------
+# Main DAG
+# --------------------------------------------------------------------------------
+
+with models.DAG(
+ 'data_pipeline_dag',
+ 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 porpuse.
+ # Consider a dedicated pipeline or tool for a real life scenario.
+ customers_import = DataflowTemplatedJobStartOperator(
+ task_id="dataflow_customers_import",
+ template="gs://dataflow-templates/latest/GCS_Text_to_BigQuery",
+ project_id=LOD_PRJ,
+ location=DF_REGION,
+ parameters={
+ "javascriptTextTransformFunctionName": "transform",
+ "JSONPath": ORC_GCS + "/customers_schema.json",
+ "javascriptTextTransformGcsPath": ORC_GCS + "/customers_udf.js",
+ "inputFilePattern": DRP_GCS + "/customers.csv",
+ "outputTable": DWH_LAND_PRJ + ":" + DWH_LAND_BQ_DATASET + ".customers",
+ "bigQueryLoadingTemporaryDirectory": LOD_GCS_STAGING + "/tmp/bq/",
+ },
+ )
+
+ purchases_import = DataflowTemplatedJobStartOperator(
+ task_id="dataflow_purchases_import",
+ template="gs://dataflow-templates/latest/GCS_Text_to_BigQuery",
+ project_id=LOD_PRJ,
+ location=DF_REGION,
+ parameters={
+ "javascriptTextTransformFunctionName": "transform",
+ "JSONPath": ORC_GCS + "/purchases_schema.json",
+ "javascriptTextTransformGcsPath": ORC_GCS + "/purchases_udf.js",
+ "inputFilePattern": DRP_GCS + "/purchases.csv",
+ "outputTable": DWH_LAND_PRJ + ":" + DWH_LAND_BQ_DATASET + ".purchases",
+ "bigQueryLoadingTemporaryDirectory": LOD_GCS_STAGING + "/tmp/bq/",
+ },
+ )
+
+ 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
\ No newline at end of file
diff --git a/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags.py b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags.py
new file mode 100644
index 0000000000..4b15eaaba5
--- /dev/null
+++ b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags.py
@@ -0,0 +1,322 @@
+# 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 csv
+import datetime
+import io
+import json
+import logging
+import os
+
+from airflow import models
+from airflow.providers.google.cloud.operators.dataflow import DataflowTemplatedJobStartOperator
+from airflow.operators import dummy
+from airflow.providers.google.cloud.operators.bigquery import BigQueryInsertJobOperator, BigQueryUpsertTableOperator, BigQueryUpdateTableSchemaOperator
+from airflow.utils.task_group import TaskGroup
+
+# --------------------------------------------------------------------------------
+# Set variables - Needed for the DEMO
+# --------------------------------------------------------------------------------
+BQ_LOCATION = os.environ.get("BQ_LOCATION")
+DATA_CAT_TAGS = json.loads(os.environ.get("DATA_CAT_TAGS"))
+DWH_LAND_PRJ = os.environ.get("DWH_LAND_PRJ")
+DWH_LAND_BQ_DATASET = os.environ.get("DWH_LAND_BQ_DATASET")
+DWH_LAND_GCS = os.environ.get("DWH_LAND_GCS")
+DWH_CURATED_PRJ = os.environ.get("DWH_CURATED_PRJ")
+DWH_CURATED_BQ_DATASET = os.environ.get("DWH_CURATED_BQ_DATASET")
+DWH_CURATED_GCS = os.environ.get("DWH_CURATED_GCS")
+DWH_CONFIDENTIAL_PRJ = os.environ.get("DWH_CONFIDENTIAL_PRJ")
+DWH_CONFIDENTIAL_BQ_DATASET = os.environ.get("DWH_CONFIDENTIAL_BQ_DATASET")
+DWH_CONFIDENTIAL_GCS = os.environ.get("DWH_CONFIDENTIAL_GCS")
+DWH_PLG_PRJ = os.environ.get("DWH_PLG_PRJ")
+DWH_PLG_BQ_DATASET = os.environ.get("DWH_PLG_BQ_DATASET")
+DWH_PLG_GCS = os.environ.get("DWH_PLG_GCS")
+GCP_REGION = os.environ.get("GCP_REGION")
+DRP_PRJ = os.environ.get("DRP_PRJ")
+DRP_BQ = os.environ.get("DRP_BQ")
+DRP_GCS = os.environ.get("DRP_GCS")
+DRP_PS = os.environ.get("DRP_PS")
+LOD_PRJ = os.environ.get("LOD_PRJ")
+LOD_GCS_STAGING = os.environ.get("LOD_GCS_STAGING")
+LOD_NET_VPC = os.environ.get("LOD_NET_VPC")
+LOD_NET_SUBNET = os.environ.get("LOD_NET_SUBNET")
+LOD_SA_DF = os.environ.get("LOD_SA_DF")
+ORC_PRJ = os.environ.get("ORC_PRJ")
+ORC_GCS = os.environ.get("ORC_GCS")
+TRF_PRJ = os.environ.get("TRF_PRJ")
+TRF_GCS_STAGING = os.environ.get("TRF_GCS_STAGING")
+TRF_NET_VPC = os.environ.get("TRF_NET_VPC")
+TRF_NET_SUBNET = os.environ.get("TRF_NET_SUBNET")
+TRF_SA_DF = os.environ.get("TRF_SA_DF")
+TRF_SA_BQ = os.environ.get("TRF_SA_BQ")
+DF_KMS_KEY = os.environ.get("DF_KMS_KEY", "")
+DF_REGION = os.environ.get("GCP_REGION")
+DF_ZONE = os.environ.get("GCP_REGION") + "-b"
+
+# --------------------------------------------------------------------------------
+# 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_default_options': {
+ 'location': DF_REGION,
+ 'zone': DF_ZONE,
+ 'stagingLocation': LOD_GCS_STAGING,
+ 'tempLocation': LOD_GCS_STAGING + "/tmp",
+ 'serviceAccountEmail': LOD_SA_DF,
+ 'subnetwork': LOD_NET_SUBNET,
+ 'ipConfiguration': "WORKER_IP_PRIVATE",
+ 'kmsKeyName' : DF_KMS_KEY
+ },
+}
+
+# --------------------------------------------------------------------------------
+# Main DAG
+# --------------------------------------------------------------------------------
+
+with models.DAG(
+ 'data_pipeline_dc_tags_dag',
+ 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_customers = 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 = DataflowTemplatedJobStartOperator(
+ task_id="dataflow_customers_import",
+ template="gs://dataflow-templates/latest/GCS_Text_to_BigQuery",
+ project_id=LOD_PRJ,
+ location=DF_REGION,
+ parameters={
+ "javascriptTextTransformFunctionName": "transform",
+ "JSONPath": ORC_GCS + "/customers_schema.json",
+ "javascriptTextTransformGcsPath": ORC_GCS + "/customers_udf.js",
+ "inputFilePattern": DRP_GCS + "/customers.csv",
+ "outputTable": DWH_LAND_PRJ + ":" + DWH_LAND_BQ_DATASET + ".customers",
+ "bigQueryLoadingTemporaryDirectory": LOD_GCS_STAGING + "/tmp/bq/",
+ },
+ )
+
+ purchases_import = DataflowTemplatedJobStartOperator(
+ task_id="dataflow_purchases_import",
+ template="gs://dataflow-templates/latest/GCS_Text_to_BigQuery",
+ project_id=LOD_PRJ,
+ location=DF_REGION,
+ parameters={
+ "javascriptTextTransformFunctionName": "transform",
+ "JSONPath": ORC_GCS + "/purchases_schema.json",
+ "javascriptTextTransformGcsPath": ORC_GCS + "/purchases_udf.js",
+ "inputFilePattern": DRP_GCS + "/purchases.csv",
+ "outputTable": DWH_LAND_PRJ + ":" + DWH_LAND_BQ_DATASET + ".purchases",
+ "bigQueryLoadingTemporaryDirectory": LOD_GCS_STAGING + "/tmp/bq/",
+ },
+ )
+
+ 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/delete_table.py b/blueprints/data-solutions/data-platform-foundations/demo/delete_table.py
new file mode 100644
index 0000000000..dc0c954b14
--- /dev/null
+++ b/blueprints/data-solutions/data-platform-foundations/demo/delete_table.py
@@ -0,0 +1,146 @@
+# 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 csv
+import datetime
+import io
+import json
+import logging
+import os
+
+from airflow import models
+from airflow.providers.google.cloud.operators.dataflow import DataflowTemplatedJobStartOperator
+from airflow.operators import dummy
+from airflow.providers.google.cloud.operators.bigquery import BigQueryDeleteTableOperator
+from airflow.utils.task_group import TaskGroup
+
+# --------------------------------------------------------------------------------
+# Set variables - Needed for the DEMO
+# --------------------------------------------------------------------------------
+BQ_LOCATION = os.environ.get("BQ_LOCATION")
+DATA_CAT_TAGS = json.loads(os.environ.get("DATA_CAT_TAGS"))
+DWH_LAND_PRJ = os.environ.get("DWH_LAND_PRJ")
+DWH_LAND_BQ_DATASET = os.environ.get("DWH_LAND_BQ_DATASET")
+DWH_LAND_GCS = os.environ.get("DWH_LAND_GCS")
+DWH_CURATED_PRJ = os.environ.get("DWH_CURATED_PRJ")
+DWH_CURATED_BQ_DATASET = os.environ.get("DWH_CURATED_BQ_DATASET")
+DWH_CURATED_GCS = os.environ.get("DWH_CURATED_GCS")
+DWH_CONFIDENTIAL_PRJ = os.environ.get("DWH_CONFIDENTIAL_PRJ")
+DWH_CONFIDENTIAL_BQ_DATASET = os.environ.get("DWH_CONFIDENTIAL_BQ_DATASET")
+DWH_CONFIDENTIAL_GCS = os.environ.get("DWH_CONFIDENTIAL_GCS")
+DWH_PLG_PRJ = os.environ.get("DWH_PLG_PRJ")
+DWH_PLG_BQ_DATASET = os.environ.get("DWH_PLG_BQ_DATASET")
+DWH_PLG_GCS = os.environ.get("DWH_PLG_GCS")
+GCP_REGION = os.environ.get("GCP_REGION")
+DRP_PRJ = os.environ.get("DRP_PRJ")
+DRP_BQ = os.environ.get("DRP_BQ")
+DRP_GCS = os.environ.get("DRP_GCS")
+DRP_PS = os.environ.get("DRP_PS")
+LOD_PRJ = os.environ.get("LOD_PRJ")
+LOD_GCS_STAGING = os.environ.get("LOD_GCS_STAGING")
+LOD_NET_VPC = os.environ.get("LOD_NET_VPC")
+LOD_NET_SUBNET = os.environ.get("LOD_NET_SUBNET")
+LOD_SA_DF = os.environ.get("LOD_SA_DF")
+ORC_PRJ = os.environ.get("ORC_PRJ")
+ORC_GCS = os.environ.get("ORC_GCS")
+TRF_PRJ = os.environ.get("TRF_PRJ")
+TRF_GCS_STAGING = os.environ.get("TRF_GCS_STAGING")
+TRF_NET_VPC = os.environ.get("TRF_NET_VPC")
+TRF_NET_SUBNET = os.environ.get("TRF_NET_SUBNET")
+TRF_SA_DF = os.environ.get("TRF_SA_DF")
+TRF_SA_BQ = os.environ.get("TRF_SA_BQ")
+DF_KMS_KEY = os.environ.get("DF_KMS_KEY", "")
+DF_REGION = os.environ.get("GCP_REGION")
+DF_ZONE = os.environ.get("GCP_REGION") + "-b"
+
+# --------------------------------------------------------------------------------
+# 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_default_options': {
+ 'location': DF_REGION,
+ 'zone': DF_ZONE,
+ 'stagingLocation': LOD_GCS_STAGING,
+ 'tempLocation': LOD_GCS_STAGING + "/tmp",
+ 'serviceAccountEmail': LOD_SA_DF,
+ 'subnetwork': LOD_NET_SUBNET,
+ 'ipConfiguration': "WORKER_IP_PRIVATE",
+ 'kmsKeyName' : DF_KMS_KEY
+ },
+}
+
+# --------------------------------------------------------------------------------
+# Main DAG
+# --------------------------------------------------------------------------------
+
+with models.DAG(
+ 'delete_tables_dag',
+ 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 deleted here for demo porpuse.
+ # Consider a dedicated pipeline or tool for a real life scenario.
+ with TaskGroup('delete_table') as delte_table:
+ delete_table_customers = BigQueryDeleteTableOperator(
+ task_id="delete_table_customers",
+ deletion_dataset_table=DWH_LAND_PRJ+"."+DWH_LAND_BQ_DATASET+".customers",
+ impersonation_chain=[TRF_SA_DF]
+ )
+
+ delete_table_purchases = BigQueryDeleteTableOperator(
+ task_id="delete_table_purchases",
+ deletion_dataset_table=DWH_LAND_PRJ+"."+DWH_LAND_BQ_DATASET+".purchases",
+ impersonation_chain=[TRF_SA_DF]
+ )
+
+ delete_table_customer_purchase_curated = BigQueryDeleteTableOperator(
+ task_id="delete_table_customer_purchase_curated",
+ deletion_dataset_table=DWH_CURATED_PRJ+"."+DWH_CURATED_BQ_DATASET+".customer_purchase",
+ impersonation_chain=[TRF_SA_DF]
+ )
+
+ delete_table_customer_purchase_confidential = BigQueryDeleteTableOperator(
+ task_id="delete_table_customer_purchase_confidential",
+ deletion_dataset_table=DWH_CONFIDENTIAL_PRJ+"."+DWH_CONFIDENTIAL_BQ_DATASET+".customer_purchase",
+ impersonation_chain=[TRF_SA_DF]
+ )
+
+ start >> delte_table >> end
diff --git a/examples/data-solutions/data-platform-foundations/images/dlp_diagram.png b/blueprints/data-solutions/data-platform-foundations/images/dlp_diagram.png
similarity index 100%
rename from examples/data-solutions/data-platform-foundations/images/dlp_diagram.png
rename to blueprints/data-solutions/data-platform-foundations/images/dlp_diagram.png
diff --git a/examples/data-solutions/data-platform-foundations/images/kms_diagram.png b/blueprints/data-solutions/data-platform-foundations/images/kms_diagram.png
similarity index 100%
rename from examples/data-solutions/data-platform-foundations/images/kms_diagram.png
rename to blueprints/data-solutions/data-platform-foundations/images/kms_diagram.png
diff --git a/blueprints/data-solutions/data-platform-foundations/images/overview_diagram.png b/blueprints/data-solutions/data-platform-foundations/images/overview_diagram.png
new file mode 100644
index 0000000000..642c81c2fc
Binary files /dev/null and b/blueprints/data-solutions/data-platform-foundations/images/overview_diagram.png differ
diff --git a/examples/data-solutions/data-platform-foundations/main.tf b/blueprints/data-solutions/data-platform-foundations/main.tf
similarity index 100%
rename from examples/data-solutions/data-platform-foundations/main.tf
rename to blueprints/data-solutions/data-platform-foundations/main.tf
diff --git a/blueprints/data-solutions/data-platform-foundations/outputs.tf b/blueprints/data-solutions/data-platform-foundations/outputs.tf
new file mode 100644
index 0000000000..b941776cb0
--- /dev/null
+++ b/blueprints/data-solutions/data-platform-foundations/outputs.tf
@@ -0,0 +1,104 @@
+# 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.
+
+# tfdoc:file:description Output variables.
+
+output "bigquery-datasets" {
+ description = "BigQuery datasets."
+ value = {
+ drop-bq-0 = module.drop-bq-0.dataset_id,
+ 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."
+ 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
| ✓ | |
+| [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({…})
| | {…}
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [bucket](outputs.tf#L15) | GCS Bucket URL. | |
+| [dataset](outputs.tf#L20) | GCS Bucket URL. | |
+| [notebook](outputs.tf#L25) | Vertex AI notebook details. | |
+| [project](outputs.tf#L33) | Project id. | |
+| [vpc](outputs.tf#L38) | VPC Network. | |
+
+
diff --git a/blueprints/data-solutions/data-playground/diagram.png b/blueprints/data-solutions/data-playground/diagram.png
new file mode 100644
index 0000000000..b2d2d8ebaf
Binary files /dev/null and b/blueprints/data-solutions/data-playground/diagram.png differ
diff --git a/blueprints/data-solutions/data-playground/main.tf b/blueprints/data-solutions/data-playground/main.tf
new file mode 100644
index 0000000000..ff079d5ebc
--- /dev/null
+++ b/blueprints/data-solutions/data-playground/main.tf
@@ -0,0 +1,170 @@
+# 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.
+
+###############################################################################
+# Project #
+###############################################################################
+locals {
+ service_encryption_keys = var.service_encryption_keys
+}
+
+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.project_create == null ? null : var.prefix
+ services = [
+ "bigquery.googleapis.com",
+ "bigquerystorage.googleapis.com",
+ "bigqueryreservation.googleapis.com",
+ "composer.googleapis.com",
+ "compute.googleapis.com",
+ "dialogflow.googleapis.com",
+ "dataflow.googleapis.com",
+ "ml.googleapis.com",
+ "notebooks.googleapis.com",
+ "orgpolicy.googleapis.com",
+ "servicenetworking.googleapis.com",
+ "stackdriver.googleapis.com",
+ "storage.googleapis.com",
+ "storage-component.googleapis.com"
+ ]
+ org_policies = {
+ # "constraints/compute.requireOsLogin" = {
+ # enforce = false
+ # }
+ # Example of applying a project wide policy, mainly useful for Composer
+ }
+ 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)]
+ }
+}
+
+###############################################################################
+# Networking #
+###############################################################################
+
+module "vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = "${var.prefix}-vpc"
+ subnets = [
+ {
+ ip_cidr_range = var.vpc_config.ip_cidr_range
+ name = "${var.prefix}-subnet"
+ region = var.region
+ }
+ ]
+}
+
+module "vpc-firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.project.project_id
+ network = module.vpc.name
+ default_rules_config = {
+ admin_ranges = [var.vpc_config.ip_cidr_range]
+ }
+ ingress_rules = {
+ #TODO Remove and rely on 'ssh' tag once terraform-provider-google/issues/9273 is fixed
+ ("${var.prefix}-iap") = {
+ description = "Enable SSH from IAP on Notebooks."
+ source_ranges = ["35.235.240.0/20"]
+ targets = ["notebook-instance"]
+ rules = [{ protocol = "tcp", ports = [22] }]
+ }
+ }
+}
+
+module "cloudnat" {
+ source = "../../../modules/net-cloudnat"
+ project_id = module.project.project_id
+ name = "${var.prefix}-default"
+ region = var.region
+ router_network = module.vpc.name
+}
+
+###############################################################################
+# Storage #
+###############################################################################
+
+module "bucket" {
+ source = "../../../modules/gcs"
+ project_id = module.project.project_id
+ prefix = var.prefix
+ location = var.location
+ name = "data"
+ encryption_key = try(local.service_encryption_keys.storage, null) # Example assignment of an encryption key
+}
+
+module "dataset" {
+ source = "../../../modules/bigquery-dataset"
+ project_id = module.project.project_id
+ id = "${replace(var.prefix, "-", "_")}_data"
+ encryption_key = try(local.service_encryption_keys.bq, null) # Example assignment of an encryption key
+}
+
+###############################################################################
+# 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"
+ project_id = module.project.project_id
+ name = "notebook-sa"
+ iam_project_roles = {
+ (module.project.project_id) = [
+ "roles/bigquery.admin",
+ "roles/bigquery.jobUser",
+ "roles/bigquery.dataEditor",
+ "roles/bigquery.user",
+ "roles/dialogflow.client",
+ "roles/storage.admin",
+ ]
+ }
+}
+
+resource "google_notebooks_instance" "playground" {
+ name = "${var.prefix}-notebook"
+ location = format("%s-%s", var.region, "b")
+ machine_type = "e2-medium"
+ project = module.project.project_id
+
+ container_image {
+ repository = "gcr.io/deeplearning-platform-release/base-cpu"
+ tag = "latest"
+ }
+
+ install_gpu_driver = true
+ boot_disk_type = "PD_SSD"
+ boot_disk_size_gb = 110
+ disk_encryption = try(local.service_encryption_keys.compute != null, false) ? "CMEK" : null
+ kms_key = try(local.service_encryption_keys.compute, null)
+
+ 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
+
+ service_account = module.service-account-notebook.email
+
+ #TODO Uncomment once terraform-provider-google/issues/9273 is fixed
+ # tags = ["ssh"]
+}
diff --git a/blueprints/data-solutions/data-playground/outputs.tf b/blueprints/data-solutions/data-playground/outputs.tf
new file mode 100644
index 0000000000..4b80c311c5
--- /dev/null
+++ b/blueprints/data-solutions/data-playground/outputs.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
+#
+# 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 "bucket" {
+ description = "GCS Bucket URL."
+ value = module.bucket.url
+}
+
+output "dataset" {
+ description = "GCS Bucket URL."
+ value = module.dataset.id
+}
+
+output "notebook" {
+ description = "Vertex AI notebook details."
+ value = {
+ name = resource.google_notebooks_instance.playground.name
+ id = resource.google_notebooks_instance.playground.id
+ }
+}
+
+output "project" {
+ description = "Project id."
+ value = module.project.project_id
+}
+
+output "vpc" {
+ description = "VPC Network."
+ value = module.vpc.name
+}
diff --git a/blueprints/data-solutions/data-playground/variables.tf b/blueprints/data-solutions/data-playground/variables.tf
new file mode 100644
index 0000000000..1735406733
--- /dev/null
+++ b/blueprints/data-solutions/data-playground/variables.tf
@@ -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
+#
+# 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.
+
+
+variable "location" {
+ description = "The location where resources will be deployed."
+ type = string
+ default = "EU"
+}
+
+variable "prefix" {
+ description = "Prefix used for resource names."
+ type = string
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty."
+ }
+}
+
+variable "project_create" {
+ description = "Provide values if project creation is needed, uses existing project if null. Parent format: folders/folder_id or organizations/org_id."
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = null
+}
+
+variable "project_id" {
+ description = "Project id, references existing project if `project_create` is null."
+ type = string
+}
+
+variable "region" {
+ description = "The region where resources will be deployed."
+ type = string
+ default = "europe-west1"
+}
+
+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
+}
+
+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
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/data-solutions/data-playground/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/README.md b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/README.md
new file mode 100644
index 0000000000..54f47ecab5
--- /dev/null
+++ b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/README.md
@@ -0,0 +1,217 @@
+# Spinning up a foundation data pipeline on Google Cloud using Cloud Storage, Dataflow and BigQuery
+
+## Introduction
+
+This repository contains the necessary Terraform modules to securely deploy a basic ETL pipeline that will dump data from a Google Cloud Storage (GCS) bucket to tables in BigQuery.
+
+An ETL pipeline is defined in three steps:
+
+* Extraction: retrieving data from sources.
+* Transformation: cleaning the data, putting it into a common format, calculating other fields, taking out duplicates or erroneous records so it can be stored into a target.
+* Loading: inserting the formatted data into the target database, data store, data warehouse or data lake.
+
+You can learn more about cloud-based ETL [here](https://cloud.google.com/learn/what-is-etl).
+
+## Use cases
+
+Whether you’re transferring from another Cloud Service Provider or you’re taking your first steps into the cloud with Google Cloud, building a data pipeline sets a good foundation to begin deriving insights for your business.
+
+* __Anomaly Detection__: building data pipelines to identify cyber security threats or fraudulent transactions using machine learning (ML) models.
+* __Interactive Data Analysis__: carry out interactive data analysis with BigQuery BI Engine that enables you to analyze large and complex datasets interactively with sub-second query response time and high concurrency.
+* __Predictive Forecasting__: building solid pipelines to capture real-time data for ML modeling and using it as a forecasting engine for situations ranging from weather predictions to market forecasting.
+* __Create Machine Learning models__: using BigQueryML you can create and execute machine learning models in BigQuery using standard SQL queries. Create a variety of models pre-built into BigQuery that you train with your data.
+
+## Architecture
+
+![GCS to BigQuery High-level diagram](images/diagram.png "GCS to BigQuery High-level diagram")
+
+The main components that we would be setting up are (to learn more about these products, click on the hyperlinks):
+
+* [Cloud Storage (GCS) bucket](https://cloud.google.com/storage/): data lake solution to store extracted raw data that must undergo some kind of transformation.
+* [Cloud Dataflow pipeline](https://cloud.google.com/dataflow): to build fully managed batch and streaming pipelines to transform data stored in GCS buckets ready for processing in the Data Warehouse using Apache Beam.
+* [BigQuery datasets and tables](https://cloud.google.com/bigquery): to store the transformed data in and query it using SQL, use it to make reports or begin training [machine learning](https://cloud.google.com/bigquery-ml/docs/introduction) models without having to take your data out.
+* [Service accounts](https://cloud.google.com/iam/docs/service-accounts) (__created with least privilege on each resource__): one for uploading data into the GCS bucket, one for Orchestration, one for Dataflow instances and one for the BigQuery tables. You can also configure users or groups of users to assign them a viewer role on the created resources and the ability to impersonate service accounts to test the Dataflow pipelines before automating them with a tool like [Cloud Composer](https://cloud.google.com/composer).
+
+For a full list of the resources that will be created, please refer to the [github repository](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/tree/master/blueprints/data-solutions/gcs-to-bq-with-least-privileges) for this project. If you're migrating from another Cloud Provider, refer to [this](https://cloud.google.com/free/docs/aws-azure-gcp-service-comparison) documentation to see equivalent services and comparisons in Microsoft Azure and Amazon Web Services
+
+## Costs
+
+Pricing Estimates - We have created a sample estimate based on some usage we see from new startups looking to scale. This estimate would give you an idea of how much this deployment would essentially cost per month at this scale and you extend it to the scale you further prefer. Here's the [link](https://cloud.google.com/products/calculator#id=44710202-c9d4-49d5-a378-99d7dd34f5e2).
+
+## Setup
+
+This solution assumes you already have a project created and set up where you wish to host these resources. If not, and you would like for the project to create a new project as well, please refer to the [github repository](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/tree/master/blueprints/data-solutions/gcs-to-bq-with-least-privileges) for instructions.
+
+### Prerequisites
+
+* Have an [organization](https://cloud.google.com/resource-manager/docs/creating-managing-organization) set up in Google cloud.
+* Have a [billing account](https://cloud.google.com/billing/docs/how-to/manage-billing-account) set up.
+* Have 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), we’ll call this the __service project__.
+
+### Roles & Permissions
+
+In order to spin up this architecture, you will need to be a user with the “__Project owner__” [IAM](https://cloud.google.com/iam) role on the existing 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
+
+#### Step 1: Cloning the repository
+
+Click on the button below, sign in if required and when the prompt appears, click on “confirm”.
+
+[![Open Cloudshell](../../../assets/images/cloud-shell-button.png)](https://goo.gle/GoDataPipe)
+
+This will clone the repository to your cloud shell and a screen like this one will appear:
+
+![cloud_shell](images/cloud_shell.png)
+
+Before you deploy the architecture, make sure you run the following command to move your cloudshell session into your service project:
+
+ gcloud config set project [SERVICE_PROJECT_ID]
+
+Once you can see your service project id in the yellow parenthesis, you’re ready to start.
+
+Before we deploy the architecture, you will need the following information:
+
+* The __service project ID__.
+* A __unique prefix__ that you want all the deployed resources to have (for example: awesomestartup). This must be a string with no spaces or tabs.
+* A __list of Groups or Users__ with Service Account Token creator role on Service Accounts in IAM format, eg 'group:group@domain.com'.
+
+#### Step 2: Deploying the resources
+
+1. Once you have the required information, head back to the cloud shell editor. Make sure you’re in the following directory:
+
+ cloudshell_open/cloud-foundation-fabric/blueprints/data-solutions/gcs-to-bq-with-least-privileges
+
+2. In the editor, edit the terraform.tfvars.sample file with the variables you gathered in the step above.
+
+![editor](images/editor.png)
+
+* a. Fill in __data_eng_principals__ with the list of Users or Groups to impersonate service accounts.
+
+* b. Fill in __project_id__ with the service project ID.
+
+* c. Fill in the prefix with your chosen unique prefix for resources.
+
+* d. Save the file with __Ctrl(or ⌘)+S__ or by going to __File → Save__.
+
+3. Then, run the following commands:
+
+ terraform init
+
+ terraform apply -var-file=terraform.tfvars.sample -auto-approve
+
+The resource creation will take a few minutes, at the end this is the output you should expect for successful completion along with a list of the created resources:
+
+![output](images/output.png)
+
+__Congratulations!__ You have successfully deployed the foundation for running your first ETL pipeline on Google Cloud.
+
+### Testing your architecture
+
+For the purpose of demonstrating how the ETL pipeline flow works, we’ve set up an example pipeline for you to run. First of all, we assume all the steps are run using a user listed on the __data_eng_principles__ variable (or a user that belongs to one of the groups you specified). Authenticate the user using the following command and make sure your active cloudshell session is set to the __service project__:
+
+ gcloud auth application-default login
+
+Follow the instructions in the cloudshell to authenticate the user.
+
+To make the next steps easier, create two environment variables with the service project id and the prefix:
+
+ export SERVICE_PROJECT_ID=[SERVICE_PROJECT_ID]
+ export PREFIX=[PREFIX]
+
+Again, make sure you’re in the following directory:
+
+ cloudshell_open/cloud-foundation-fabric/blueprints/data-solutions/gcs-to-bq-with-least-privileges
+
+For the purpose of the example we will import from GCS to Bigquery a CSV file with the following structure:
+
+ name,surname,timestamp
+
+We need to create 3 files:
+
+* A person.csv file containing your data in the form name,surname,timestamp. For example: `Eva,Rivarola,1637771951'.
+* A person_udf.js containing the [UDF javascript file](https://cloud.google.com/bigquery/docs/reference/standard-sql/user-defined-functions) used by the Dataflow template.
+* A person_schema.json file containing the table schema used to import the CSV.
+
+An example of those files can be found in the folder ./data-demo. Inside the same repository where you ran the terraform commands.
+
+You can copy the example files into the GCS bucket by running:
+
+ gsutil -i gcs-landing@$SERVICE_PROJECT_ID.iam.gserviceaccount.com cp data-demo/* gs://$PREFIX-data
+
+Once this is done, the 3 files necessary to run the Dataflow Job will have been copied to the GCS bucket that was created along with the resources.
+
+Run the following command to start the dataflow job:
+
+ gcloud --impersonate-service-account=orchestrator@$SERVICE_PROJECT_ID.iam.gserviceaccount.com dataflow jobs run test_batch_01 \
+ --gcs-location gs://dataflow-templates/latest/GCS_Text_to_BigQuery \
+ --project $SERVICE_PROJECT_ID \
+ --region europe-west1 \
+ --disable-public-ips \
+ --subnetwork https://www.googleapis.com/compute/v1/projects/$SERVICE_PROJECT_ID/regions/europe-west1/subnetworks/subnet \
+ --staging-location gs://$PREFIX-df-tmp \
+ --service-account-email df-loading@$SERVICE_PROJECT_ID.iam.gserviceaccount.com \
+ --parameters \
+ javascriptTextTransformFunctionName=transform,\
+ JSONPath=gs://$PREFIX-data/person_schema.json,\
+ javascriptTextTransformGcsPath=gs://$PREFIX-data/person_udf.js,\
+ inputFilePattern=gs://$PREFIX-data/person.csv,\
+ outputTable=$SERVICE_PROJECT_ID:datalake.person,\
+ bigQueryLoadingTemporaryDirectory=gs://$PREFIX-df-tmp
+
+This command will start a dataflow job called test_batch_01 that uses a Dataflow transformation script stored in the public GCS bucket:
+
+ gs://dataflow-templates/latest/GCS_Text_to_BigQuery.
+
+The expected output is the following:
+
+![second_output](images/second_output.png)
+
+Then, if you navigate to Dataflow on the console, you will see the following:
+
+![dataflow_console](images/dataflow_console.png)
+
+This shows the job you started from the cloudshell is currently running in Dataflow.
+If you click on the job name, you can see the job graph created and how every step of the Dataflow pipeline is moving along:
+
+![dataflow_execution](images/dataflow_execution.png)
+
+Once the job completes, you can navigate to BigQuery in the console and under __SERVICE_PROJECT_ID__ → datalake → person, you can see the data that was successfully imported into BigQuery through the Dataflow job.
+
+## Cleaning up your environment
+
+The easiest way to remove all the deployed resources is to run the following command in Cloud Shell:
+
+ terraform destroy -var-file=terraform.tfvars.sample -auto-approve
+
+The above command will delete the associated resources so there will be no billable charges made afterwards.
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [prefix](variables.tf#L36) | Prefix used for resource names. | string
| ✓ | |
+| [project_id](variables.tf#L54) | Project id, references existing project if `project_create` is null. | string
| ✓ | |
+| [cmek_encryption](variables.tf#L15) | Flag to enable CMEK on GCP resources created. | bool
| | false
|
+| [data_eng_principals](variables.tf#L21) | Groups with Service Account Token creator role on service accounts in IAM format, eg 'group:group@domain.com'. | list(string)
| | []
|
+| [network_config](variables.tf#L27) | Shared VPC network configurations to use. If null networks will be created in projects with preconfigured values. | object({…})
| | null
|
+| [project_create](variables.tf#L45) | Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…})
| | null
|
+| [region](variables.tf#L59) | The region where resources will be deployed. | string
| | "europe-west1"
|
+| [vpc_subnet_range](variables.tf#L65) | Ip range used for the VPC subnet created for the example. | string
| | "10.0.0.0/20"
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [bq_tables](outputs.tf#L15) | Bigquery Tables. | |
+| [buckets](outputs.tf#L20) | GCS bucket Cloud KMS crypto keys. | |
+| [command_01_gcs](outputs.tf#L28) | gcloud command to copy data into the created bucket impersonating the service account. | |
+| [command_02_dataflow](outputs.tf#L33) | Command to run Dataflow template impersonating the service account. | |
+| [command_03_bq](outputs.tf#L54) | BigQuery command to query imported data. | |
+| [project_id](outputs.tf#L64) | Project id. | |
+| [service_accounts](outputs.tf#L69) | Service account. | |
+
+
diff --git a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/backend.tf.sample b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/backend.tf.sample
new file mode 100644
index 0000000000..49a0883db6
--- /dev/null
+++ b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/backend.tf.sample
@@ -0,0 +1,30 @@
+# 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.
+
+# The `impersonate_service_account` option require the identity launching terraform
+# role `roles/iam.serviceAccountTokenCreator` on the Service Account specified.
+
+terraform {
+ backend "gcs" {
+ bucket = "BUCKET_NAME"
+ prefix = "PREFIX"
+ impersonate_service_account = "SERVICE_ACCOUNT@PROJECT_ID.iam.gserviceaccount.com"
+ }
+}
+provider "google" {
+ impersonate_service_account = "SERVICE_ACCOUNT@PROJECT_ID.iam.gserviceaccount.com"
+}
+provider "google-beta" {
+ impersonate_service_account = "SERVICE_ACCOUNT@PROJECT_ID.iam.gserviceaccount.com"
+}
\ No newline at end of file
diff --git a/examples/data-solutions/gcs-to-bq-with-least-privileges/data-demo/person.csv b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/data-demo/person.csv
similarity index 100%
rename from examples/data-solutions/gcs-to-bq-with-least-privileges/data-demo/person.csv
rename to blueprints/data-solutions/gcs-to-bq-with-least-privileges/data-demo/person.csv
diff --git a/examples/data-solutions/gcs-to-bq-with-least-privileges/data-demo/person.json b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/data-demo/person.json
similarity index 100%
rename from examples/data-solutions/gcs-to-bq-with-least-privileges/data-demo/person.json
rename to blueprints/data-solutions/gcs-to-bq-with-least-privileges/data-demo/person.json
diff --git a/examples/data-solutions/gcs-to-bq-with-least-privileges/data-demo/person_schema.json b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/data-demo/person_schema.json
similarity index 100%
rename from examples/data-solutions/gcs-to-bq-with-least-privileges/data-demo/person_schema.json
rename to blueprints/data-solutions/gcs-to-bq-with-least-privileges/data-demo/person_schema.json
diff --git a/examples/data-solutions/gcs-to-bq-with-least-privileges/data-demo/person_udf.js b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/data-demo/person_udf.js
similarity index 100%
rename from examples/data-solutions/gcs-to-bq-with-least-privileges/data-demo/person_udf.js
rename to blueprints/data-solutions/gcs-to-bq-with-least-privileges/data-demo/person_udf.js
diff --git a/examples/data-solutions/gcs-to-bq-with-least-privileges/datastorage.tf b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/datastorage.tf
similarity index 100%
rename from examples/data-solutions/gcs-to-bq-with-least-privileges/datastorage.tf
rename to blueprints/data-solutions/gcs-to-bq-with-least-privileges/datastorage.tf
diff --git a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/dataflow_console.png b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/dataflow_console.png
new file mode 100644
index 0000000000..526aa05785
Binary files /dev/null and b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/dataflow_console.png differ
diff --git a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/dataflow_execution.png b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/dataflow_execution.png
new file mode 100644
index 0000000000..690e498e0a
Binary files /dev/null and b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/dataflow_execution.png differ
diff --git a/examples/data-solutions/gcs-to-bq-with-least-privileges/diagram.png b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/diagram.png
similarity index 100%
rename from examples/data-solutions/gcs-to-bq-with-least-privileges/diagram.png
rename to blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/diagram.png
diff --git a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/editor.png b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/editor.png
new file mode 100644
index 0000000000..b6626aa1d7
Binary files /dev/null and b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/editor.png differ
diff --git a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/output.png b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/output.png
new file mode 100644
index 0000000000..3758c3145b
Binary files /dev/null and b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/output.png differ
diff --git a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/second_output.png b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/second_output.png
new file mode 100644
index 0000000000..618c341551
Binary files /dev/null and b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/second_output.png differ
diff --git a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/shell_button.png b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/shell_button.png
new file mode 100644
index 0000000000..21a3f3de9d
Binary files /dev/null and b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/images/shell_button.png differ
diff --git a/examples/data-solutions/gcs-to-bq-with-least-privileges/kms.tf b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/kms.tf
similarity index 100%
rename from examples/data-solutions/gcs-to-bq-with-least-privileges/kms.tf
rename to blueprints/data-solutions/gcs-to-bq-with-least-privileges/kms.tf
diff --git a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/main.tf b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/main.tf
new file mode 100644
index 0000000000..b4e0b8347e
--- /dev/null
+++ b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/main.tf
@@ -0,0 +1,138 @@
+# 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.
+
+locals {
+ iam = {
+ # GCS roles
+ "roles/storage.objectAdmin" = [
+ module.service-account-df.iam_email,
+ module.service-account-landing.iam_email
+ ],
+ "roles/storage.objectViewer" = [
+ module.service-account-orch.iam_email,
+ ],
+ # BigQuery roles
+ "roles/bigquery.admin" = concat([
+ module.service-account-orch.iam_email,
+ ], var.data_eng_principals
+ )
+ "roles/bigquery.dataEditor" = [
+ module.service-account-df.iam_email,
+ module.service-account-bq.iam_email
+ ]
+ "roles/bigquery.dataViewer" = [
+ module.service-account-bq.iam_email,
+ module.service-account-orch.iam_email
+ ]
+ "roles/bigquery.jobUser" = [
+ module.service-account-df.iam_email,
+ module.service-account-bq.iam_email
+ ]
+ "roles/bigquery.user" = [
+ module.service-account-bq.iam_email,
+ module.service-account-df.iam_email
+ ]
+ # common roles
+ "roles/logging.admin" = var.data_eng_principals
+ "roles/logging.logWriter" = [
+ module.service-account-bq.iam_email,
+ module.service-account-landing.iam_email,
+ module.service-account-orch.iam_email,
+ ]
+ "roles/monitoring.metricWriter" = [
+ module.service-account-bq.iam_email,
+ module.service-account-landing.iam_email,
+ module.service-account-orch.iam_email,
+ ]
+ "roles/iam.serviceAccountUser" = [
+ module.service-account-orch.iam_email,
+ ]
+ "roles/iam.serviceAccountTokenCreator" = concat(
+ var.data_eng_principals
+ )
+ # Dataflow roles
+ "roles/dataflow.admin" = concat(
+ [module.service-account-orch.iam_email],
+ var.data_eng_principals
+ )
+ "roles/dataflow.worker" = [
+ module.service-account-df.iam_email,
+ ]
+ "roles/dataflow.developer" = var.data_eng_principals
+ "roles/compute.viewer" = var.data_eng_principals
+ # network roles
+ "roles/compute.networkUser" = [
+ module.service-account-df.iam_email,
+ "serviceAccount:${module.project.service_accounts.robots.dataflow}"
+ ]
+ }
+ network_subnet_selflink = try(
+ module.vpc[0].subnets["${var.region}/subnet"].self_link,
+ var.network_config.subnet_self_link
+ )
+ shared_vpc_bindings = {
+ "roles/compute.networkUser" = [
+ "robot-df", "sa-df-worker"
+ ]
+ }
+ # 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
+ }
+ shared_vpc_project = try(var.network_config.host_project, null)
+ shared_vpc_role_members = {
+ robot-df = "serviceAccount:${module.project.service_accounts.robots.dataflow}"
+ sa-df-worker = module.service-account-df.iam_email
+ }
+ use_shared_vpc = var.network_config != null
+}
+
+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.project_create == null ? null : var.prefix
+ services = [
+ "bigquery.googleapis.com",
+ "bigquerystorage.googleapis.com",
+ "bigqueryreservation.googleapis.com",
+ "cloudkms.googleapis.com",
+ "compute.googleapis.com",
+ "dataflow.googleapis.com",
+ "servicenetworking.googleapis.com",
+ "storage.googleapis.com",
+ "storage-component.googleapis.com",
+ ]
+
+ # additive IAM bindings avoid disrupting bindings in existing project
+ iam = var.project_create != null ? local.iam : {}
+ iam_additive = var.project_create == null ? local.iam : {}
+ shared_vpc_service_config = local.shared_vpc_project == null ? null : {
+ attach = true
+ host_project = local.shared_vpc_project
+ }
+}
+
+resource "google_project_iam_member" "shared_vpc" {
+ for_each = local.use_shared_vpc ? local.shared_vpc_bindings_map : {}
+ project = var.network_config.host_project
+ role = each.value.role
+ member = lookup(local.shared_vpc_role_members, each.value.member)
+}
diff --git a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/outputs.tf b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/outputs.tf
new file mode 100644
index 0000000000..82b059cc1d
--- /dev/null
+++ b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/outputs.tf
@@ -0,0 +1,77 @@
+# 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.
+
+output "bq_tables" {
+ description = "Bigquery Tables."
+ value = module.bigquery-dataset.table_ids
+}
+
+output "buckets" {
+ description = "GCS bucket Cloud KMS crypto keys."
+ value = {
+ data = module.gcs-data.name
+ df-tmp = module.gcs-df-tmp.name
+ }
+}
+
+output "command_01_gcs" {
+ description = "gcloud command to copy data into the created bucket impersonating the service account."
+ value = "gsutil -i ${module.service-account-landing.email} cp data-demo/* ${module.gcs-data.url}"
+}
+
+output "command_02_dataflow" {
+ description = "Command to run Dataflow template impersonating the service account."
+ value = templatefile("${path.module}/templates/dataflow.tftpl", {
+ sa_orch_email = module.service-account-orch.email
+ project_id = module.project.project_id
+ region = var.region
+ subnet = local.network_subnet_selflink
+ gcs_df_stg = format("%s/%s", module.gcs-df-tmp.url, "stg")
+ sa_df_email = module.service-account-df.email
+ cmek_encryption = var.cmek_encryption
+ kms_key_df = var.cmek_encryption ? module.kms[0].key_ids.key-df : null
+ gcs_data = module.gcs-data.url
+ data_schema_file = format("%s/%s", module.gcs-data.url, "person_schema.json")
+ data_udf_file = format("%s/%s", module.gcs-data.url, "person_udf.js")
+ data_file = format("%s/%s", module.gcs-data.url, "person.csv")
+ bigquery_dataset = module.bigquery-dataset.dataset_id
+ bigquery_table = module.bigquery-dataset.tables["person"].table_id
+ gcs_df_tmp = format("%s/%s", module.gcs-df-tmp.url, "tmp")
+ })
+}
+
+output "command_03_bq" {
+ description = "BigQuery command to query imported data."
+ value = templatefile("${path.module}/templates/bigquery.tftpl", {
+ project_id = module.project.project_id
+ bigquery_dataset = module.bigquery-dataset.dataset_id
+ bigquery_table = module.bigquery-dataset.tables["person"].table_id
+ sql_limit = 1000
+ })
+}
+
+output "project_id" {
+ description = "Project id."
+ value = module.project.project_id
+}
+
+output "service_accounts" {
+ description = "Service account."
+ value = {
+ bq = module.service-account-bq.email
+ df = module.service-account-df.email
+ orch = module.service-account-orch.email
+ landing = module.service-account-landing.email
+ }
+}
diff --git a/examples/data-solutions/gcs-to-bq-with-least-privileges/serviceaccounts.tf b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/serviceaccounts.tf
similarity index 100%
rename from examples/data-solutions/gcs-to-bq-with-least-privileges/serviceaccounts.tf
rename to blueprints/data-solutions/gcs-to-bq-with-least-privileges/serviceaccounts.tf
diff --git a/examples/data-solutions/gcs-to-bq-with-least-privileges/bigquery.tftpl b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/templates/bigquery.tftpl
similarity index 100%
rename from examples/data-solutions/gcs-to-bq-with-least-privileges/bigquery.tftpl
rename to blueprints/data-solutions/gcs-to-bq-with-least-privileges/templates/bigquery.tftpl
diff --git a/examples/data-solutions/gcs-to-bq-with-least-privileges/dataflow.tftpl b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/templates/dataflow.tftpl
similarity index 100%
rename from examples/data-solutions/gcs-to-bq-with-least-privileges/dataflow.tftpl
rename to blueprints/data-solutions/gcs-to-bq-with-least-privileges/templates/dataflow.tftpl
diff --git a/examples/data-solutions/gcs-to-bq-with-least-privileges/terraform.tfvars.sample b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/terraform.tfvars.sample
similarity index 100%
rename from examples/data-solutions/gcs-to-bq-with-least-privileges/terraform.tfvars.sample
rename to blueprints/data-solutions/gcs-to-bq-with-least-privileges/terraform.tfvars.sample
diff --git a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/variables.tf b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/variables.tf
new file mode 100644
index 0000000000..97d3de77f9
--- /dev/null
+++ b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/variables.tf
@@ -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
+#
+# 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.
+
+variable "cmek_encryption" {
+ description = "Flag to enable CMEK on GCP resources created."
+ type = bool
+ default = false
+}
+
+variable "data_eng_principals" {
+ description = "Groups with Service Account Token creator role on service accounts in IAM format, eg 'group:group@domain.com'."
+ type = list(string)
+ default = []
+}
+
+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
+ subnet_self_link = string
+ })
+ default = null
+}
+
+variable "prefix" {
+ description = "Prefix used for resource names."
+ type = string
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty."
+ }
+}
+
+variable "project_create" {
+ description = "Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format."
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = null
+}
+
+variable "project_id" {
+ description = "Project id, references existing project if `project_create` is null."
+ type = string
+}
+
+variable "region" {
+ description = "The region where resources will be deployed."
+ type = string
+ default = "europe-west1"
+}
+
+variable "vpc_subnet_range" {
+ description = "Ip range used for the VPC subnet created for the example."
+ type = string
+ default = "10.0.0.0/20"
+}
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
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/data-solutions/gcs-to-bq-with-least-privileges/vpc.tf b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/vpc.tf
new file mode 100644
index 0000000000..fd47952bfb
--- /dev/null
+++ b/blueprints/data-solutions/gcs-to-bq-with-least-privileges/vpc.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
+#
+# 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.
+
+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_subnet_range
+ name = "subnet"
+ region = var.region
+ }
+ ]
+}
+
+module "vpc-firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ count = local.use_shared_vpc ? 0 : 1
+ project_id = module.project.project_id
+ network = module.vpc[0].name
+ default_rules_config = {
+ admin_ranges = [var.vpc_subnet_range]
+ }
+}
+
+module "nat" {
+ source = "../../../modules/net-cloudnat"
+ count = local.use_shared_vpc ? 0 : 1
+ project_id = module.project.project_id
+ region = var.region
+ name = "${var.prefix}-default"
+ router_network = module.vpc[0].name
+}
diff --git a/blueprints/data-solutions/sqlserver-alwayson/README.md b/blueprints/data-solutions/sqlserver-alwayson/README.md
new file mode 100644
index 0000000000..1ce4dad7d4
--- /dev/null
+++ b/blueprints/data-solutions/sqlserver-alwayson/README.md
@@ -0,0 +1,71 @@
+## SQL Server Always On Groups blueprint
+
+This is an blueprint of building [SQL Server Always On Availability Groups](https://cloud.google.com/compute/docs/instances/sql-server/configure-availability)
+using Fabric modules. It builds a two node cluster with a fileshare witness instance in an existing VPC and adds the necessary firewalling.
+
+![Architecture diagram](https://cloud.google.com/compute/images/sqlserver-ag-architecture.svg)
+
+The actual setup process (apart from Active Directory operations) has been scripted, so that least amount of
+manual works needs to performed:
+
+ - Joining the domain using appropriate credentials
+ - Running an automatically generated initialization script (`C:\InitializeCluster.ps1`)
+ - Creating the [Availability Groups using the wizard](https://cloud.google.com/compute/docs/instances/sql-server/configure-availability#creating_an_availability_group)
+ (please note that healthchecks are automatically configured when the appropriate AGs are created)
+
+To monitor the installation process, the startup scripts log output to Application Log (visible under Windows Logs in Event Viewer)
+and to `C:\GcpSetupLog.txt` file.
+
+
+
+
+## Files
+
+| name | description | modules |
+|---|---|---|
+| [instances.tf](./instances.tf) | Creates SQL Server instances and witness. | compute-vm
|
+| [main.tf](./main.tf) | Module-level locals and resources. | project
|
+| [outputs.tf](./outputs.tf) | Module outputs. | |
+| [secrets.tf](./secrets.tf) | Creates SQL admin user password secret. | secret-manager
|
+| [service-accounts.tf](./service-accounts.tf) | Creates service accounts for the instances. | iam-service-account
|
+| [variables.tf](./variables.tf) | Module variables. | |
+| [vpc.tf](./vpc.tf) | Creates the VPC and manages the firewall rules and ILB. | net-address
· net-ilb
· net-vpc
· net-vpc-firewall
|
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [ad_domain_fqdn](variables.tf#L15) | Active Directory domain (FQDN). | string
| ✓ | |
+| [ad_domain_netbios](variables.tf#L24) | Active Directory domain (NetBIOS). | string
| ✓ | |
+| [network](variables.tf#L90) | Network to use in the project. | string
| ✓ | |
+| [prefix](variables.tf#L113) | Prefix used for resource names. | string
| ✓ | |
+| [project_id](variables.tf#L131) | Google Cloud project ID. | string
| ✓ | |
+| [sql_admin_password](variables.tf#L148) | Password for the SQL admin user to be created. | string
| ✓ | |
+| [subnetwork](variables.tf#L163) | Subnetwork to use in the project. | string
| ✓ | |
+| [always_on_groups](variables.tf#L33) | List of Always On Groups. | list(string)
| | ["bookshelf"]
|
+| [boot_disk_size](variables.tf#L39) | Boot disk size in GB. | number
| | 50
|
+| [cluster_name](variables.tf#L45) | Cluster name (prepended with prefix). | string
| | "cluster"
|
+| [data_disk_size](variables.tf#L51) | Database disk size in GB. | number
| | 200
|
+| [health_check_config](variables.tf#L57) | Health check configuration. | …
| | {…}
|
+| [health_check_port](variables.tf#L72) | Health check port. | number
| | 59997
|
+| [health_check_ranges](variables.tf#L78) | Health check ranges. | list(string)
| | ["35.191.0.0/16", "209.85.152.0/22", "209.85.204.0/22"]
|
+| [managed_ad_dn](variables.tf#L84) | Managed Active Directory domain (eg. OU=Cloud,DC=example,DC=com). | string
| | ""
|
+| [node_image](variables.tf#L95) | SQL Server node machine image. | string
| | "projects/windows-sql-cloud/global/images/family/sql-ent-2019-win-2019"
|
+| [node_instance_type](variables.tf#L101) | SQL Server database node instance type. | string
| | "n2-standard-8"
|
+| [node_name](variables.tf#L107) | Node base name. | string
| | "node"
|
+| [project_create](variables.tf#L122) | Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…})
| | null
|
+| [region](variables.tf#L136) | Region for resources. | string
| | "europe-west4"
|
+| [shared_vpc_project_id](variables.tf#L142) | Shared VPC project ID for firewall rules. | string
| | null
|
+| [sql_client_cidrs](variables.tf#L157) | CIDR ranges that are allowed to connect to SQL Server. | list(string)
| | ["0.0.0.0/0"]
|
+| [vpc_ip_cidr_range](variables.tf#L168) | Ip range used in the subnet deployef in the Service Project. | string
| | "10.0.0.0/20"
|
+| [witness_image](variables.tf#L174) | SQL Server witness machine image. | string
| | "projects/windows-cloud/global/images/family/windows-2019"
|
+| [witness_instance_type](variables.tf#L180) | SQL Server witness node instance type. | string
| | "n2-standard-2"
|
+| [witness_name](variables.tf#L186) | Witness base name. | string
| | "witness"
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [instructions](outputs.tf#L19) | List of steps to follow after applying. | |
+
+
diff --git a/blueprints/data-solutions/sqlserver-alwayson/instances.tf b/blueprints/data-solutions/sqlserver-alwayson/instances.tf
new file mode 100644
index 0000000000..40f26e95d8
--- /dev/null
+++ b/blueprints/data-solutions/sqlserver-alwayson/instances.tf
@@ -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
+#
+# 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 Creates SQL Server instances and witness.
+locals {
+ _functions = templatefile("${path.module}/scripts/functions.ps1", local._template_vars0)
+ _scripts = [
+ "specialize-node",
+ "specialize-witness",
+ "windows-startup-node",
+ "windows-startup-witness"
+ ]
+ _secret_parts = split("/", module.secret-manager.secrets[local.ad_user_password_secret].id)
+ _template_vars0 = {
+ prefix = var.prefix
+ ad_domain = var.ad_domain_fqdn
+ ad_netbios = var.ad_domain_netbios
+ managed_ad_dn = var.managed_ad_dn
+ managed_ad_dn_path = var.managed_ad_dn != "" ? "-Path \"${var.managed_ad_dn}\"" : ""
+ health_check_port = var.health_check_port
+ sql_admin_password_secret = local._secret_parts[length(local._secret_parts) - 1]
+ cluster_ip = module.ip-addresses.internal_addresses["${var.prefix}-cluster"].address
+ loadbalancer_ips = jsonencode({ for aog in var.always_on_groups : aog => module.ip-addresses.internal_addresses["${var.prefix}-lb-${aog}"].address })
+ sql_cluster_name = local.cluster_netbios_name
+ sql_cluster_full = local.cluster_full_name
+ node_netbios_1 = local.node_netbios_names[0]
+ node_netbios_2 = local.node_netbios_names[1]
+ witness_netbios = local.witness_netbios_name
+ always_on_groups = join(",", var.always_on_groups)
+ sql_user_name = length(local._user_name) > 20 ? substr(local._user_name, 0, 20) : local._user_name
+ }
+ _template_vars = merge(local._template_vars0, {
+ functions = local._functions
+ })
+ _user_name = "${var.prefix}-sqlserver"
+ scripts = {
+ for script in local._scripts :
+ script => templatefile("${path.module}/scripts/${script}.ps1", local._template_vars)
+ }
+}
+
+
+# Nodes
+module "nodes" {
+ source = "../../../modules/compute-vm"
+ for_each = toset(local.node_netbios_names)
+
+ project_id = var.project_id
+ zone = local.node_zones[each.value]
+ name = each.value
+
+ instance_type = var.node_instance_type
+
+ network_interfaces = [{
+ network = local.network
+ subnetwork = local.subnetwork
+ nat = false
+ addresses = {
+ internal = module.ip-addresses.internal_addresses[each.value].address
+ external = null
+ }
+ }]
+
+ boot_disk = {
+ image = var.node_image
+ type = "pd-ssd"
+ size = var.boot_disk_size
+ }
+
+ attached_disks = [{
+ name = "${each.value}-datadisk"
+ size = var.data_disk_size
+ source_type = null
+ source = null
+ options = null
+ }]
+
+ service_account = module.compute-service-account.email
+ service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ metadata = {
+ enable-wsfc = "true"
+ sysprep-specialize-script-ps1 = local.scripts["specialize-node"]
+ windows-startup-script-ps1 = local.scripts["windows-startup-node"]
+ }
+
+ group = {
+ named_ports = {
+ }
+ }
+
+ service_account_create = false
+ create_template = false
+}
+
+# Witness
+module "witness" {
+ source = "../../../modules/compute-vm"
+ for_each = toset([local.witness_netbios_name])
+
+ project_id = var.project_id
+ zone = local.node_zones[each.value]
+ name = each.value
+
+ instance_type = var.witness_instance_type
+
+ network_interfaces = [{
+ network = local.network
+ subnetwork = local.subnetwork
+ nat = false
+ addresses = {
+ internal = module.ip-addresses.internal_addresses[each.value].address
+ external = null
+ }
+ }]
+
+ boot_disk = {
+ image = var.witness_image
+ type = "pd-ssd"
+ size = var.boot_disk_size
+ }
+
+ service_account = module.witness-service-account.email
+ service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ metadata = {
+ sysprep-specialize-script-ps1 = local.scripts["specialize-witness"]
+ windows-startup-script-ps1 = local.scripts["windows-startup-witness"]
+ }
+
+ service_account_create = false
+ create_template = false
+}
diff --git a/blueprints/data-solutions/sqlserver-alwayson/main.tf b/blueprints/data-solutions/sqlserver-alwayson/main.tf
new file mode 100644
index 0000000000..4a2550153f
--- /dev/null
+++ b/blueprints/data-solutions/sqlserver-alwayson/main.tf
@@ -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
+#
+# 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.
+
+locals {
+ ad_user_password_secret = "${local.cluster_full_name}-password"
+ cluster_full_name = "${var.prefix}-${var.cluster_name}"
+ cluster_netbios_name = (
+ length(local.cluster_full_name) > 15
+ ? substr(local.cluster_full_name, 0, 15)
+ : local.cluster_full_name
+ )
+ network = module.vpc.self_link
+ node_base = "${var.prefix}-${var.node_name}"
+ node_prefix = (
+ length(local.node_base) > 12
+ ? substr(local.node_base, 0, 12)
+ : local.node_base
+ )
+ node_netbios_names = [
+ for idx in range(1, 3) : format("%s-%02d", local.node_prefix, idx)
+ ]
+ node_zones = merge(
+ {
+ for idx, node_name in local.node_netbios_names :
+ node_name => local.zones[idx]
+ },
+ {
+ (local.witness_netbios_name) = local.zones[length(local.zones) - 1]
+ }
+ )
+ subnetwork = (
+ var.project_create != null
+ ? module.vpc.subnet_self_links["${var.region}/${var.subnetwork}"]
+ : data.google_compute_subnetwork.subnetwork[0].self_link
+ )
+ vpc_project = (
+ var.shared_vpc_project_id != null
+ ? var.shared_vpc_project_id
+ : module.project.project_id
+ )
+ witness_name = "${var.prefix}-${var.witness_name}"
+ witness_netbios_name = (
+ length(local.witness_name) > 15
+ ? substr(local.witness_name, 0, 15)
+ : local.witness_name
+ )
+ zones = (
+ var.project_create == null
+ ? data.google_compute_zones.zones[0].names
+ : formatlist("${var.region}-%s", ["a", "b", "c"])
+ )
+}
+
+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.project_create == null ? null : var.prefix
+ services = [
+ "compute.googleapis.com",
+ "secretmanager.googleapis.com",
+ ]
+
+ iam = {}
+ iam_additive = {}
+ shared_vpc_service_config = var.shared_vpc_project_id == null ? null : {
+ attach = true
+ host_project = var.shared_vpc_project_id
+ }
+}
diff --git a/blueprints/data-solutions/sqlserver-alwayson/outputs.tf b/blueprints/data-solutions/sqlserver-alwayson/outputs.tf
new file mode 100644
index 0000000000..1856f823c1
--- /dev/null
+++ b/blueprints/data-solutions/sqlserver-alwayson/outputs.tf
@@ -0,0 +1,32 @@
+# 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.
+
+locals {
+ loadbalancer_outputs = [for aog in var.always_on_groups : format("%s (%s)", module.listener-ilb[aog].forwarding_rule_address, aog)]
+}
+
+output "instructions" {
+ description = "List of steps to follow after applying."
+ value = <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
| ✓ | |
+
+
+## TODO
+
+- [ ] add external table support
+- [ ] add materialized view support
+
diff --git a/blueprints/factories/bigquery-factory/main.tf b/blueprints/factories/bigquery-factory/main.tf
new file mode 100644
index 0000000000..5995ea1914
--- /dev/null
+++ b/blueprints/factories/bigquery-factory/main.tf
@@ -0,0 +1,67 @@
+/**
+ * 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 {
+ views = {
+ for f in fileset("${var.views_dir}", "**/*.yaml") :
+ trimsuffix(f, ".yaml") => yamldecode(file("${var.views_dir}/${f}"))
+ }
+
+ tables = {
+ for f in fileset("${var.tables_dir}", "**/*.yaml") :
+ trimsuffix(f, ".yaml") => yamldecode(file("${var.tables_dir}/${f}"))
+ }
+
+ output = {
+ for dataset in distinct([for v in values(merge(local.views, local.tables)) : v.dataset]) :
+ dataset => {
+ "views" = {
+ for k, v in local.views :
+ v.view => {
+ friendly_name = v.view
+ labels = try(v.labels, null)
+ query = v.query
+ use_legacy_sql = try(v.use_legacy_sql, false)
+ deletion_protection = try(v.deletion_protection, false)
+ }
+ if v.dataset == dataset
+ },
+ "tables" = {
+ for k, v in local.tables :
+ v.table => {
+ friendly_name = v.table
+ labels = try(v.labels, null)
+ options = try(v.options, null)
+ partitioning = try(v.partitioning, null)
+ schema = jsonencode(v.schema)
+ use_legacy_sql = try(v.use_legacy_sql, false)
+ deletion_protection = try(v.deletion_protection, false)
+ }
+ if v.dataset == dataset
+ }
+ }
+ }
+}
+
+module "bq" {
+ source = "../../../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)
+}
diff --git a/blueprints/factories/bigquery-factory/variables.tf b/blueprints/factories/bigquery-factory/variables.tf
new file mode 100644
index 0000000000..774ec86e1c
--- /dev/null
+++ b/blueprints/factories/bigquery-factory/variables.tf
@@ -0,0 +1,30 @@
+/**
+ * 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_id" {
+ description = "Project ID."
+ type = string
+}
+
+variable "tables_dir" {
+ description = "Relative path for the folder storing table data."
+ type = string
+}
+
+variable "views_dir" {
+ 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
new file mode 100644
index 0000000000..b833304eb2
--- /dev/null
+++ b/blueprints/factories/cloud-identity-group-factory/README.md
@@ -0,0 +1,60 @@
+# Google Cloud Identity Group Factory
+
+This module allows creation and management of Cloud Identity Groups by defining them in well formatted `yaml` files.
+
+Yaml abstraction for Groups can simplify groups creation and members management. Yaml can be simpler and clearer comparing to HCL.
+
+## Example
+
+### Terraform code
+
+```hcl
+module "prod-firewall" {
+ source = "./fabric/blueprints/factories/cloud-identity-group-factory"
+
+ customer_id = "customers/C0xxxxxxx"
+ data_dir = "data"
+}
+# tftest skip
+```
+
+### Configuration Structure
+
+Groups configuration should be placed in a set of yaml files. The name of the file identify the name of the group.
+
+```bash
+├── data
+ ├── group1@domain.com.yaml
+ ├── group2@domain.com.yaml
+
+```
+
+### Group definition format and structure
+
+Within each file, the group entry structure is following:
+
+```yaml
+display_name: Group 1 # Group display name.
+description: Group 1 description # Group description.
+members: # List of group members.
+ - user_1@example.com
+ - user_2@example.com
+managers: # List of group managers.
+ - manager_1@example.com
+```
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [customer_id](variables.tf#L17) | Directory customer ID in the form customers/C0xxxxxxx. | string
| ✓ | |
+| [data_dir](variables.tf#L22) | Relative path for the folder storing configuration data. | string
| ✓ | |
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [group_id](outputs.tf#L17) | Group name => Group ID mapping. | |
+
+
diff --git a/blueprints/factories/cloud-identity-group-factory/main.tf b/blueprints/factories/cloud-identity-group-factory/main.tf
new file mode 100644
index 0000000000..b20d4a1a68
--- /dev/null
+++ b/blueprints/factories/cloud-identity-group-factory/main.tf
@@ -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.
+ */
+
+locals {
+ groups = {
+ for f in fileset("${var.data_dir}", "**/*.yaml") :
+ trimsuffix(f, ".yaml") => yamldecode(file("${var.data_dir}/${f}"))
+ }
+}
+
+module "group" {
+ source = "../../../modules/cloud-identity-group"
+ for_each = local.groups
+ customer_id = var.customer_id
+ name = each.key
+ display_name = try(each.value.display_name, null)
+ description = try(each.value.description, null)
+ members = try(each.value.members, [])
+ managers = try(each.value.managers, [])
+}
diff --git a/blueprints/factories/cloud-identity-group-factory/outputs.tf b/blueprints/factories/cloud-identity-group-factory/outputs.tf
new file mode 100644
index 0000000000..63535ca7fe
--- /dev/null
+++ b/blueprints/factories/cloud-identity-group-factory/outputs.tf
@@ -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.
+ */
+
+output "group_id" {
+ description = "Group name => Group ID mapping."
+ value = {
+ for k in module.group :
+ k.name => k.id
+ }
+}
diff --git a/blueprints/factories/cloud-identity-group-factory/variables.tf b/blueprints/factories/cloud-identity-group-factory/variables.tf
new file mode 100644
index 0000000000..012af8663b
--- /dev/null
+++ b/blueprints/factories/cloud-identity-group-factory/variables.tf
@@ -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.
+ */
+
+variable "customer_id" {
+ description = "Directory customer ID in the form customers/C0xxxxxxx."
+ type = string
+}
+
+variable "data_dir" {
+ description = "Relative path for the folder storing configuration data."
+ type = string
+}
+
diff --git a/blueprints/factories/net-vpc-firewall-yaml/README.md b/blueprints/factories/net-vpc-firewall-yaml/README.md
new file mode 100644
index 0000000000..5e7260e942
--- /dev/null
+++ b/blueprints/factories/net-vpc-firewall-yaml/README.md
@@ -0,0 +1,157 @@
+# Google Cloud VPC Firewall Factory
+
+This module allows creation and management of different types of firewall rules by defining them in well formatted `yaml` files.
+
+Yaml abstraction for FW rules can simplify users onboarding and also makes rules definition simpler and clearer comparing to HCL.
+
+Nested folder structure for yaml configurations is optionally supported, which allows better and structured code management for multiple teams and environments.
+
+## Example
+
+### Terraform code
+
+```hcl
+module "prod-firewall" {
+ source = "./fabric/blueprints/factories/net-vpc-firewall-yaml"
+
+ project_id = "my-prod-project"
+ network = "my-prod-network"
+ config_directories = [
+ "./prod",
+ "./common"
+ ]
+
+ log_config = {
+ metadata = "INCLUDE_ALL_METADATA"
+ }
+}
+
+module "dev-firewall" {
+ source = "./fabric/blueprints/factories/net-vpc-firewall-yaml"
+
+ project_id = "my-dev-project"
+ network = "my-dev-network"
+ config_directories = [
+ "./dev",
+ "./common"
+ ]
+}
+# tftest skip
+```
+
+### Configuration Structure
+
+```bash
+├── common
+│ ├── default-egress.yaml
+│ ├── lb-rules.yaml
+│ └── iap-ingress.yaml
+├── dev
+│ ├── team-a
+│ │ ├── databases.yaml
+│ │ └── webb-app-a.yaml
+│ └── team-b
+│ ├── backend.yaml
+│ └── frontend.yaml
+└── prod
+ ├── team-a
+ │ ├── databases.yaml
+ │ └── webb-app-a.yaml
+ └── team-b
+ ├── backend.yaml
+ └── frontend.yaml
+```
+
+### Rule definition format and structure
+
+Firewall rules configuration should be placed in a set of yaml files in a folder/s. Firewall rule entry structure is following:
+
+```yaml
+rule-name: # descriptive name, naming convention is adjusted by the module
+ allow: # `allow` or `deny`
+ - ports: ['443', '80'] # ports for a specific protocol, keep empty list `[]` for all ports
+ protocol: tcp # protocol, put `all` for any protocol
+ direction: EGRESS # EGRESS or INGRESS
+ disabled: false # `false` or `true`, FW rule is disabled when `true`, default value is `false`
+ priority: 1000 # rule priority value, default value is 1000
+ source_ranges: # list of source ranges, should be specified only for `INGRESS` rule
+ - 0.0.0.0/0
+ destination_ranges: # list of destination ranges, should be specified only for `EGRESS` rule
+ - 0.0.0.0/0
+ source_tags: ['some-tag'] # list of source tags, should be specified only for `INGRESS` rule
+ source_service_accounts: # list of source service accounts, should be specified only for `INGRESS` rule, cannot be specified together with `source_tags` or `target_tags`
+ - myapp@myproject-id.iam.gserviceaccount.com
+ target_tags: ['some-tag'] # list of target tags
+ target_service_accounts: # list of target service accounts, , cannot be specified together with `source_tags` or `target_tags`
+ - myapp@myproject-id.iam.gserviceaccount.com
+```
+
+
+Firewall rules example yaml configuration
+
+```bash
+cat ./prod/core-network/common-rules.yaml
+# allow ingress from GCLB to all instances in the network
+lb-health-checks:
+ allow:
+ - ports: []
+ protocol: tcp
+ direction: INGRESS
+ priority: 1001
+ source_ranges:
+ - 35.191.0.0/16
+ - 130.211.0.0/22
+
+# deny all egress
+deny-all:
+ deny:
+ - ports: []
+ protocol: all
+ direction: EGRESS
+ priority: 65535
+ destination_ranges:
+ - 0.0.0.0/0
+
+cat ./dev/team-a/web-app-a.yaml
+# Myapp egress
+web-app-a-egress:
+ allow:
+ - ports: [443]
+ protocol: tcp
+ direction: EGRESS
+ destination_ranges:
+ - 192.168.0.0/24
+ target_service_accounts:
+ - myapp@myproject-id.iam.gserviceaccount.com
+# Myapp ingress
+web-app-a-ingress:
+ allow:
+ - ports: [1234]
+ protocol: tcp
+ direction: INGRESS
+ source_service_accounts:
+ - frontend-sa@myproject-id.iam.gserviceaccount.com
+ target_service_accounts:
+ - web-app-a@myproject-id.iam.gserviceaccount.com
+```
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [config_directories](variables.tf#L17) | List of paths to folders where firewall configs are stored in yaml format. Folder may include subfolders with configuration files. Files suffix must be `.yaml`. | list(string)
| ✓ | |
+| [network](variables.tf#L30) | Name of the network this set of firewall rules applies to. | string
| ✓ | |
+| [project_id](variables.tf#L35) | Project Id. | string
| ✓ | |
+| [log_config](variables.tf#L22) | Log configuration. Possible values for `metadata` are `EXCLUDE_ALL_METADATA` and `INCLUDE_ALL_METADATA`. Set to `null` for disabling firewall logging. | object({…})
| | null
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [egress_allow_rules](outputs.tf#L17) | Egress rules with allow blocks. | |
+| [egress_deny_rules](outputs.tf#L25) | Egress rules with allow blocks. | |
+| [ingress_allow_rules](outputs.tf#L33) | Ingress rules with allow blocks. | |
+| [ingress_deny_rules](outputs.tf#L41) | Ingress rules with deny blocks. | |
+
+
diff --git a/examples/factories/net-vpc-firewall-yaml/main.tf b/blueprints/factories/net-vpc-firewall-yaml/main.tf
similarity index 100%
rename from examples/factories/net-vpc-firewall-yaml/main.tf
rename to blueprints/factories/net-vpc-firewall-yaml/main.tf
diff --git a/examples/factories/net-vpc-firewall-yaml/outputs.tf b/blueprints/factories/net-vpc-firewall-yaml/outputs.tf
similarity index 100%
rename from examples/factories/net-vpc-firewall-yaml/outputs.tf
rename to blueprints/factories/net-vpc-firewall-yaml/outputs.tf
diff --git a/examples/factories/net-vpc-firewall-yaml/variables.tf b/blueprints/factories/net-vpc-firewall-yaml/variables.tf
similarity index 100%
rename from examples/factories/net-vpc-firewall-yaml/variables.tf
rename to blueprints/factories/net-vpc-firewall-yaml/variables.tf
diff --git a/blueprints/factories/net-vpc-firewall-yaml/versions.tf b/blueprints/factories/net-vpc-firewall-yaml/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/factories/net-vpc-firewall-yaml/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/factories/project-factory/README.md b/blueprints/factories/project-factory/README.md
new file mode 100644
index 0000000000..32a1f8f070
--- /dev/null
+++ b/blueprints/factories/project-factory/README.md
@@ -0,0 +1,255 @@
+# Minimal Project Factory
+
+This module implements a minimal, opinionated project factory (see [Factories](../README.md) for rationale) that allows for the creation of projects.
+
+While the module can be invoked by manually populating the required variables, its interface is meant for the massive creation of resources leveraging a set of well-defined YaML documents, as shown in the examples below.
+
+The Project Factory is meant to be executed by a Service Account (or a regular user) having this minimal set of permissions over your resources:
+
+* **Org level** - a custom role for networking operations including the following permissions
+ * `"compute.organizations.enableXpnResource"`,
+ * `"compute.organizations.disableXpnResource"`,
+ * `"compute.subnetworks.setIamPolicy"`,
+ * `"dns.networks.bindPrivateDNSZone"`
+ * and role `"roles/orgpolicy.policyAdmin"`
+* **on each folder** where projects will be created
+ * `"roles/logging.admin"`
+ * `"roles/owner"`
+ * `"roles/resourcemanager.folderAdmin"`
+ * `"roles/resourcemanager.projectCreator"`
+* **on the host project** for the Shared VPC/s
+ * `"roles/browser"`
+ * `"roles/compute.viewer"`
+ * `"roles/dns.admin"`
+
+## Example
+
+### Directory structure
+
+```
+.
+├── data
+│ ├── defaults.yaml
+│ └── projects
+│ ├── project-example-one.yaml
+│ ├── project-example-two.yaml
+│ └── project-example-three.yaml
+├── main.tf
+└── terraform.tfvars
+
+```
+
+### Terraform code
+
+```hcl
+locals {
+ defaults = yamldecode(file(local._defaults_file))
+ projects = {
+ for f in fileset("${local._data_dir}", "**/*.yaml") :
+ 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/"
+ _defaults_file = "${local._base_dir}/sample-data/defaults.yaml"
+}
+
+module "projects" {
+ source = "./fabric/blueprints/factories/project-factory"
+ for_each = local.projects
+ defaults = local.defaults
+ project_id = each.key
+ 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, [])
+ essential_contacts = try(each.value.essential_contacts, [])
+ folder_id = each.value.folder_id
+ group_iam = try(each.value.group_iam, {})
+ iam = try(each.value.iam, {})
+ kms_service_agents = try(each.value.kms, {})
+ labels = try(each.value.labels, {})
+ org_policies = try(each.value.org_policies, {})
+ prefix = each.value.prefix
+ service_accounts = try(each.value.service_accounts, {})
+ services = try(each.value.services, [])
+ service_identities_iam = try(each.value.service_identities_iam, {})
+ vpc = try(each.value.vpc, null)
+}
+# tftest modules=7 resources=29
+```
+
+### Projects configuration
+
+```yaml
+# ./data/defaults.yaml
+# The following applies as overrideable defaults for all projects
+# All attributes are required
+
+billing_account_id: 012345-67890A-BCDEF0
+billing_alert:
+ amount: 1000
+ thresholds:
+ current: [0.5, 0.8]
+ forecasted: [0.5, 0.8]
+ credit_treatment: INCLUDE_ALL_CREDITS
+environment_dns_zone: prod.gcp.example.com
+essential_contacts: []
+labels:
+ environment: production
+ department: legal
+ application: my-legal-bot
+notification_channels: []
+shared_vpc_self_link: https://www.googleapis.com/compute/v1/projects/project-example-host-project/global/networks/vpc-one
+vpc_host_project: project-example-host-project
+
+```
+
+```yaml
+# ./data/projects/project-example-one.yaml
+# One file per project - projects will be named after the filename
+
+# [opt] Billing account id - overrides default if set
+billing_account_id: 012345-67890A-BCDEF0
+
+# [opt] Billing alerts config - overrides default if set
+billing_alert:
+ amount: 10
+ thresholds:
+ current:
+ - 0.5
+ - 0.8
+ forecasted: []
+
+# [opt] DNS zones to be created as children of the environment_dns_zone defined in defaults
+dns_zones:
+ - lorem
+ - ipsum
+
+# [opt] Contacts for billing alerts and important notifications
+essential_contacts:
+ - team-a-contacts@example.com
+
+# Folder the project will be created as children of
+folder_id: folders/012345678901
+
+# [opt] Authoritative IAM bindings in group => [roles] format
+group_iam:
+ test-team-foobar@fast-lab-0.gcp-pso-italy.net:
+ - roles/compute.admin
+
+# [opt] Authoritative IAM bindings in role => [principals] format
+# Generally used to grant roles to service accounts external to the project
+iam:
+ roles/compute.admin:
+ - serviceAccount:service-account
+
+# [opt] Service robots and keys they will be assigned as cryptoKeyEncrypterDecrypter
+# in service => [keys] format
+kms_service_agents:
+ compute: [key1, key2]
+ storage: [key1, key2]
+
+# [opt] Labels for the project - merged with the ones defined in defaults
+labels:
+ environment: prod
+
+# [opt] Org policy overrides defined at project level
+org_policies:
+ constraints/compute.disableGuestAttributesAccess:
+ enforce: true
+ constraints/compute.trustedImageProjects:
+ allow:
+ values:
+ - projects/fast-dev-iac-core-0
+ constraints/compute.vmExternalIpAccess:
+ deny:
+ all: true
+
+# [opt] Service account to create for the project and their roles on the project
+# in name => [roles] format
+service_accounts:
+ another-service-account:
+ - roles/compute.admin
+ my-service-account:
+ - roles/compute.admin
+
+# [opt] IAM bindings on the service account resources.
+# in name => {role => [members]} format
+service_accounts_iam:
+ another-service-account:
+ - roles/iam.serviceAccountTokenCreator:
+ - group: app-team-1@example.com
+
+# [opt] APIs to enable on the project.
+services:
+ - storage.googleapis.com
+ - stackdriver.googleapis.com
+ - compute.googleapis.com
+
+# [opt] Roles to assign to the robots service accounts in robot => [roles] format
+services_iam:
+ compute:
+ - roles/storage.objectViewer
+
+ # [opt] VPC setup.
+ # If set enables the `compute.googleapis.com` service and configures
+ # service project attachment
+vpc:
+
+ # [opt] If set, enables the container API
+ gke_setup:
+
+ # Grants "roles/container.hostServiceAgentUser" to the container robot if set
+ enable_host_service_agent: false
+
+ # Grants "roles/compute.securityAdmin" to the container robot if set
+ enable_security_admin: true
+
+ # Host project the project will be service project of
+ host_project: fast-prod-net-spoke-0
+
+ # [opt] Subnets in the host project where principals will be granted networkUser
+ # in region/subnet-name => [principals]
+ subnets_iam:
+ europe-west1/prod-default-ew1:
+ - user:foobar@example.com
+ - serviceAccount:service-account1@my-project.iam.gserviceaccount.com
+```
+
+
+## Variables
+
+| 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
| ✓ | |
+| [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
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [project](outputs.tf#L19) | The project resource as return by the `project` module. | |
+| [project_id](outputs.tf#L29) | Project ID. | |
+
+
diff --git a/blueprints/factories/project-factory/main.tf b/blueprints/factories/project-factory/main.tf
new file mode 100644
index 0000000000..f6b2a797c6
--- /dev/null
+++ b/blueprints/factories/project-factory/main.tf
@@ -0,0 +1,229 @@
+/**
+ * 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 = {
+ for r in local._group_iam_bindings : r => [
+ for k, v in var.group_iam :
+ "group:${k}" if try(index(v, r), null) != null
+ ]
+ }
+ _group_iam_additive = {
+ for r in local._group_iam_additive_bindings : r => [
+ for k, v in var.group_iam_additive :
+ "group:${k}" if try(index(v, r), null) != null
+ ]
+ }
+ _group_iam_bindings = distinct(flatten(values(var.group_iam)))
+ _group_iam_additive_bindings = distinct(flatten(values(var.group_iam_additive)))
+
+ _service_accounts_iam = {
+ for r in local._service_accounts_iam_bindings : r => [
+ for k, v in var.service_accounts :
+ module.service-accounts[k].iam_email
+ if try(index(v, r), null) != null
+ ]
+ }
+ _service_accounts_iam_bindings = distinct(flatten(
+ values(var.service_accounts)
+ ))
+ _service_accounts_iam_additive = {
+ for r in local._service_accounts_iam_additive_bindings : r => [
+ for k, v in var.service_accounts_additive :
+ module.service-accounts[k].iam_email
+ if try(index(v, r), null) != null
+ ]
+ }
+ _service_accounts_iam_additive_bindings = distinct(flatten(
+ values(var.service_accounts_additive)
+ ))
+ _services = concat([
+ "billingbudgets.googleapis.com",
+ "essentialcontacts.googleapis.com",
+ "orgpolicy.googleapis.com",
+ ],
+ length(var.dns_zones) > 0 ? ["dns.googleapis.com"] : [],
+ try(var.vpc.gke_setup, null) != null ? ["container.googleapis.com"] : [],
+ var.vpc != null ? ["compute.googleapis.com"] : [],
+ )
+ _service_identities_roles = distinct(flatten(values(var.service_identities_iam)))
+ _service_identities_iam = {
+ for role in local._service_identities_roles : role => [
+ for service, roles in var.service_identities_iam :
+ "serviceAccount:${module.project.service_accounts.robots[service]}"
+ if contains(roles, role)
+ ]
+ }
+ _service_identities_roles_additive = distinct(flatten(values(var.service_identities_iam_additive)))
+ _service_identities_iam_additive = {
+ for role in local._service_identities_roles_additive : role => [
+ for service, roles in var.service_identities_iam_additive :
+ "serviceAccount:${module.project.service_accounts.robots[service]}"
+ if contains(roles, role)
+ ]
+ }
+ _vpc_subnet_bindings = (
+ local.vpc.subnets_iam == null || local.vpc.host_project == null
+ ? []
+ : flatten([
+ for subnet, members in local.vpc.subnets_iam : [
+ for member in members : {
+ region = split("/", subnet)[0]
+ subnet = split("/", subnet)[1]
+ member = member
+ }
+ ]
+ ])
+ )
+ billing_account_id = coalesce(
+ var.billing_account_id, try(var.defaults.billing_account_id, "")
+ )
+ billing_alert = (
+ var.billing_alert == null
+ ? try(var.defaults.billing_alert, null)
+ : var.billing_alert
+ )
+ essential_contacts = concat(
+ try(var.defaults.essential_contacts, []), var.essential_contacts
+ )
+ iam = {
+ for role in distinct(concat(
+ keys(var.iam),
+ keys(local._group_iam),
+ keys(local._service_accounts_iam),
+ keys(local._service_identities_iam),
+ )) :
+ role => concat(
+ try(var.iam[role], []),
+ try(local._group_iam[role], []),
+ try(local._service_accounts_iam[role], []),
+ try(local._service_identities_iam[role], []),
+ )
+ }
+ iam_additive = {
+ for role in distinct(concat(
+ keys(var.iam_additive),
+ keys(local._group_iam_additive),
+ keys(local._service_accounts_iam_additive),
+ keys(local._service_identities_iam_additive),
+ )) :
+ role => concat(
+ try(var.iam_additive[role], []),
+ try(local._group_iam_additive[role], []),
+ try(local._service_accounts_iam_additive[role], []),
+ try(local._service_identities_iam_additive[role], []),
+ )
+ }
+ labels = merge(
+ coalesce(var.labels, {}), coalesce(try(var.defaults.labels, {}), {})
+ )
+ services = distinct(concat(var.services, local._services))
+ vpc = coalesce(var.vpc, {
+ host_project = null, gke_setup = null, subnets_iam = null
+ })
+ vpc_cloudservices = (
+ local.vpc_gke_service_agent ||
+ contains(var.services, "compute.googleapis.com")
+ )
+ vpc_gke_security_admin = coalesce(
+ try(local.vpc.gke_setup.enable_security_admin, null), false
+ )
+ vpc_gke_service_agent = coalesce(
+ try(local.vpc.gke_setup.enable_host_service_agent, null), false
+ )
+ vpc_subnet_bindings = {
+ for binding in local._vpc_subnet_bindings :
+ "${binding.subnet}:${binding.member}" => binding
+ }
+}
+
+module "billing-alert" {
+ for_each = local.billing_alert == null ? {} : { 1 = 1 }
+ source = "../../../modules/billing-budget"
+ billing_account = local.billing_account_id
+ name = "${module.project.project_id} budget"
+ amount = local.billing_alert.amount
+ thresholds = local.billing_alert.thresholds
+ credit_treatment = local.billing_alert.credit_treatment
+ notification_channels = var.defaults.notification_channels
+ projects = ["projects/${module.project.number}"]
+ email_recipients = {
+ project_id = module.project.project_id
+ emails = local.essential_contacts
+ }
+}
+
+module "dns" {
+ source = "../../../modules/dns"
+ for_each = toset(var.dns_zones)
+ project_id = coalesce(local.vpc.host_project, module.project.project_id)
+ type = "private"
+ name = each.value
+ domain = "${each.value}.${var.defaults.environment_dns_zone}"
+ client_networks = [var.defaults.shared_vpc_self_link]
+}
+
+module "project" {
+ source = "../../../modules/project"
+ billing_account = local.billing_account_id
+ name = var.project_id
+ prefix = var.prefix
+ contacts = { for c in local.essential_contacts : c => ["ALL"] }
+ iam = local.iam
+ iam_additive = local.iam_additive
+ labels = local.labels
+ org_policies = try(var.org_policies, {})
+ parent = var.folder_id
+ service_encryption_key_ids = var.kms_service_agents
+ services = local.services
+ shared_vpc_service_config = var.vpc == null ? null : {
+ host_project = local.vpc.host_project
+ # these are non-authoritative
+ service_identity_iam = {
+ "roles/compute.networkUser" = compact([
+ local.vpc_gke_service_agent ? "container-engine" : null,
+ local.vpc_cloudservices ? "cloudservices" : null
+ ])
+ "roles/compute.securityAdmin" = compact([
+ local.vpc_gke_security_admin ? "container-engine" : null,
+ ])
+ "roles/container.hostServiceAgentUser" = compact([
+ local.vpc_gke_service_agent ? "container-engine" : null
+ ])
+ }
+ }
+}
+
+module "service-accounts" {
+ source = "../../../modules/iam-service-account"
+ for_each = var.service_accounts
+ name = each.key
+ project_id = module.project.project_id
+ iam = lookup(var.service_accounts_iam, each.key, null)
+}
+
+resource "google_compute_subnetwork_iam_member" "default" {
+ for_each = local.vpc_subnet_bindings
+ project = local.vpc.host_project
+ subnetwork = "projects/${local.vpc.host_project}/regions/${each.value.region}/subnetworks/${each.value.subnet}"
+ region = each.value.region
+ role = "roles/compute.networkUser"
+ member = (
+ lookup(var.service_accounts, each.value.member, null) != null
+ ? module.service-accounts[each.value.member].iam_email
+ : each.value.member
+ )
+}
diff --git a/blueprints/factories/project-factory/outputs.tf b/blueprints/factories/project-factory/outputs.tf
new file mode 100644
index 0000000000..a989eaba86
--- /dev/null
+++ b/blueprints/factories/project-factory/outputs.tf
@@ -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.
+ */
+
+# TODO(): proper outputs
+
+output "project" {
+ description = "The project resource as return by the `project` module."
+ value = module.project
+
+ depends_on = [
+ google_compute_subnetwork_iam_member.default,
+ module.dns
+ ]
+}
+
+output "project_id" {
+ description = "Project ID."
+ value = module.project.project_id
+ depends_on = [
+ google_compute_subnetwork_iam_member.default,
+ module.dns
+ ]
+}
diff --git a/blueprints/factories/project-factory/sample-data/defaults.yaml b/blueprints/factories/project-factory/sample-data/defaults.yaml
new file mode 100644
index 0000000000..72ed3f0d2a
--- /dev/null
+++ b/blueprints/factories/project-factory/sample-data/defaults.yaml
@@ -0,0 +1,29 @@
+# skip boilerplate check
+
+billing_account_id: 012345-67890A-BCDEF0
+
+# [opt] Setup for billing alerts
+billing_alert:
+ amount: 1000
+ thresholds:
+ current: [0.5, 0.8]
+ forecasted: [0.5, 0.8]
+ credit_treatment: INCLUDE_ALL_CREDITS
+
+environment_dns_zone: dev.example.org
+
+# [opt] Contacts for billing alerts and important notifications
+essential_contacts: ["team-contacts@example.com"]
+
+# [opt] Labels set for all projects
+labels:
+ environment: dev
+ department: accounting
+ application: example-app
+ foo: bar
+
+# [opt] Additional notification channels for billing
+notification_channels: []
+shared_vpc_self_link: projects/foo/networks/bar
+prefix: test
+vpc_host_project:
diff --git a/blueprints/factories/project-factory/sample-data/projects/project.yaml b/blueprints/factories/project-factory/sample-data/projects/project.yaml
new file mode 100644
index 0000000000..0344991380
--- /dev/null
+++ b/blueprints/factories/project-factory/sample-data/projects/project.yaml
@@ -0,0 +1,103 @@
+# skip boilerplate check
+
+# [opt] Billing account id - overrides default if set
+billing_account_id: 012345-67890A-BCDEF0
+
+# [opt] Billing alerts config - overrides default if set
+billing_alert:
+ amount: 10
+ thresholds:
+ current:
+ - 0.5
+ - 0.8
+ forecasted: []
+ credit_treatment: INCLUDE_ALL_CREDITS
+
+# [opt] DNS zones to be created as children of the environment_dns_zone defined in defaults
+dns_zones:
+ - lorem
+ - ipsum
+
+# [opt] Contacts for billing alerts and important notifications
+essential_contacts:
+ - team-a-contacts@example.com
+
+# Folder the project will be created as children of
+folder_id: folders/012345678901
+
+# [opt] Authoritative IAM bindings in group => [roles] format
+group_iam:
+ test-team-foobar@fast-lab-0.gcp-pso-italy.net:
+ - roles/compute.admin
+
+# [opt] Authoritative IAM bindings in role => [principals] format
+# Generally used to grant roles to service accounts external to the project
+iam:
+ roles/compute.admin:
+ - serviceAccount:service-account
+
+# [opt] Service robots and keys they will be assigned as cryptoKeyEncrypterDecrypter
+# in service => [keys] format
+kms_service_agents:
+ compute: [key1, key2]
+ storage: [key1, key2]
+
+# [opt] Labels for the project - merged with the ones defined in defaults
+labels:
+ environment: dev
+
+# [opt] Org policy overrides defined at project level
+org_policies:
+ constraints/compute.disableGuestAttributesAccess:
+ enforce: true
+ constraints/compute.trustedImageProjects:
+ allow:
+ values:
+ - projects/fast-dev-iac-core-0
+ constraints/compute.vmExternalIpAccess:
+ deny:
+ all: true
+
+# [opt] Prefix - overrides default if set
+prefix: test1
+
+# [opt] Service account to create for the project and their roles on the project
+# in name => [roles] format
+service_accounts:
+ another-service-account:
+ - roles/compute.admin
+ my-service-account:
+ - roles/compute.admin
+
+# [opt] APIs to enable on the project.
+services:
+ - storage.googleapis.com
+ - stackdriver.googleapis.com
+ - compute.googleapis.com
+
+# [opt] Roles to assign to the service identities in service => [roles] format
+service_identities_iam:
+ compute:
+ - roles/storage.objectViewer
+
+ # [opt] VPC setup.
+ # If set enables the `compute.googleapis.com` service and configures
+ # service project attachment
+vpc:
+ # [opt] If set, enables the container API
+ gke_setup:
+ # Grants "roles/container.hostServiceAgentUser" to the container robot if set
+ enable_host_service_agent: false
+
+ # Grants "roles/compute.securityAdmin" to the container robot if set
+ enable_security_admin: true
+
+ # Host project the project will be service project of
+ host_project: fast-dev-net-spoke-0
+
+ # [opt] Subnets in the host project where principals will be granted networkUser
+ # in region/subnet-name => [principals]
+ subnets_iam:
+ europe-west1/dev-default-ew1:
+ - user:foobar@example.com
+ - serviceAccount:service-account1
diff --git a/blueprints/factories/project-factory/variables.tf b/blueprints/factories/project-factory/variables.tf
new file mode 100644
index 0000000000..0ece0f0423
--- /dev/null
+++ b/blueprints/factories/project-factory/variables.tf
@@ -0,0 +1,223 @@
+/**
+ * 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_id" {
+ description = "Billing account id."
+ type = string
+}
+
+variable "billing_alert" {
+ description = "Billing alert configuration."
+ type = object({
+ amount = number
+ thresholds = object({
+ current = list(number)
+ forecasted = list(number)
+ })
+ credit_treatment = string
+ })
+ default = null
+}
+
+variable "defaults" {
+ description = "Project factory default values."
+ type = object({
+ billing_account_id = string
+ billing_alert = object({
+ amount = number
+ thresholds = object({
+ current = list(number)
+ forecasted = list(number)
+ })
+ credit_treatment = string
+ })
+ environment_dns_zone = string
+ essential_contacts = list(string)
+ labels = map(string)
+ notification_channels = list(string)
+ shared_vpc_self_link = string
+ vpc_host_project = string
+ })
+ default = null
+}
+
+variable "dns_zones" {
+ description = "DNS private zones to create as child of var.defaults.environment_dns_zone."
+ type = list(string)
+ default = []
+}
+
+variable "essential_contacts" {
+ description = "Email contacts to be used for billing and GCP notifications."
+ type = list(string)
+ default = []
+}
+
+variable "folder_id" {
+ description = "Folder ID for the folder where the project will be created."
+ type = string
+ default = null
+}
+
+variable "group_iam" {
+ description = "Custom IAM settings in group => [role] format."
+ type = map(list(string))
+ default = {}
+}
+
+variable "group_iam_additive" {
+ description = "Custom additive IAM settings in group => [role] format."
+ type = map(list(string))
+ default = {}
+}
+
+variable "iam" {
+ description = "Custom IAM settings in role => [principal] format."
+ type = map(list(string))
+ default = {}
+}
+
+variable "iam_additive" {
+ description = "Custom additive IAM settings in role => [principal] format."
+ type = map(list(string))
+ default = {}
+}
+
+variable "kms_service_agents" {
+ description = "KMS IAM configuration in as service => [key]."
+ type = map(list(string))
+ default = {}
+}
+
+variable "labels" {
+ description = "Labels to be assigned at project level."
+ type = map(string)
+ default = {}
+}
+
+variable "org_policies" {
+ description = "Org-policy overrides at project level."
+ type = map(object({
+ inherit_from_parent = optional(bool) # for list policies only.
+ reset = optional(bool)
+
+ # default (unconditional) values
+ allow = optional(object({
+ all = optional(bool)
+ values = optional(list(string))
+ }))
+ deny = optional(object({
+ all = optional(bool)
+ values = optional(list(string))
+ }))
+ enforce = optional(bool, true) # for boolean policies only.
+
+ # conditional values
+ rules = optional(list(object({
+ allow = optional(object({
+ all = optional(bool)
+ values = optional(list(string))
+ }))
+ deny = optional(object({
+ all = optional(bool)
+ values = optional(list(string))
+ }))
+ enforce = optional(bool, true) # for boolean policies only.
+ condition = object({
+ description = optional(string)
+ expression = optional(string)
+ location = optional(string)
+ title = optional(string)
+ })
+ })), [])
+ }))
+ default = {}
+ nullable = false
+}
+
+variable "prefix" {
+ description = "Prefix used for resource names."
+ type = string
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty."
+ }
+}
+
+variable "project_id" {
+ description = "Project id."
+ type = string
+}
+
+variable "service_accounts" {
+ description = "Service accounts to be created, and roles assigned them on the project."
+ type = map(list(string))
+ default = {}
+}
+
+variable "service_accounts_additive" {
+ description = "Service accounts to be created, and roles assigned them on the project additively."
+ type = map(list(string))
+ default = {}
+}
+
+variable "service_accounts_iam" {
+ description = "IAM bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}."
+ type = map(map(list(string)))
+ default = {}
+ nullable = false
+}
+
+variable "service_accounts_iam_additive" {
+ description = "IAM additive bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}."
+ type = map(map(list(string)))
+ default = {}
+ nullable = false
+}
+
+variable "service_identities_iam" {
+ description = "Custom IAM settings for service identities in service => [role] format."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "service_identities_iam_additive" {
+ description = "Custom additive IAM settings for service identities in service => [role] format."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "services" {
+ description = "Services to be enabled for the project."
+ type = list(string)
+ default = []
+ nullable = false
+}
+
+variable "vpc" {
+ description = "VPC configuration for the project."
+ type = object({
+ host_project = string
+ gke_setup = object({
+ enable_security_admin = bool
+ enable_host_service_agent = bool
+ })
+ subnets_iam = map(list(string))
+ })
+ default = null
+}
diff --git a/blueprints/gke/README.md b/blueprints/gke/README.md
new file mode 100644
index 0000000000..30418ca419
--- /dev/null
+++ b/blueprints/gke/README.md
@@ -0,0 +1,32 @@
+# GKE blueprints
+
+The blueprints in this folder show implement **end-to-end scenarios** for GKE topologies that show how to automate common configurations or leverage specific products.
+
+They are meant to be used as minimal but complete starting points to create actual infrastructure, and as playgrounds to experiment with Google Cloud features.
+
+## Blueprints
+
+### Binary Authorization Pipeline
+
+ This [blueprint](../gke/binauthz/) shows how to create a CI and a CD pipeline in Cloud Build for the deployment of an application to a private GKE cluster with unrestricted access to a public endpoint. The blueprint enables a Binary Authorization policy in the project so only images that have been attested can be deployed to the cluster. The attestations are created using a cryptographic key pair that has been provisioned in KMS.
+
+string
| ✓ | |
+| [project_id](variables.tf#L47) | Project ID. | string
| ✓ | |
+| [master_cidr_block](variables.tf#L17) | Master CIDR block. | string
| | "10.0.0.0/28"
|
+| [pods_cidr_block](variables.tf#L23) | Pods CIDR block. | string
| | "172.16.0.0/20"
|
+| [project_create](variables.tf#L38) | Parameters for the creation of the new project. | object({…})
| | null
|
+| [region](variables.tf#L52) | Region. | string
| | "europe-west1"
|
+| [services_cidr_block](variables.tf#L58) | Services CIDR block. | string
| | "192.168.0.0/24"
|
+| [subnet_cidr_block](variables.tf#L64) | Subnet CIDR block. | string
| | "10.0.1.0/24"
|
+| [zone](variables.tf#L70) | Zone. | string
| | "europe-west1-c"
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [app_repo_url](outputs.tf#L17) | App source repository url. | |
+| [image_repo_url](outputs.tf#L22) | Image source repository url. | |
+
+
diff --git a/blueprints/gke/binauthz/app/clobuild.yaml b/blueprints/gke/binauthz/app/clobuild.yaml
new file mode 100644
index 0000000000..6477ecd7d7
--- /dev/null
+++ b/blueprints/gke/binauthz/app/clobuild.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
+#
+# 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.
+
+steps:
+ - id: 'Deploy app'
+ name: 'gcr.io/cloud-builders/kubectl'
+ args:
+ - 'apply'
+ - '-f'
+ - 'app.yaml'
+ env:
+ - 'CLOUDSDK_COMPUTE_ZONE=${_ZONE}'
+ - 'CLOUDSDK_CONTAINER_CLUSTER=${_CLUSTER}'
+options:
+ logging: CLOUD_LOGGING_ONLY
diff --git a/blueprints/gke/binauthz/diagram.png b/blueprints/gke/binauthz/diagram.png
new file mode 100644
index 0000000000..0ee786f032
Binary files /dev/null and b/blueprints/gke/binauthz/diagram.png differ
diff --git a/blueprints/gke/binauthz/image/.dockerignore b/blueprints/gke/binauthz/image/.dockerignore
new file mode 100644
index 0000000000..b512c09d47
--- /dev/null
+++ b/blueprints/gke/binauthz/image/.dockerignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/blueprints/gke/binauthz/image/.gitignore b/blueprints/gke/binauthz/image/.gitignore
new file mode 100644
index 0000000000..a8603104af
--- /dev/null
+++ b/blueprints/gke/binauthz/image/.gitignore
@@ -0,0 +1 @@
+node_modules/**
diff --git a/blueprints/gke/binauthz/image/Dockerfile b/blueprints/gke/binauthz/image/Dockerfile
new file mode 100644
index 0000000000..03c3e436c4
--- /dev/null
+++ b/blueprints/gke/binauthz/image/Dockerfile
@@ -0,0 +1,25 @@
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 node:18-alpine
+
+WORKDIR /app
+
+COPY ["package.json", "package-lock.json*", "./"]
+
+RUN npm install
+
+COPY . .
+
+CMD [ "node", "index.js" ]
\ No newline at end of file
diff --git a/blueprints/gke/binauthz/image/README.md b/blueprints/gke/binauthz/image/README.md
new file mode 100644
index 0000000000..88481cc829
--- /dev/null
+++ b/blueprints/gke/binauthz/image/README.md
@@ -0,0 +1,27 @@
+# Storage API
+
+This application it is a RESTful API that let's you manage the Google Cloud Storage buckets available is a project. In order to do so the application needs to authenticate with a service account that has been granted the Storage Admin (`roles/storage.admin`) role.
+
+Find below the operations that can be performed using it:
+
+* Get buckets in project
+
+ curl -v http://localhost:3000/buckets
+
+* Get files in bucket
+
+ curl -v http://localhost:3000/buckets/BUCKET_NAME
+
+* Create a bucket
+
+ curl -v http://localhost:3000/buckets \
+ -H'Content-Type: application/json' \
+ -d @- <local_file
|
+| [gke.tf](./gke.tf) | GKE cluster and hub resources. | gke-cluster
· gke-hub
· gke-nodepool
| |
+| [main.tf](./main.tf) | Project resources. | project
| |
+| [variables.tf](./variables.tf) | Module variables. | | |
+| [vm.tf](./vm.tf) | Management server. | compute-vm
| |
+| [vpc.tf](./vpc.tf) | Networking resources. | net-cloudnat
· net-vpc
· net-vpc-firewall
| |
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [billing_account_id](variables.tf#L17) | Billing account id. | string
| ✓ | |
+| [fleet_project_id](variables.tf#L46) | Management Project ID. | string
| ✓ | |
+| [host_project_id](variables.tf#L51) | Project ID. | string
| ✓ | |
+| [mgmt_project_id](variables.tf#L63) | Management Project ID. | string
| ✓ | |
+| [parent](variables.tf#L94) | Parent. | string
| ✓ | |
+| [clusters_config](variables.tf#L22) | Clusters configuration. | map(object({…}))
| | {…}
|
+| [istio_version](variables.tf#L57) | ASM version. | string
| | "1.14.1-asm.3"
|
+| [mgmt_server_config](variables.tf#L68) | Mgmt server configuration. | object({…})
| | {…}
|
+| [mgmt_subnet_cidr_block](variables.tf#L88) | Management subnet CIDR block. | string
| | "10.0.0.0/28"
|
+| [region](variables.tf#L99) | Region. | string
| | "europe-west1"
|
+
+
diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible.tf b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible.tf
new file mode 100644
index 0000000000..b239bf0941
--- /dev/null
+++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible.tf
@@ -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.
+ */
+
+# tfdoc:file:description Ansible generated files.
+
+resource "local_file" "vars_file" {
+ content = templatefile("${path.module}/templates/vars.yaml.tpl", {
+ istio_version = var.istio_version
+ region = var.region
+ clusters = keys(var.clusters_config)
+ service_account_email = module.mgmt_server.service_account_email
+ project_id = module.fleet_project.project_id
+ })
+ 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 = var.mgmt_project_id
+ zone = var.mgmt_server_config.zone
+ })
+ filename = "${path.module}/ansible/gssh.sh"
+ file_permission = "0777"
+}
diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/ansible.cfg b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/ansible.cfg
new file mode 100644
index 0000000000..4558c2b6b1
--- /dev/null
+++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/ansible.cfg
@@ -0,0 +1,9 @@
+[defaults]
+inventory = inventory/hosts.ini
+timeout = 900
+
+[ssh_connection]
+pipelining = True
+ssh_executable = ./gssh.sh
+transfer_method = piped
+
diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/inventory/hosts.ini b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/inventory/hosts.ini
new file mode 100644
index 0000000000..842da83f43
--- /dev/null
+++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/inventory/hosts.ini
@@ -0,0 +1 @@
+mgmt
\ No newline at end of file
diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/playbook.yaml b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/playbook.yaml
new file mode 100644
index 0000000000..30114d22c8
--- /dev/null
+++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/playbook.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.
+
+- 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: install
+ - role: test
+
diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/endpoint-discovery-config.yaml b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/endpoint-discovery-config.yaml
new file mode 100644
index 0000000000..99abc8923d
--- /dev/null
+++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/endpoint-discovery-config.yaml
@@ -0,0 +1,21 @@
+# 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: Configure endpoint discovery
+ shell: >
+ kubectl apply \
+ -f ~/{{ item }}.secret \
+ --context "gke_{{ project_id }}_{{ region }}_{{ cluster }}"
+ with_items: "{{ clusters }}"
+ when: cluster != item
\ No newline at end of file
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
new file mode 100644
index 0000000000..b81c49622a
--- /dev/null
+++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/install.yaml
@@ -0,0 +1,58 @@
+# 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: Get cluster credentials
+ shell: >
+ gcloud container clusters get-credentials {{ cluster }} \
+ --region {{ region }} \
+ --project {{ project_id }} \
+ --internal-ip
+
+- name: Set context
+ 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 }}" \
+ --project "{{ project_id }}" \
+ --region "{{ region }}" \
+ --format "value(privateClusterConfig.publicEndpoint)"
+ register: endpoint
+
+- name: Create secret
+ shell: >
+ ~/istio-*/bin/istioctl x create-remote-secret \
+ --context={{ context }} \
+ --name={{ cluster }} \
+ --server=https://{{ endpoint.stdout }} > ~/{{ cluster }}.secret
\ No newline at end of file
diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/main.yaml b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/main.yaml
new file mode 100644
index 0000000000..9e181bfbf7
--- /dev/null
+++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/install/tasks/main.yaml
@@ -0,0 +1,39 @@
+
+# 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 istio bundle
+ get_url:
+ url: https://storage.googleapis.com/gke-release/asm/istio-{{ istio_version }}-linux-amd64.tar.gz
+ dest: ~/istio.tar.gz
+
+- name: Unarchive istio bundle
+ unarchive:
+ src: ~/istio.tar.gz
+ dest: ~/
+ remote_src: yes
+
+- name: Install
+ include_tasks: install.yaml
+ vars:
+ cluster: "{{ item }}"
+ with_items: "{{ clusters }}"
+
+- name: Configure endpoint discovery
+ include_tasks: endpoint-discovery-config.yaml
+ vars:
+ cluster: "{{ outer_item }}"
+ with_items: "{{ clusters }}"
+ loop_control:
+ loop_var: outer_item
\ No newline at end of file
diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/prerequisites/tasks/main.yaml b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/prerequisites/tasks/main.yaml
new file mode 100644
index 0000000000..8b889230ed
--- /dev/null
+++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/prerequisites/tasks/main.yaml
@@ -0,0 +1,38 @@
+# 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
+ - jq
+ 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/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/test/tasks/main.yaml b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/test/tasks/main.yaml
new file mode 100644
index 0000000000..46a06c16e0
--- /dev/null
+++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/test/tasks/main.yaml
@@ -0,0 +1,36 @@
+# 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: Deploy test app
+ include_tasks: test.yaml
+ vars:
+ cluster: "{{ item }}"
+ with_items: "{{ clusters }}"
+ loop_control:
+ index_var: index
+
+- name: Test
+ shell: >
+ for i in $(seq 400); do
+ kubectl exec \
+ --context="gke_{{ project_id }}_{{ region }}_{{ item }}" \
+ -n sample \
+ -c sleep "$(kubectl get pod \
+ --context="gke_{{ project_id }}_{{ region }}_{{ item }}" \
+ -n sample \
+ -l app=sleep \
+ -o jsonpath='{.items[0].metadata.name}')" \
+ -- curl -sS helloworld.sample:5000/hello
+ done
+ with_items: "{{ clusters }}"
diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/test/tasks/test.yaml b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/test/tasks/test.yaml
new file mode 100644
index 0000000000..2936907473
--- /dev/null
+++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/ansible/roles/test/tasks/test.yaml
@@ -0,0 +1,63 @@
+# 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: Set context
+ set_fact:
+ context: "gke_{{ project_id }}_{{ region }}_{{ cluster }}"
+
+- name: Create sample namespace
+ shell:
+ cmd: |
+ cat << EOF | kubectl apply --context {{ context }} -f -
+ apiVersion: v1
+ kind: Namespace
+ metadata:
+ name: sample
+ EOF
+
+- name: Label the sample namespace for istio sidecar injection
+ shell: >
+ kubectl label namespace sample \
+ istio-injection- istio.io/rev=asm-managed \
+ --context {{ context }} \
+ --overwrite
+
+- name: Create helloworld service
+ shell: >
+ kubectl apply
+ -f samples/helloworld/helloworld.yaml \
+ -l service=helloworld \
+ -n sample \
+ --context {{ context }}
+ args:
+ chdir: ~/istio-{{ istio_version }}
+
+- name: Create helloworld deployment
+ shell: >
+ kubectl apply
+ -f samples/helloworld/helloworld.yaml \
+ -l version=v{{ index + 1 }} \
+ -n sample \
+ --context {{ context }}
+ args:
+ chdir: ~/istio-{{ istio_version }}
+
+- name: Create sleep service and deployment
+ shell: >
+ kubectl apply \
+ -f samples/sleep/sleep.yaml \
+ -n sample \
+ --context {{ context }}
+ args:
+ chdir: ~/istio-{{ istio_version }}
\ No newline at end of file
diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/diagram.png b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/diagram.png
new file mode 100644
index 0000000000..0838570f4a
Binary files /dev/null and b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/diagram.png differ
diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/gke.tf b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/gke.tf
new file mode 100644
index 0000000000..6c769d9201
--- /dev/null
+++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/gke.tf
@@ -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.
+ */
+
+# tfdoc:file:description GKE cluster and hub resources.
+
+module "clusters" {
+ for_each = var.clusters_config
+ source = "../../../modules/gke-cluster"
+ project_id = module.fleet_project.project_id
+ name = each.key
+ location = var.region
+ vpc_config = {
+ network = module.svpc.self_link
+ subnetwork = module.svpc.subnet_self_links["${var.region}/subnet-${each.key}"]
+ master_authorized_ranges = merge({
+ mgmt : var.mgmt_subnet_cidr_block
+ },
+ { for key, config in var.clusters_config :
+ "pods-${key}" => config.pods_cidr_block if key != each.key
+ })
+ master_ipv4_cidr_block = each.value.master_cidr_block
+ }
+ private_cluster_config = {
+ enable_private_endpoint = true
+ master_global_access = true
+ }
+ release_channel = "REGULAR"
+ labels = {
+ mesh_id = "proj-${module.fleet_project.number}"
+ }
+}
+
+module "cluster_nodepools" {
+ for_each = var.clusters_config
+ source = "../../../modules/gke-nodepool"
+ project_id = module.fleet_project.project_id
+ cluster_name = module.clusters[each.key].name
+ location = var.region
+ name = "nodepool-${each.key}"
+ node_count = { initial = 1 }
+ service_account = {
+ create = true
+ }
+ tags = ["${each.key}-node"]
+}
+
+module "hub" {
+ source = "../../../modules/gke-hub"
+ project_id = module.fleet_project.project_id
+ clusters = { for k, v in module.clusters : k => v.id }
+ features = {
+ appdevexperience = false
+ configmanagement = false
+ identityservice = false
+ multiclusteringress = null
+ servicemesh = true
+ multiclusterservicediscovery = false
+ }
+ depends_on = [
+ module.fleet_project
+ ]
+}
diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/main.tf b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/main.tf
new file mode 100644
index 0000000000..663cf8c239
--- /dev/null
+++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/main.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.
+ */
+
+# tfdoc:file:description Project resources.
+
+locals {
+ np_service_account_iam_email = [
+ for k, v in module.cluster_nodepools : v.service_account_iam_email
+ ]
+}
+
+module "host_project" {
+ source = "../../../modules/project"
+ billing_account = var.billing_account_id
+ parent = var.parent
+ name = var.host_project_id
+ shared_vpc_host_config = {
+ enabled = true
+ }
+ services = [
+ "container.googleapis.com"
+ ]
+}
+
+module "mgmt_project" {
+ source = "../../../modules/project"
+ billing_account = var.billing_account_id
+ parent = var.parent
+ name = var.mgmt_project_id
+ shared_vpc_service_config = {
+ attach = true
+ host_project = module.host_project.project_id
+ service_identity_iam = null
+ }
+ services = [
+ "cloudresourcemanager.googleapis.com",
+ "container.googleapis.com",
+ "serviceusage.googleapis.com"
+ ]
+}
+
+module "fleet_project" {
+ source = "../../../modules/project"
+ billing_account = var.billing_account_id
+ parent = var.parent
+ name = var.fleet_project_id
+ shared_vpc_service_config = {
+ attach = true
+ host_project = module.host_project.project_id
+ service_identity_iam = {
+ "roles/compute.networkUser" = [
+ "cloudservices", "container-engine"
+ ]
+ "roles/container.hostServiceAgentUser" = [
+ "container-engine"
+ ]
+ }
+ }
+ services = [
+ "anthos.googleapis.com",
+ "cloudresourcemanager.googleapis.com",
+ "container.googleapis.com",
+ "gkehub.googleapis.com",
+ "gkeconnect.googleapis.com",
+ "logging.googleapis.com",
+ "mesh.googleapis.com",
+ "monitoring.googleapis.com",
+ "stackdriver.googleapis.com"
+ ]
+ iam = {
+ "roles/container.admin" = [module.mgmt_server.service_account_iam_email]
+ "roles/gkehub.admin" = [module.mgmt_server.service_account_iam_email]
+ "roles/gkehub.serviceAgent" = ["serviceAccount:${module.fleet_project.service_accounts.robots.fleet}"]
+ "roles/monitoring.viewer" = local.np_service_account_iam_email
+ "roles/monitoring.metricWriter" = local.np_service_account_iam_email
+ "roles/logging.logWriter" = local.np_service_account_iam_email
+ "roles/stackdriver.resourceMetadata.writer" = local.np_service_account_iam_email
+ }
+ service_config = {
+ disable_on_destroy = false
+ disable_dependent_services = true
+ }
+}
diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/templates/gssh.sh.tpl b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/templates/gssh.sh.tpl
new file mode 100644
index 0000000000..c61460ba2d
--- /dev/null
+++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/templates/gssh.sh.tpl
@@ -0,0 +1,30 @@
+#!/bin/bash
+#
+# 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.
+
+host="$${@: -2: 1}"
+cmd="$${@: -1: 1}"
+
+gcloud_args="
+--tunnel-through-iap
+--zone=${zone}
+--project=${project_id}
+--quiet
+--no-user-output-enabled
+--
+-C
+"
+
+exec gcloud compute ssh "$host" $gcloud_args "$cmd"
\ No newline at end of file
diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/templates/vars.yaml.tpl b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/templates/vars.yaml.tpl
new file mode 100644
index 0000000000..f31b4fd752
--- /dev/null
+++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/templates/vars.yaml.tpl
@@ -0,0 +1,8 @@
+istio_version: ${istio_version}
+clusters:
+%{ for cluster in clusters ~}
+ - ${cluster}
+%{ endfor ~}
+region: ${region}
+service_account_email: ${service_account_email}
+project_id: ${project_id}
\ No newline at end of file
diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/variables.tf b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/variables.tf
new file mode 100644
index 0000000000..428778f264
--- /dev/null
+++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/variables.tf
@@ -0,0 +1,103 @@
+/**
+ * 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_id" {
+ description = "Billing account id."
+ type = string
+}
+
+variable "clusters_config" {
+ description = "Clusters configuration."
+ type = map(object({
+ subnet_cidr_block = string
+ master_cidr_block = string
+ services_cidr_block = string
+ pods_cidr_block = string
+ }))
+ default = {
+ cluster-a = {
+ subnet_cidr_block = "10.0.1.0/24"
+ master_cidr_block = "10.16.0.0/28"
+ services_cidr_block = "192.168.1.0/24"
+ pods_cidr_block = "172.16.0.0/20"
+ }
+ cluster-b = {
+ subnet_cidr_block = "10.0.2.0/24"
+ master_cidr_block = "10.16.0.16/28"
+ services_cidr_block = "192.168.2.0/24"
+ pods_cidr_block = "172.16.16.0/20"
+ }
+ }
+}
+
+variable "fleet_project_id" {
+ description = "Management Project ID."
+ type = string
+}
+
+variable "host_project_id" {
+ description = "Project ID."
+ type = string
+}
+
+
+variable "istio_version" {
+ description = "ASM version."
+ type = string
+ default = "1.14.1-asm.3"
+}
+
+variable "mgmt_project_id" {
+ description = "Management Project ID."
+ type = string
+}
+
+variable "mgmt_server_config" {
+ description = "Mgmt server configuration."
+ type = object({
+ disk_size = number
+ disk_type = string
+ image = string
+ instance_type = string
+ region = string
+ zone = string
+ })
+ default = {
+ disk_size = 50
+ disk_type = "pd-ssd"
+ image = "projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts"
+ instance_type = "n1-standard-2"
+ region = "europe-west1"
+ zone = "europe-west1-c"
+ }
+}
+
+variable "mgmt_subnet_cidr_block" {
+ description = "Management subnet CIDR block."
+ type = string
+ default = "10.0.0.0/28"
+}
+
+variable "parent" {
+ description = "Parent."
+ type = string
+}
+
+variable "region" {
+ description = "Region."
+ type = string
+ default = "europe-west1"
+}
diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/vm.tf b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/vm.tf
new file mode 100644
index 0000000000..0a53ee5152
--- /dev/null
+++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/vm.tf
@@ -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.
+ */
+
+# tfdoc:file:description Management server.
+
+module "mgmt_server" {
+ source = "../../../modules/compute-vm"
+ project_id = module.mgmt_project.project_id
+ zone = var.mgmt_server_config.zone
+ name = "mgmt"
+ instance_type = var.mgmt_server_config.instance_type
+ network_interfaces = [{
+ network = module.svpc.self_link
+ subnetwork = module.svpc.subnet_self_links["${var.mgmt_server_config.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
+ }
+}
+
diff --git a/blueprints/gke/multi-cluster-mesh-gke-fleet-api/vpc.tf b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/vpc.tf
new file mode 100644
index 0000000000..604797be7b
--- /dev/null
+++ b/blueprints/gke/multi-cluster-mesh-gke-fleet-api/vpc.tf
@@ -0,0 +1,86 @@
+/**
+ * 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:file:description Networking resources.
+
+module "svpc" {
+ source = "../../../modules/net-vpc"
+ project_id = module.host_project.project_id
+ name = "svpc"
+ mtu = 1500
+ subnets = concat([for key, config in var.clusters_config : {
+ ip_cidr_range = config.subnet_cidr_block
+ name = "subnet-${key}"
+ region = var.region
+ secondary_ip_ranges = {
+ pods = config.pods_cidr_block
+ services = config.services_cidr_block
+ }
+ }], [{
+ ip_cidr_range = var.mgmt_subnet_cidr_block
+ name = "subnet-mgmt"
+ region = var.mgmt_server_config.region
+ }])
+}
+
+module "firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.host_project.project_id
+ network = module.svpc.name
+ ingress_rules = merge(
+ {
+ allow-mesh = {
+ description = "Allow mesh."
+ priority = 900
+ source_ranges = [
+ for k, v in var.clusters_config : v.pods_cidr_block
+ ]
+ targets = [
+ for k, v in var.clusters_config : "${k}-node"
+ ]
+ rules = [
+ { protocol = "tcp" },
+ { protocol = "udp" },
+ { protocol = "icmp" },
+ { protocol = "esp" },
+ { protocol = "ah" },
+ { protocol = "sctp" }
+ ]
+ }
+ },
+ {
+ for k, v in var.clusters_config : "allow-${k}-istio" => {
+ description = "Allow istio."
+ source_ranges = [v.master_cidr_block]
+ targets = ["${k}-node"]
+ rules = [{
+ protocol = "tcp"
+ ports = [8080, 15014, 15017]
+ }]
+ }
+ }
+ )
+}
+
+module "nat" {
+ source = "../../../modules/net-cloudnat"
+ project_id = module.host_project.project_id
+ region = var.region
+ name = "nat"
+ router_create = true
+ router_network = module.svpc.name
+}
+
diff --git a/blueprints/gke/multitenant-fleet/README.md b/blueprints/gke/multitenant-fleet/README.md
new file mode 100644
index 0000000000..52f26ceddc
--- /dev/null
+++ b/blueprints/gke/multitenant-fleet/README.md
@@ -0,0 +1,272 @@
+# GKE Multitenant Blueprint
+
+This blueprint presents an opinionated architecture to handle multiple homogeneous GKE clusters. The general idea behind this blueprint is to deploy a single project hosting multiple clusters leveraging several useful GKE features.
+
+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.
+
++ +
+ +The overall architecture is based on the following design decisions: + +- All clusters are assumed to be [private](https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters), therefore only [VPC-native clusters](https://cloud.google.com/kubernetes-engine/docs/concepts/alias-ips) are supported. +- Logging and monitoring configured to use Cloud Operations for system components and user workloads. +- [GKE metering](https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-usage-metering) enabled by default and stored in a bigquery dataset created within the project. +- Optional [GKE Fleet](https://cloud.google.com/kubernetes-engine/docs/fleets-overview) support with the possibility to enable any of the following features: + - [Fleet workload identity](https://cloud.google.com/anthos/fleet-management/docs/use-workload-identity) + - [Anthos Config Management](https://cloud.google.com/anthos-config-management/docs/overview) + - [Anthos Service Mesh](https://cloud.google.com/service-mesh/docs/overview) + - [Anthos Identity Service](https://cloud.google.com/anthos/identity/setup/fleet) + - [Multi-cluster services](https://cloud.google.com/kubernetes-engine/docs/concepts/multi-cluster-services) + - [Multi-cluster ingress](https://cloud.google.com/kubernetes-engine/docs/concepts/multi-cluster-ingress). +- Support for [Config Sync](https://cloud.google.com/anthos-config-management/docs/config-sync-overview), [Hierarchy Controller](https://cloud.google.com/anthos-config-management/docs/concepts/hierarchy-controller), and [Policy Controller](https://cloud.google.com/anthos-config-management/docs/concepts/policy-controller) when using Anthos Config Management. +- [Groups for GKE](https://cloud.google.com/kubernetes-engine/docs/how-to/google-groups-rbac) can be enabled to facilitate the creation of flexible RBAC policies referencing group principals. +- Support for [application layer secret encryption](https://cloud.google.com/kubernetes-engine/docs/how-to/encrypting-secrets). +- Support to customize peering configuration of the control plane VPC (e.g. to import/export routes to the peered network) +- Some features are enabled by default in all clusters: + - [Intranode visibility](https://cloud.google.com/kubernetes-engine/docs/how-to/intranode-visibility) + - [Dataplane v2](https://cloud.google.com/kubernetes-engine/docs/concepts/dataplane-v2) + - [Shielded GKE nodes](https://cloud.google.com/kubernetes-engine/docs/how-to/shielded-gke-nodes) + - [Workload identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) + - [Node local DNS cache](https://cloud.google.com/kubernetes-engine/docs/how-to/nodelocal-dns-cache) + - [Use of the GCE persistent disk CSI driver](https://cloud.google.com/kubernetes-engine/docs/how-to/persistent-volumes/gce-pd-csi-driver) + - Node [auto-upgrade](https://cloud.google.com/kubernetes-engine/docs/how-to/node-auto-upgrades) and [auto-repair](https://cloud.google.com/kubernetes-engine/docs/how-to/node-auto-repair) for all node pools + + + +## Basic usage + +The following example shows how to deploy two clusters and one node pool for each + +```hcl +locals { + cluster_defaults = { + private_cluster_config = { + enable_private_endpoint = true + master_global_access = true + } + } + subnet_self_links = { + ew1 = "projects/prj-host/regions/europe-west1/subnetworks/gke-0" + ew3 = "projects/prj-host/regions/europe-west3/subnetworks/gke-0" + } +} + +module "gke-fleet" { + source = "./fabric/blueprints/gke/multitenant-fleet/" + project_id = var.project_id + billing_account_id = var.billing_account_id + folder_id = var.folder_id + prefix = "myprefix" + group_iam = { + "gke-admin@example.com" = [ + "roles/container.admin" + ] + } + iam = { + "roles/container.clusterAdmin" = [ + "cicd@my-cicd-project.iam.gserviceaccount.com" + ] + } + clusters = { + cluster-0 = { + location = "europe-west1" + private_cluster_config = local.cluster_defaults.private_cluster_config + vpc_config = { + subnetwork = local.subnet_self_links.ew1 + master_ipv4_cidr_block = "172.16.10.0/28" + } + } + cluster-1 = { + location = "europe-west3" + private_cluster_config = local.cluster_defaults.private_cluster_config + vpc_config = { + subnetwork = local.subnet_self_links.ew3 + master_ipv4_cidr_block = "172.16.20.0/28" + } + } + } + nodepools = { + cluster-0 = { + nodepool-0 = { + node_config = { + disk_type = "pd-balanced" + machine_type = "n2-standard-4" + spot = true + } + } + } + cluster-1 = { + nodepool-0 = { + node_config = { + disk_type = "pd-balanced" + machine_type = "n2-standard-4" + } + } + } + } + vpc_config = { + host_project_id = "my-host-project-id" + vpc_self_link = "projects/prj-host/global/networks/prod-0" + } +} +# tftest modules=7 resources=26 +``` + +## GKE Fleet + +This example deploys two clusters and configures several GKE Fleet features: + +- Enables [multi-cluster ingress](https://cloud.google.com/kubernetes-engine/docs/concepts/multi-cluster-ingress) and sets the configuration cluster to be `cluster-eu1`. +- Enables [Multi-cluster services](https://cloud.google.com/kubernetes-engine/docs/concepts/multi-cluster-services) and assigns the [required roles](https://cloud.google.com/kubernetes-engine/docs/how-to/multi-cluster-services#authenticating) to its service accounts. +- A `default` Config Management template is created with binary authorization, config sync enabled with a git repository, hierarchy controller, and policy controller. +- The two clusters are configured to use the `default` Config Management template. + +```hcl +locals { + subnet_self_links = { + ew1 = "projects/prj-host/regions/europe-west1/subnetworks/gke-0" + ew3 = "projects/prj-host/regions/europe-west3/subnetworks/gke-0" + } +} + +module "gke" { + source = "./fabric/blueprints/gke/multitenant-fleet/" + project_id = var.project_id + billing_account_id = var.billing_account_id + folder_id = var.folder_id + prefix = "myprefix" + clusters = { + cluster-0 = { + location = "europe-west1" + vpc_config = { + subnetwork = local.subnet_self_links.ew1 + } + } + cluster-1 = { + location = "europe-west3" + vpc_config = { + subnetwork = local.subnet_self_links.ew3 + } + } + } + nodepools = { + cluster-0 = { + nodepool-0 = { + node_config = { + disk_type = "pd-balanced" + machine_type = "n2-standard-4" + spot = true + } + } + } + cluster-1 = { + nodepool-0 = { + node_config = { + disk_type = "pd-balanced" + machine_type = "n2-standard-4" + } + } + } + } + fleet_features = { + appdevexperience = false + configmanagement = true + identityservice = true + multiclusteringress = "cluster-0" + multiclusterservicediscovery = true + servicemesh = true + } + fleet_workload_identity = true + fleet_configmanagement_templates = { + default = { + binauthz = true + config_sync = { + git = { + gcp_service_account_email = null + https_proxy = null + policy_dir = "configsync" + secret_type = "none" + source_format = "hierarchy" + sync_branch = "main" + sync_repo = "https://github.com/myorg/myrepo" + sync_rev = null + sync_wait_secs = null + } + prevent_drift = true + source_format = "hierarchy" + } + hierarchy_controller = { + enable_hierarchical_resource_quota = true + enable_pod_tree_labels = true + } + 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" + } + } + fleet_configmanagement_clusters = { + default = ["cluster-0", "cluster-1"] + } + vpc_config = { + host_project_id = "my-host-project-id" + vpc_self_link = "projects/prj-host/global/networks/prod-0" + } +} + +# tftest modules=8 resources=35 +``` + + + + +## Files + +| name | description | modules | +|---|---|---| +| [gke-clusters.tf](./gke-clusters.tf) | GKE clusters. |gke-cluster
|
+| [gke-hub.tf](./gke-hub.tf) | GKE hub configuration. | gke-hub
|
+| [gke-nodepools.tf](./gke-nodepools.tf) | GKE nodepools. | gke-nodepool
|
+| [main.tf](./main.tf) | Project and usage dataset. | bigquery-dataset
· project
|
+| [outputs.tf](./outputs.tf) | Output variables. | |
+| [variables.tf](./variables.tf) | Module variables. | |
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [billing_account_id](variables.tf#L17) | Billing account id. | string
| ✓ | |
+| [folder_id](variables.tf#L132) | Folder used for the GKE project in folders/nnnnnnnnnnn format. | string
| ✓ | |
+| [prefix](variables.tf#L179) | Prefix used for resource names. | string
| ✓ | |
+| [project_id](variables.tf#L188) | ID of the project that will contain all the clusters. | string
| ✓ | |
+| [vpc_config](variables.tf#L200) | Shared VPC project and VPC details. | object({…})
| ✓ | |
+| [clusters](variables.tf#L22) | Clusters configuration. Refer to the gke-cluster module for type details. | map(object({…}))
| | {}
|
+| [fleet_configmanagement_clusters](variables.tf#L70) | Config management features enabled on specific sets of member clusters, in config name => [cluster name] format. | map(list(string))
| | {}
|
+| [fleet_configmanagement_templates](variables.tf#L77) | Sets of config management configurations that can be applied to member clusters, in config name => {options} format. | map(object({…}))
| | {}
|
+| [fleet_features](variables.tf#L112) | 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#L125) | Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true. | bool
| | false
|
+| [group_iam](variables.tf#L137) | Project-level IAM bindings for groups. Use group emails as keys, list of roles as values. | map(list(string))
| | {}
|
+| [iam](variables.tf#L144) | Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
+| [labels](variables.tf#L151) | Project-level labels. | map(string)
| | {}
|
+| [nodepools](variables.tf#L157) | Nodepools configuration. Refer to the gke-nodepool module for type details. | map(map(object({…})))
| | {}
|
+| [project_services](variables.tf#L193) | Additional project services to enable. | list(string)
| | []
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [cluster_ids](outputs.tf#L17) | Cluster ids. | |
+| [clusters](outputs.tf#L24) | Cluster resources. | |
+| [project_id](outputs.tf#L29) | GKE project id. | |
+
+
diff --git a/blueprints/gke/multitenant-fleet/diagram.png b/blueprints/gke/multitenant-fleet/diagram.png
new file mode 100644
index 0000000000..a282e7d5e6
Binary files /dev/null and b/blueprints/gke/multitenant-fleet/diagram.png differ
diff --git a/blueprints/gke/multitenant-fleet/gke-clusters.tf b/blueprints/gke/multitenant-fleet/gke-clusters.tf
new file mode 100644
index 0000000000..a932cfa13c
--- /dev/null
+++ b/blueprints/gke/multitenant-fleet/gke-clusters.tf
@@ -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.
+ */
+
+# tfdoc:file:description GKE clusters.
+
+module "gke-cluster" {
+ source = "../../../modules/gke-cluster"
+ for_each = var.clusters
+ name = each.key
+ project_id = module.gke-project-0.project_id
+ cluster_autoscaling = each.value.cluster_autoscaling
+ description = each.value.description
+ enable_features = each.value.enable_features
+ issue_client_certificate = each.value.issue_client_certificate
+ labels = each.value.labels
+ location = each.value.location
+ logging_config = each.value.logging_config
+ maintenance_config = each.value.maintenance_config
+ max_pods_per_node = each.value.max_pods_per_node
+ min_master_version = each.value.min_master_version
+ monitoring_config = each.value.monitoring_config
+ node_locations = each.value.node_locations
+ private_cluster_config = each.value.private_cluster_config
+ release_channel = each.value.release_channel
+ vpc_config = merge(each.value.vpc_config, {
+ network = coalesce(
+ each.value.vpc_config.network, var.vpc_config.vpc_self_link
+ )
+ })
+}
diff --git a/blueprints/gke/multitenant-fleet/gke-hub.tf b/blueprints/gke/multitenant-fleet/gke-hub.tf
new file mode 100644
index 0000000000..2707046227
--- /dev/null
+++ b/blueprints/gke/multitenant-fleet/gke-hub.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.
+ */
+
+# tfdoc:file:description GKE hub configuration.
+
+locals {
+ fleet_enabled = (
+ var.fleet_features != null || var.fleet_workload_identity
+ )
+ fleet_mcs_enabled = (
+ try(var.fleet_features.multiclusterservicediscovery, false) == true
+ )
+}
+
+module "gke-hub" {
+ source = "../../../modules/gke-hub"
+ count = local.fleet_enabled ? 1 : 0
+ project_id = module.gke-project-0.project_id
+ clusters = {
+ for cluster_id in keys(var.clusters) :
+ cluster_id => module.gke-cluster[cluster_id].id
+ }
+ features = var.fleet_features
+ configmanagement_templates = var.fleet_configmanagement_templates
+ configmanagement_clusters = var.fleet_configmanagement_clusters
+ workload_identity_clusters = (
+ var.fleet_workload_identity ? keys(var.clusters) : []
+ )
+
+ depends_on = [
+ module.gke-nodepool
+ ]
+}
diff --git a/blueprints/gke/multitenant-fleet/gke-nodepools.tf b/blueprints/gke/multitenant-fleet/gke-nodepools.tf
new file mode 100644
index 0000000000..fdadf95455
--- /dev/null
+++ b/blueprints/gke/multitenant-fleet/gke-nodepools.tf
@@ -0,0 +1,51 @@
+/**
+ * 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:file:description GKE nodepools.
+
+locals {
+ nodepools = merge([
+ for cluster, nodepools in var.nodepools : {
+ for nodepool, config in nodepools :
+ "${cluster}/${nodepool}" => merge(config, {
+ name = nodepool
+ cluster = cluster
+ })
+ }
+ ]...)
+}
+
+module "gke-nodepool" {
+ source = "../../../modules/gke-nodepool"
+ for_each = local.nodepools
+ name = each.value.name
+ project_id = module.gke-project-0.project_id
+ cluster_name = module.gke-cluster[each.value.cluster].name
+ location = module.gke-cluster[each.value.cluster].location
+ gke_version = each.value.gke_version
+ labels = each.value.labels
+ max_pods_per_node = each.value.max_pods_per_node
+ node_config = each.value.node_config
+ node_count = each.value.node_count
+ node_locations = each.value.node_locations
+ nodepool_config = each.value.nodepool_config
+ pod_range = each.value.pod_range
+ reservation_affinity = each.value.reservation_affinity
+ service_account = each.value.service_account
+ sole_tenant_nodegroup = each.value.sole_tenant_nodegroup
+ tags = each.value.tags
+ taints = each.value.taints
+}
diff --git a/blueprints/gke/multitenant-fleet/main.tf b/blueprints/gke/multitenant-fleet/main.tf
new file mode 100644
index 0000000000..588d6c5b62
--- /dev/null
+++ b/blueprints/gke/multitenant-fleet/main.tf
@@ -0,0 +1,84 @@
+/**
+ * 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:file:description Project and usage dataset.
+
+module "gke-project-0" {
+ source = "../../../modules/project"
+ billing_account = var.billing_account_id
+ name = var.project_id
+ parent = var.folder_id
+ prefix = var.prefix
+ group_iam = var.group_iam
+ labels = var.labels
+ iam = merge(var.iam, {
+ "roles/gkehub.serviceAgent" = [
+ "serviceAccount:${module.gke-project-0.service_accounts.robots.fleet}"
+ ] }
+ )
+ services = concat(
+ [
+ "anthos.googleapis.com",
+ "anthosconfigmanagement.googleapis.com",
+ "cloudresourcemanager.googleapis.com",
+ "container.googleapis.com",
+ "dns.googleapis.com",
+ "gkeconnect.googleapis.com",
+ "gkehub.googleapis.com",
+ "iam.googleapis.com",
+ "multiclusteringress.googleapis.com",
+ "multiclusterservicediscovery.googleapis.com",
+ "stackdriver.googleapis.com",
+ "trafficdirector.googleapis.com"
+ ],
+ var.project_services
+ )
+ shared_vpc_service_config = {
+ attach = true
+ host_project = var.vpc_config.host_project_id
+ service_identity_iam = merge({
+ "roles/compute.networkUser" = [
+ "cloudservices", "container-engine"
+ ]
+ "roles/container.hostServiceAgentUser" = [
+ "container-engine"
+ ]
+ },
+ !local.fleet_mcs_enabled ? {} : {
+ "roles/multiclusterservicediscovery.serviceAgent" = ["gke-mcs"]
+ "roles/compute.networkViewer" = ["gke-mcs-importer"]
+ })
+ }
+ # specify project-level org policies here if you need them
+ # policy_boolean = {
+ # "constraints/compute.disableGuestAttributesAccess" = true
+ # }
+ # policy_list = {
+ # "constraints/compute.trustedImageProjects" = {
+ # inherit_from_parent = null
+ # suggested_value = null
+ # status = true
+ # values = ["projects/fl01-prod-iac-core-0"]
+ # }
+ # }
+}
+
+module "gke-dataset-resource-usage" {
+ source = "../../../modules/bigquery-dataset"
+ project_id = module.gke-project-0.project_id
+ id = "gke_resource_usage"
+ friendly_name = "GKE resource usage."
+}
diff --git a/blueprints/gke/multitenant-fleet/outputs.tf b/blueprints/gke/multitenant-fleet/outputs.tf
new file mode 100644
index 0000000000..e9eb6985ee
--- /dev/null
+++ b/blueprints/gke/multitenant-fleet/outputs.tf
@@ -0,0 +1,32 @@
+# 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.
+
+# tfdoc:file:description Output variables.
+
+output "cluster_ids" {
+ description = "Cluster ids."
+ value = {
+ for k, v in module.gke-cluster : k => v.id
+ }
+}
+
+output "clusters" {
+ description = "Cluster resources."
+ value = module.gke-cluster
+}
+
+output "project_id" {
+ description = "GKE project id."
+ value = module.gke-project-0.project_id
+}
diff --git a/blueprints/gke/multitenant-fleet/variables.tf b/blueprints/gke/multitenant-fleet/variables.tf
new file mode 100644
index 0000000000..2cfd26a1bc
--- /dev/null
+++ b/blueprints/gke/multitenant-fleet/variables.tf
@@ -0,0 +1,206 @@
+/**
+ * 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_id" {
+ description = "Billing account id."
+ type = string
+}
+
+variable "clusters" {
+ description = "Clusters configuration. Refer to the gke-cluster module for type details."
+ type = map(object({
+ cluster_autoscaling = optional(any)
+ description = optional(string)
+ enable_addons = optional(any, {
+ horizontal_pod_autoscaling = true, http_load_balancing = true
+ })
+ enable_features = optional(any, {
+ workload_identity = true
+ })
+ issue_client_certificate = optional(bool, false)
+ labels = optional(map(string))
+ location = string
+ logging_config = optional(list(string), ["SYSTEM_COMPONENTS"])
+ maintenance_config = optional(any, {
+ daily_window_start_time = "03:00"
+ recurring_window = null
+ maintenance_exclusion = []
+ })
+ max_pods_per_node = optional(number, 110)
+ min_master_version = optional(string)
+ monitoring_config = optional(object({
+ enable_components = optional(list(string), ["SYSTEM_COMPONENTS"])
+ managed_prometheus = optional(bool)
+ }))
+ node_locations = optional(list(string))
+ private_cluster_config = optional(any)
+ release_channel = optional(string)
+ vpc_config = object({
+ subnetwork = string
+ network = optional(string)
+ secondary_range_blocks = optional(object({
+ pods = string
+ services = string
+ }))
+ secondary_range_names = optional(object({
+ pods = string
+ services = string
+ }), { pods = "pods", services = "services" })
+ master_authorized_ranges = optional(map(string))
+ master_ipv4_cidr_block = optional(string)
+ })
+ }))
+ default = {}
+ nullable = false
+}
+
+variable "fleet_configmanagement_clusters" {
+ description = "Config management features enabled on specific sets of member clusters, in config name => [cluster name] format."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "fleet_configmanagement_templates" {
+ description = "Sets of config management configurations that can be applied to member clusters, in config name => {options} format."
+ type = map(object({
+ binauthz = bool
+ config_sync = object({
+ git = object({
+ gcp_service_account_email = string
+ https_proxy = string
+ policy_dir = string
+ secret_type = string
+ sync_branch = string
+ sync_repo = string
+ sync_rev = string
+ sync_wait_secs = number
+ })
+ prevent_drift = string
+ source_format = string
+ })
+ hierarchy_controller = object({
+ enable_hierarchical_resource_quota = bool
+ enable_pod_tree_labels = bool
+ })
+ policy_controller = object({
+ audit_interval_seconds = number
+ exemptable_namespaces = list(string)
+ log_denies_enabled = bool
+ referential_rules_enabled = bool
+ template_library_installed = bool
+ })
+ version = string
+ }))
+ default = {}
+ nullable = false
+}
+
+variable "fleet_features" {
+ description = "Enable and configue fleet features. Set to null to disable GKE Hub if fleet workload identity is not used."
+ type = object({
+ appdevexperience = bool
+ configmanagement = bool
+ identityservice = bool
+ multiclusteringress = string
+ multiclusterservicediscovery = bool
+ servicemesh = bool
+ })
+ default = null
+}
+
+variable "fleet_workload_identity" {
+ description = "Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true."
+ type = bool
+ default = false
+ nullable = false
+}
+
+variable "folder_id" {
+ description = "Folder used for the GKE project in folders/nnnnnnnnnnn format."
+ type = string
+}
+
+variable "group_iam" {
+ description = "Project-level IAM bindings for groups. Use group emails as keys, list of roles as values."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "iam" {
+ description = "Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "labels" {
+ description = "Project-level labels."
+ type = map(string)
+ default = {}
+}
+
+variable "nodepools" {
+ description = "Nodepools configuration. Refer to the gke-nodepool module for type details."
+ type = map(map(object({
+ gke_version = optional(string)
+ labels = optional(map(string), {})
+ max_pods_per_node = optional(number)
+ name = optional(string)
+ node_config = optional(any, { disk_type = "pd-balanced" })
+ node_count = optional(map(number), { initial = 1 })
+ node_locations = optional(list(string))
+ nodepool_config = optional(any)
+ pod_range = optional(any)
+ reservation_affinity = optional(any)
+ service_account = optional(any)
+ sole_tenant_nodegroup = optional(string)
+ tags = optional(list(string))
+ taints = optional(list(any))
+ })))
+ default = {}
+ nullable = false
+}
+
+variable "prefix" {
+ description = "Prefix used for resource names."
+ type = string
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty."
+ }
+}
+
+variable "project_id" {
+ description = "ID of the project that will contain all the clusters."
+ type = string
+}
+
+variable "project_services" {
+ description = "Additional project services to enable."
+ type = list(string)
+ default = []
+ nullable = false
+}
+
+variable "vpc_config" {
+ description = "Shared VPC project and VPC details."
+ type = object({
+ host_project_id = string
+ vpc_self_link = string
+ })
+}
diff --git a/blueprints/gke/shared-vpc-gke b/blueprints/gke/shared-vpc-gke
new file mode 120000
index 0000000000..e7fc85e67b
--- /dev/null
+++ b/blueprints/gke/shared-vpc-gke
@@ -0,0 +1 @@
+../networking/shared-vpc-gke
\ No newline at end of file
diff --git a/blueprints/networking/README.md b/blueprints/networking/README.md
new file mode 100644
index 0000000000..ec510d5649
--- /dev/null
+++ b/blueprints/networking/README.md
@@ -0,0 +1,84 @@
+# Networking and infrastructure blueprints
+
+The blueprints in this folder implement **typical network topologies** like hub and spoke, or **end-to-end scenarios** that allow testing specific features like on-premises DNS policies and Private Google Access.
+
+They are meant to be used as minimal but complete starting points to create actual infrastructure, and as playgrounds to experiment with specific Google Cloud features.
+
+## Blueprints
+
+### Decentralized firewall management
+
+ This [blueprint](./decentralized-firewall/) shows how a decentralized firewall management can be organized using the [firewall factory](../factories/net-vpc-firewall-yaml/).
+
+object({…}
| ✓ | |
+| [prefix](variables.tf#L94) | Prefix used for resource names. | string
| ✓ | |
+| [project_name](variables.tf#L112) | Name of an existing project or of the new project. | string
| ✓ | |
+| [autoscaling](variables.tf#L17) | Autoscaling configuration for the instance group. | object({…})
| | {…}
|
+| [backends](variables.tf#L50) | Nginx locations configurations to proxy traffic to. | string
| | "<<-EOT…EOT"
|
+| [cidrs](variables.tf#L62) | Subnet IP CIDR ranges. | map(string)
| | {…}
|
+| [network](variables.tf#L70) | Network name. | string
| | "reverse-proxy-vpc"
|
+| [network_create](variables.tf#L76) | Create network or use existing one. | bool
| | true
|
+| [nginx_image](variables.tf#L82) | Nginx container image to use. | string
| | "gcr.io/cloud-marketplace/google/nginx1:latest"
|
+| [ops_agent_image](variables.tf#L88) | Google Cloud Ops Agent container image to use. | string
| | "gcr.io/sfans-hub-project-d647/ops-agent:latest"
|
+| [project_create](variables.tf#L103) | Parameters for the creation of the new project. | object({…})
| | null
|
+| [region](variables.tf#L117) | Default region for resources. | string
| | "europe-west4"
|
+| [subnetwork](variables.tf#L123) | Subnetwork name. | string
| | "gce"
|
+| [tls](variables.tf#L129) | Also offer reverse proxying with TLS (self-signed certificate). | bool
| | false
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [load_balancer_url](outputs.tf#L17) | Load balancer for the reverse proxy instance group. | |
+
+
diff --git a/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/main.tf b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/main.tf
new file mode 100644
index 0000000000..df0397bd4b
--- /dev/null
+++ b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/main.tf
@@ -0,0 +1,327 @@
+/**
+ * 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 {
+ monitoring_agent_unit = <<-EOT
+ [Unit]
+ Description=Start monitoring agent container
+ After=gcr-online.target docker.socket
+ Wants=gcr-online.target docker.socket docker-events-collector.service
+
+ [Service]
+ Environment="HOME=/home/opsagent"
+ ExecStartPre=/usr/bin/docker-credential-gcr configure-docker
+ ExecStart=/usr/bin/docker run --rm --name=monitoring-agent \
+ --network host \
+ -v /etc/google-cloud-ops-agent/config.yaml:/etc/google-cloud-ops-agent/config.yaml \
+ ${var.ops_agent_image}
+ ExecStop=/usr/bin/docker stop monitoring-agent
+ EOT
+ monitoring_agent_config = <<-EOT
+ logging:
+ service:
+ pipelines:
+ default_pipeline:
+ receivers: []
+ metrics:
+ receivers:
+ hostmetrics:
+ type: hostmetrics
+ nginx:
+ type: nginx
+ collection_interval: 10s
+ stub_status_url: http://localhost/healthz
+ service:
+ pipelines:
+ default_pipeline:
+ receivers:
+ - hostmetrics
+ - nginx
+ EOT
+ nginx_config = <<-EOT
+ server {
+ listen 80;
+ server_name HOSTNAME localhost;
+ %{if var.tls}
+ listen 443 ssl;
+ ssl_certificate /etc/ssl/self-signed.crt;
+ ssl_certificate_key /etc/ssl/self-signed.key;
+ %{endif}
+
+ keepalive_timeout 650s;
+ keepalive_requests 10000;
+
+ proxy_connect_timeout 60s;
+ proxy_read_timeout 5m;
+ proxy_send_timeout 5m;
+
+ error_log stderr;
+ access_log /dev/stdout combined;
+
+ set_real_ip_from ${module.glb.address}/32;
+ set_real_ip_from 35.191.0.0/16;
+ set_real_ip_from 130.211.0.0/22;
+ real_ip_header X-Forwarded-For;
+ real_ip_recursive off;
+
+ location /healthz {
+ stub_status on;
+ access_log off;
+ allow 127.0.0.1;
+ allow 35.191.0.0/16;
+ allow 130.211.0.0/22;
+ deny all;
+ }
+
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ root /usr/share/nginx/html;
+ }
+
+ ${var.backends}
+ }
+ EOT
+ nginx_files = {
+ "/etc/systemd/system/monitoring-agent.service" = {
+ content = local.monitoring_agent_unit
+ owner = "root"
+ permissions = "0644"
+ }
+ "/etc/nginx/conf.d/default.conf" = {
+ content = local.nginx_config
+ owner = "root"
+ permissions = "0644"
+ }
+ "/etc/google-cloud-ops-agent/config.yaml" = {
+ content = local.monitoring_agent_config
+ owner = "root"
+ permissions = "0644"
+ }
+ }
+ users = [
+ {
+ username = "opsagent"
+ uid = 2001
+ }
+ ]
+}
+
+module "project" {
+ source = "../../../modules/project"
+ billing_account = (
+ var.project_create != null
+ ? var.project_create.billing_account_id
+ : null
+ )
+ name = var.project_name
+ parent = (var.project_create != null
+ ? var.project_create.parent
+ : null
+ )
+ project_create = var.project_create != null
+ services = [
+ "cloudresourcemanager.googleapis.com",
+ "compute.googleapis.com",
+ "iam.googleapis.com",
+ "logging.googleapis.com",
+ "monitoring.googleapis.com",
+ ]
+}
+
+module "vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = var.network
+ subnets = [{
+ name = var.subnetwork
+ ip_cidr_range = var.cidrs[var.subnetwork]
+ region = var.region
+ }]
+ vpc_create = var.network_create
+}
+
+module "firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.project.project_id
+ network = module.vpc.name
+ ingress_rules = {
+ "${var.prefix}-allow-http-to-proxy-cluster" = {
+ description = "Allow Nginx HTTP(S) ingress traffic"
+ source_ranges = [
+ var.cidrs[var.subnetwork], "35.191.0.0/16", "130.211.0.0/22"
+ ]
+ targets = [module.service-account-proxy.email]
+ use_service_accounts = true
+ rules = [{ protocol = "tcp", ports = [80, 443] }]
+ }
+ "${var.prefix}-allow-iap-ssh" = {
+ description = "Allow Nginx SSH traffic from IAP"
+ source_ranges = ["35.235.240.0/20"]
+ targets = [module.service-account-proxy.email]
+ use_service_accounts = true
+ rules = [{ protocol = "tcp", ports = [22] }]
+ }
+ }
+}
+
+module "nat" {
+ source = "../../../modules/net-cloudnat"
+ project_id = module.project.project_id
+ region = var.region
+ name = "${var.prefix}-nat"
+ config_min_ports_per_vm = 4000
+ config_source_subnets = "LIST_OF_SUBNETWORKS"
+ logging_filter = "ALL"
+ router_network = module.vpc.name
+ subnetworks = [{
+ self_link = (
+ module.vpc.subnet_self_links[format("%s/%s", var.region, var.subnetwork)]
+ )
+ config_source_ranges = ["ALL_IP_RANGES"]
+ secondary_ranges = null
+ }]
+}
+
+###############################################################################
+# Proxy resources #
+###############################################################################
+
+module "service-account-proxy" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.project.project_id
+ name = "${var.prefix}-reverse-proxy"
+ iam_project_roles = {
+ (module.project.project_id) = [
+ "roles/logging.logWriter",
+ "roles/monitoring.metricWriter",
+ "roles/storage.objectViewer", // For pulling the Ops Agent image
+ ]
+ }
+}
+
+module "cos-nginx" {
+ count = !var.tls ? 1 : 0
+ source = "../../../modules/cloud-config-container/nginx"
+ image = var.nginx_image
+ files = local.nginx_files
+ users = local.users
+ runcmd_pre = ["sed -i \"s/HOSTNAME/$${HOSTNAME}/\" /etc/nginx/conf.d/default.conf"]
+ runcmd_post = ["systemctl start monitoring-agent"]
+}
+
+module "cos-nginx-tls" {
+ count = var.tls ? 1 : 0
+ source = "../../../modules/cloud-config-container/nginx-tls"
+ nginx_image = var.nginx_image
+ files = local.nginx_files
+ users = local.users
+ runcmd_post = ["systemctl start monitoring-agent"]
+}
+
+module "mig-proxy" {
+ source = "../../../modules/compute-mig"
+ project_id = module.project.project_id
+ location = var.region
+ name = "${var.prefix}-proxy-cluster"
+ named_ports = {
+ http = "80"
+ https = "443"
+ }
+ autoscaler_config = var.autoscaling == null ? null : {
+ min_replicas = var.autoscaling.min_replicas
+ max_replicas = var.autoscaling.max_replicas
+ cooldown_period = var.autoscaling.cooldown_period
+ cpu_utilization_target = null
+ load_balancing_utilization_target = null
+ metric = var.autoscaling_metric
+ }
+ update_policy = {
+ minimal_action = "REPLACE"
+ type = "PROACTIVE"
+ min_ready_sec = 30
+ max_surge = {
+ fixed = 1
+ }
+ }
+ instance_template = module.proxy-vm.template.self_link
+ health_check_config = {
+ type = "http"
+ check = {
+ port = 80
+ request_path = "/healthz"
+ }
+ config = {
+ check_interval_sec = 10
+ healthy_threshold = 1
+ unhealthy_threshold = 1
+ timeout_sec = 10
+ }
+ logging = true
+ }
+ auto_healing_policies = {
+ health_check = module.mig-proxy.health_check.self_link
+ initial_delay_sec = 60
+ }
+}
+
+module "proxy-vm" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project.project_id
+ zone = format("%s-c", var.region)
+ name = "nginx-test-vm"
+ instance_type = "e2-standard-2"
+ tags = ["proxy-cluster"]
+ network_interfaces = [{
+ network = module.vpc.self_link
+ subnetwork = module.vpc.subnet_self_links[format("%s/%s", var.region, var.subnetwork)]
+ }]
+ boot_disk = {
+ image = "projects/cos-cloud/global/images/family/cos-stable"
+ }
+ create_template = true
+ metadata = {
+ user-data = !var.tls ? module.cos-nginx.0.cloud_config : module.cos-nginx-tls.0.cloud_config
+ google-logging-enabled = true
+ }
+ service_account = module.service-account-proxy.email
+ service_account_create = false
+}
+
+module "glb" {
+ source = "../../../modules/net-glb"
+ project_id = module.project.project_id
+ name = "${var.prefix}-reverse-proxy-glb"
+ health_check_configs = {
+ default = {
+ check_interval_sec = 10
+ healthy_threshold = 1
+ unhealthy_threshold = 1
+ timeout_sec = 10
+ http = {
+ port_specification = "USE_NAMED_PORT"
+ port_name = "http"
+ request_path = "/healthz"
+ }
+ }
+ }
+ backend_service_configs = {
+ default = {
+ backends = [{ backend = module.mig-proxy.group_manager.instance_group }]
+ port_name = !var.tls ? "http" : "https"
+ protocol = !var.tls ? "HTTP" : "HTTPS"
+ }
+ }
+}
diff --git a/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/outputs.tf b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/outputs.tf
new file mode 100644
index 0000000000..1e0fda0045
--- /dev/null
+++ b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/outputs.tf
@@ -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.
+ */
+
+output "load_balancer_url" {
+ description = "Load balancer for the reverse proxy instance group."
+ value = format("http%s://%s/", var.tls ? "s" : "", module.glb.address)
+}
diff --git a/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/reverse-proxy.png b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/reverse-proxy.png
new file mode 100644
index 0000000000..dd0c8e6223
Binary files /dev/null and b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/reverse-proxy.png differ
diff --git a/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/variables.tf b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/variables.tf
new file mode 100644
index 0000000000..286bbcbebb
--- /dev/null
+++ b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/variables.tf
@@ -0,0 +1,133 @@
+/**
+ * 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 "autoscaling" {
+ description = "Autoscaling configuration for the instance group."
+ type = object({
+ min_replicas = number
+ max_replicas = number
+ cooldown_period = number
+ })
+ default = {
+ min_replicas = 1
+ max_replicas = 10
+ cooldown_period = 30
+ }
+}
+
+variable "autoscaling_metric" {
+ description = "Definition of metric to use for scaling."
+ type = object({
+ name = string
+ single_instance_assignment = number
+ target = number
+ type = string # GAUGE, DELTA_PER_SECOND, DELTA_PER_MINUTE
+ filter = string
+ })
+
+ default = {
+ name = "workload.googleapis.com/nginx.connections_current"
+ single_instance_assignment = null
+ target = 10 # Target 10 connections per instance, just for demonstration purposes
+ type = "GAUGE"
+ filter = null
+ }
+}
+
+variable "backends" {
+ description = "Nginx locations configurations to proxy traffic to."
+ type = string
+ default = <<-EOT
+ location / {
+ proxy_pass http://10.0.16.58:80;
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ }
+ EOT
+}
+
+variable "cidrs" {
+ description = "Subnet IP CIDR ranges."
+ type = map(string)
+ default = {
+ gce = "10.0.16.0/24"
+ }
+}
+
+variable "network" {
+ description = "Network name."
+ type = string
+ default = "reverse-proxy-vpc"
+}
+
+variable "network_create" {
+ description = "Create network or use existing one."
+ type = bool
+ default = true
+}
+
+variable "nginx_image" {
+ description = "Nginx container image to use."
+ type = string
+ default = "gcr.io/cloud-marketplace/google/nginx1:latest"
+}
+
+variable "ops_agent_image" {
+ description = "Google Cloud Ops Agent container image to use."
+ type = string
+ default = "gcr.io/sfans-hub-project-d647/ops-agent:latest"
+}
+
+variable "prefix" {
+ description = "Prefix used for resource names."
+ type = string
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty."
+ }
+}
+
+variable "project_create" {
+ description = "Parameters for the creation of the new project."
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = null
+}
+
+variable "project_name" {
+ description = "Name of an existing project or of the new project."
+ type = string
+}
+
+variable "region" {
+ description = "Default region for resources."
+ type = string
+ default = "europe-west4"
+}
+
+variable "subnetwork" {
+ description = "Subnetwork name."
+ type = string
+ default = "gce"
+}
+
+variable "tls" {
+ description = "Also offer reverse proxying with TLS (self-signed certificate)."
+ type = bool
+ default = false
+}
diff --git a/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/versions.tf b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/networking/__need_fixing/nginx-reverse-proxy-cluster/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/networking/__need_fixing/onprem-google-access-dns/README.md b/blueprints/networking/__need_fixing/onprem-google-access-dns/README.md
new file mode 100644
index 0000000000..deb3593f79
--- /dev/null
+++ b/blueprints/networking/__need_fixing/onprem-google-access-dns/README.md
@@ -0,0 +1,226 @@
+# On-prem DNS and Google Private Access
+
+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)
+- [Private Access for on-premises hosts](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid)
+
+The blueprint has been purposefully kept simple to show how to use and wire the on-prem module, but it lends itself well to experimenting and can be combined with the other [infrastructure blueprints](../) in this repository to test different GCP networking patterns in connection to on-prem. This is the high level diagram:
+
+![High-level diagram](diagram.png "High-level diagram")
+
+## Managed resources and services
+
+This sample creates several distinct groups of resources:
+
+- one VPC with two regions
+- one set of firewall rules
+- one Cloud NAT configuration per region
+- one test instance on each region
+- one service account for the test instances
+- one service account for the onprem instance
+- two dynamic VPN gateways in each of the regions with a single tunnel
+- two DNS zones (private and forwarding) and a DNS inbound policy
+- one emulated on-premises environment in a single GCP instance
+
+## Cloud DNS inbound forwarder entry point
+
+The Cloud DNS inbound policy reserves an IP address in the VPC, which is used by the on-prem DNS server to forward queries to Cloud DNS. This address needs of course to be explicitly set in the on-prem DNS configuration (see below for details), but since there's currently no way for Terraform to find the exact address (cf [Google provider issue](https://github.com/terraform-providers/terraform-provider-google/issues/3753)), the following manual workaround needs to be applied.
+
+### 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):
+
+```bash
+gcloud compute addresses list --project [your project id]
+```
+
+In the list of addresses, look for the address with purpose `DNS_RESOLVER` in the subnet `to-onprem-default`. If its IP address is `10.0.0.2` it matches the default value in the Terraform `forwarder_address` variable, which means you're all set. If it's different, proceed to the next step.
+
+### Update the forwarder address variable and recreate on-prem
+
+If the forwarder address does not match the Terraform variable, add the correct value in your `terraform.tfvars` (or change the default value in `variables.tf`), then taint the onprem instance and apply to recreate it with the correct value in the DNS configuration:
+
+```bash
+tf apply
+tf taint 'module.vm-onprem.google_compute_instance.default["onprem-1"]'
+tf apply
+```
+
+## CoreDNS configuration for on-premises
+
+The on-prem module uses a CoreDNS container to expose its DNS service, configured with foru distinct blocks:
+
+- the onprem block serving static records for the `onprem.example.com` zone that map to each of the on-prem containers
+- the forwarding block for the `gcp.example.com` zone and for Google Private Access, that map to the IP address of the Cloud DNS inbound policy
+- the `google.internal` block that exposes to containers a name for the instance metadata address
+- the default block that forwards to Google public DNS resolvers
+
+This is the CoreDNS configuration:
+
+```coredns
+onprem.example.com {
+ root /etc/coredns
+ hosts onprem.hosts
+ log
+ errors
+}
+gcp.example.com googleapis.com {
+ forward . ${resolver_address}
+ log
+ errors
+}
+google.internal {
+ hosts {
+ 169.254.169.254 metadata.google.internal
+ }
+}
+. {
+ forward . 8.8.8.8
+ log
+ errors
+}
+```
+
+## Testing
+
+### Onprem to cloud
+
+```bash
+# check containers are running
+sudo docker ps
+
+# connect to the onprem instance
+gcloud compute ssh onprem-1
+
+# check that the VPN tunnels are up
+sudo docker exec -it onprem_vpn_1 ipsec statusall
+
+Status of IKE charon daemon (strongSwan 5.8.1, Linux 5.4.0-1029-gcp, x86_64):
+ uptime: 6 minutes, since Nov 30 08:42:08 2020
+ worker threads: 11 of 16 idle, 5/0/0/0 working, job queue: 0/0/0/0, scheduled: 8
+ loaded plugins: charon aesni mgf1 random nonce x509 revocation constraints pubkey pkcs1 pkcs7 pkcs8 pkcs12 pgp dnskey sshkey pem openssl fips-prf gmp curve25519 xcbc cmac curl sqlite attr kernel-netlink resolve socket-default farp stroke vici updown eap-identity eap-sim eap-aka eap-aka-3gpp2 eap-simaka-pseudonym eap-simaka-reauth eap-md5 eap-mschapv2 eap-radius eap-tls xauth-generic xauth-eap dhcp unity counters
+Listening IP addresses:
+ 10.0.16.2
+ 169.254.1.2
+ 169.254.2.2
+Connections:
+ gcp: %any...35.233.104.67,0.0.0.0/0,::/0 IKEv2, dpddelay=30s
+ gcp: local: uses pre-shared key authentication
+ gcp: remote: [35.233.104.67] uses pre-shared key authentication
+ gcp: child: 0.0.0.0/0 === 0.0.0.0/0 TUNNEL, dpdaction=restart
+ gcp2: %any...35.246.101.51,0.0.0.0/0,::/0 IKEv2, dpddelay=30s
+ gcp2: local: uses pre-shared key authentication
+ gcp2: remote: [35.246.101.51] uses pre-shared key authentication
+ gcp2: child: 0.0.0.0/0 === 0.0.0.0/0 TUNNEL, dpdaction=restart
+Security Associations (2 up, 0 connecting):
+ gcp2[4]: ESTABLISHED 6 minutes ago, 10.0.16.2[34.76.57.103]...35.246.101.51[35.246.101.51]
+ gcp2[4]: IKEv2 SPIs: 227cb2c52085a743_i 13b18b0ad5d4de2b_r*, pre-shared key reauthentication in 9 hours
+ gcp2[4]: IKE proposal: AES_GCM_16_256/PRF_HMAC_SHA2_512/MODP_2048
+ gcp2{4}: INSTALLED, TUNNEL, reqid 2, ESP in UDP SPIs: cb6fdb84_i eea28dee_o
+ gcp2{4}: AES_GCM_16_256, 3298 bytes_i, 3051 bytes_o (48 pkts, 3s ago), rekeying in 2 hours
+ gcp2{4}: 0.0.0.0/0 === 0.0.0.0/0
+ gcp[3]: ESTABLISHED 6 minutes ago, 10.0.16.2[34.76.57.103]...35.233.104.67[35.233.104.67]
+ gcp[3]: IKEv2 SPIs: e2cffed5395b63dd_i 99f343468625507c_r*, pre-shared key reauthentication in 9 hours
+ gcp[3]: IKE proposal: AES_GCM_16_256/PRF_HMAC_SHA2_512/MODP_2048
+ gcp{3}: INSTALLED, TUNNEL, reqid 1, ESP in UDP SPIs: c3f09701_i 4e8cc8d5_o
+ gcp{3}: AES_GCM_16_256, 3438 bytes_i, 3135 bytes_o (49 pkts, 8s ago), rekeying in 2 hours
+ gcp{3}: 0.0.0.0/0 === 0.0.0.0/0
+
+# check that the BGP sessions works and the advertised routes are set
+sudo docker exec -it onprem_bird_1 ip route
+default via 10.0.16.1 dev eth0
+10.0.0.0/24 proto bird src 10.0.16.2
+ nexthop via 169.254.1.1 dev vti0 weight 1
+ nexthop via 169.254.2.1 dev vti1 weight 1
+10.0.16.0/24 dev eth0 proto kernel scope link src 10.0.16.2
+10.10.0.0/24 proto bird src 10.0.16.2
+ nexthop via 169.254.1.1 dev vti0 weight 1
+ nexthop via 169.254.2.1 dev vti1 weight 1
+35.199.192.0/19 proto bird src 10.0.16.2
+ nexthop via 169.254.1.1 dev vti0 weight 1
+ nexthop via 169.254.2.1 dev vti1 weight 1
+169.254.1.0/30 dev vti0 proto kernel scope link src 169.254.1.2
+169.254.2.0/30 dev vti1 proto kernel scope link src 169.254.2.2
+199.36.153.4/30 proto bird src 10.0.16.2
+ nexthop via 169.254.1.1 dev vti0 weight 1
+ nexthop via 169.254.2.1 dev vti1 weight 1
+199.36.153.8/30 proto bird src 10.0.16.2
+ nexthop via 169.254.1.1 dev vti0 weight 1
+ nexthop via 169.254.2.1 dev vti1 weight 1
+
+
+# get a shell on the toolbox container
+sudo docker exec -it onprem_toolbox_1 sh
+
+# test pinging the IP address of the test instances (check outputs for it)
+ping 10.0.0.3
+ping 10.10.0.3
+
+# note: if you are able to ping the IP but the DNS tests below do not work,
+# refer to the sections above on configuring the DNS inbound fwd IP
+
+# test forwarding from CoreDNS via the Cloud DNS inbound policy
+dig test-1-1.gcp.example.org +short
+10.0.0.3
+dig test-2-1.gcp.example.org +short
+10.10.0.3
+
+# test that Private Access is configured correctly
+dig compute.googleapis.com +short
+private.googleapis.com.
+199.36.153.8
+199.36.153.9
+199.36.153.10
+199.36.153.11
+
+# issue an API call via Private Access
+gcloud config set project [your project id]
+gcloud compute instances list
+```
+
+### Cloud to onprem
+
+```bash
+# connect to the test instance
+gcloud compute ssh test-1
+
+# test forwarding from Cloud DNS to onprem CoreDNS (address may differ)
+dig gw.onprem.example.org +short
+10.0.16.1
+
+# test a request to the onprem web server
+curl www.onprem.example.org -s |grep h1
+string
| ✓ | |
+| [bgp_asn](variables.tf#L17) | BGP ASNs. | map(number)
| | {…}
|
+| [bgp_interface_ranges](variables.tf#L28) | BGP interface IP CIDR ranges. | map(string)
| | {…}
|
+| [dns_forwarder_address](variables.tf#L37) | Address of the DNS server used to forward queries from on-premises. | string
| | "10.0.0.2"
|
+| [forwarder_address](variables.tf#L43) | GCP DNS inbound policy forwarder address. | string
| | "10.0.0.2"
|
+| [ip_ranges](variables.tf#L49) | IP CIDR ranges. | map(string)
| | {…}
|
+| [region](variables.tf#L64) | VPC region. | map(string)
| | {…}
|
+| [ssh_source_ranges](variables.tf#L73) | IP CIDR ranges that will be allowed to connect via SSH to the onprem instance. | list(string)
| | ["0.0.0.0/0"]
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [onprem-instance](outputs.tf#L17) | Onprem instance details. | |
+| [test-instance1](outputs.tf#L26) | Test instance details. | |
+| [test-instance2](outputs.tf#L33) | Test instance details. | |
+
+
diff --git a/examples/networking/onprem-google-access-dns/assets/Corefile b/blueprints/networking/__need_fixing/onprem-google-access-dns/assets/Corefile
similarity index 100%
rename from examples/networking/onprem-google-access-dns/assets/Corefile
rename to blueprints/networking/__need_fixing/onprem-google-access-dns/assets/Corefile
diff --git a/examples/networking/decentralized-firewall/backend.tf.sample b/blueprints/networking/__need_fixing/onprem-google-access-dns/backend.tf.sample
similarity index 100%
rename from examples/networking/decentralized-firewall/backend.tf.sample
rename to blueprints/networking/__need_fixing/onprem-google-access-dns/backend.tf.sample
diff --git a/examples/networking/onprem-google-access-dns/diagram.png b/blueprints/networking/__need_fixing/onprem-google-access-dns/diagram.png
similarity index 100%
rename from examples/networking/onprem-google-access-dns/diagram.png
rename to blueprints/networking/__need_fixing/onprem-google-access-dns/diagram.png
diff --git a/blueprints/networking/__need_fixing/onprem-google-access-dns/main.tf b/blueprints/networking/__need_fixing/onprem-google-access-dns/main.tf
new file mode 100644
index 0000000000..8be38fbc28
--- /dev/null
+++ b/blueprints/networking/__need_fixing/onprem-google-access-dns/main.tf
@@ -0,0 +1,317 @@
+/**
+ * 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 {
+ bgp_interface_gcp1 = cidrhost(var.bgp_interface_ranges.gcp1, 1)
+ bgp_interface_onprem1 = cidrhost(var.bgp_interface_ranges.gcp1, 2)
+ bgp_interface_gcp2 = cidrhost(var.bgp_interface_ranges.gcp2, 1)
+ bgp_interface_onprem2 = cidrhost(var.bgp_interface_ranges.gcp2, 2)
+ netblocks = {
+ dns = data.google_netblock_ip_ranges.dns-forwarders.cidr_blocks_ipv4.0
+ private = data.google_netblock_ip_ranges.private-googleapis.cidr_blocks_ipv4.0
+ restricted = data.google_netblock_ip_ranges.restricted-googleapis.cidr_blocks_ipv4.0
+ }
+ vips = {
+ private = [for i in range(4) : cidrhost(local.netblocks.private, i)]
+ restricted = [for i in range(4) : cidrhost(local.netblocks.restricted, i)]
+ }
+ vm-startup-script = join("\n", [
+ "#! /bin/bash",
+ "apt-get update && apt-get install -y bash-completion dnsutils kubectl"
+ ])
+}
+
+data "google_netblock_ip_ranges" "dns-forwarders" {
+ range_type = "dns-forwarders"
+}
+
+data "google_netblock_ip_ranges" "private-googleapis" {
+ range_type = "private-googleapis"
+}
+
+data "google_netblock_ip_ranges" "restricted-googleapis" {
+ range_type = "restricted-googleapis"
+}
+
+################################################################################
+# Networking #
+################################################################################
+
+module "vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = var.project_id
+ name = "to-onprem"
+ subnets = [
+ {
+ ip_cidr_range = var.ip_ranges.gcp1
+ name = "subnet1"
+ region = var.region.gcp1
+ },
+ {
+ ip_cidr_range = var.ip_ranges.gcp2
+ name = "subnet2"
+ region = var.region.gcp2
+ }
+ ]
+}
+
+module "vpc-firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = var.project_id
+ network = module.vpc.name
+ default_rules_config = {
+ admin_ranges = values(var.ip_ranges)
+ ssh_ranges = var.ssh_source_ranges
+ }
+}
+
+module "vpn1" {
+ source = "../../../modules/net-vpn-dynamic"
+ project_id = var.project_id
+ region = var.region.gcp1
+ network = module.vpc.name
+ name = "to-onprem1"
+ router_config = { asn = var.bgp_asn.gcp1 }
+ tunnels = {
+ onprem = {
+ bgp_peer = {
+ address = local.bgp_interface_onprem1
+ asn = var.bgp_asn.onprem1
+ custom_advertise = {
+ all_subnets = true
+ all_vpc_subnets = false
+ all_peer_vpc_subnets = false
+ ip_ranges = {
+ (local.netblocks.dns) = "DNS resolvers"
+ (local.netblocks.private) = "private.gooogleapis.com"
+ (local.netblocks.restricted) = "restricted.gooogleapis.com"
+ } }
+ }
+ bgp_session_range = "${local.bgp_interface_gcp1}/30"
+ peer_ip = module.vm-onprem.external_ip
+ }
+ }
+}
+
+module "vpn2" {
+ source = "../../../modules/net-vpn-dynamic"
+ project_id = var.project_id
+ region = var.region.gcp2
+ network = module.vpc.name
+ name = "to-onprem2"
+ router_config = { asn = var.bgp_asn.gcp2 }
+ tunnels = {
+ onprem = {
+ bgp_peer = {
+ address = local.bgp_interface_onprem2
+ asn = var.bgp_asn.onprem2
+ custom_advertise = {
+ all_subnets = true
+ all_vpc_subnets = false
+ all_peer_vpc_subnets = false
+ ip_ranges = {
+ (local.netblocks.dns) = "DNS resolvers"
+ (local.netblocks.private) = "private.gooogleapis.com"
+ (local.netblocks.restricted) = "restricted.gooogleapis.com"
+ }
+ }
+ }
+ bgp_session_range = "${local.bgp_interface_gcp2}/30"
+ peer_ip = module.vm-onprem.external_ip
+ }
+ }
+}
+
+module "nat1" {
+ source = "../../../modules/net-cloudnat"
+ project_id = var.project_id
+ region = var.region.gcp1
+ name = "default"
+ router_create = false
+ router_name = module.vpn1.router_name
+}
+module "nat2" {
+ source = "../../../modules/net-cloudnat"
+ project_id = var.project_id
+ region = var.region.gcp2
+ name = "default"
+ router_create = false
+ router_name = module.vpn2.router_name
+}
+
+################################################################################
+# DNS #
+################################################################################
+
+module "dns-gcp" {
+ source = "../../../modules/dns"
+ project_id = var.project_id
+ type = "private"
+ name = "gcp-example"
+ domain = "gcp.example.org."
+ client_networks = [module.vpc.self_link]
+ recordsets = {
+ "A localhost" = { records = ["127.0.0.1"] }
+ "A test-1" = { records = [module.vm-test1.internal_ip] }
+ "A test-2" = { records = [module.vm-test2.internal_ip] }
+ }
+}
+
+module "dns-api" {
+ source = "../../../modules/dns"
+ project_id = var.project_id
+ type = "private"
+ name = "googleapis"
+ domain = "googleapis.com."
+ client_networks = [module.vpc.self_link]
+ recordsets = {
+ "CNAME *" = { records = ["private.googleapis.com."] }
+ "A private" = { records = local.vips.private }
+ "A restricted" = { records = local.vips.restricted }
+ }
+}
+
+module "dns-onprem" {
+ source = "../../../modules/dns"
+ project_id = var.project_id
+ type = "forwarding"
+ name = "onprem-example"
+ domain = "onprem.example.org."
+ client_networks = [module.vpc.self_link]
+ forwarders = {
+ "${cidrhost(var.ip_ranges.onprem, 3)}" = null
+ }
+}
+
+resource "google_dns_policy" "inbound" {
+ provider = google-beta
+ project = var.project_id
+ name = "gcp-inbound"
+ enable_inbound_forwarding = true
+ networks {
+ network_url = module.vpc.self_link
+ }
+}
+
+################################################################################
+# Test instance #
+################################################################################
+
+module "service-account-gce" {
+ source = "../../../modules/iam-service-account"
+ project_id = var.project_id
+ name = "gce-test"
+ iam_project_roles = {
+ (var.project_id) = [
+ "roles/logging.logWriter",
+ "roles/monitoring.metricWriter",
+ ]
+ }
+}
+
+module "vm-test1" {
+ source = "../../../modules/compute-vm"
+ project_id = var.project_id
+ zone = "${var.region.gcp1}-b"
+ name = "test-1"
+ network_interfaces = [{
+ network = module.vpc.self_link
+ subnetwork = module.vpc.subnet_self_links["${var.region.gcp1}/subnet1"]
+ }]
+ metadata = { startup-script = local.vm-startup-script }
+ service_account = module.service-account-gce.email
+ service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ tags = ["ssh"]
+}
+
+module "vm-test2" {
+ source = "../../../modules/compute-vm"
+ project_id = var.project_id
+ zone = "${var.region.gcp2}-b"
+ name = "test-2"
+ network_interfaces = [{
+ network = module.vpc.self_link
+ subnetwork = module.vpc.subnet_self_links["${var.region.gcp2}/subnet2"]
+ nat = false
+ addresses = null
+ }]
+ metadata = { startup-script = local.vm-startup-script }
+ service_account = module.service-account-gce.email
+ service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ tags = ["ssh"]
+}
+
+################################################################################
+# On prem #
+################################################################################
+
+module "config-onprem" {
+ source = "../../../modules/cloud-config-container/onprem"
+ config_variables = { dns_forwarder_address = var.dns_forwarder_address }
+ coredns_config = "${path.module}/assets/Corefile"
+ local_ip_cidr_range = var.ip_ranges.onprem
+ vpn_config = {
+ peer_ip = module.vpn1.address
+ peer_ip2 = module.vpn2.address
+ shared_secret = module.vpn1.random_secret
+ shared_secret2 = module.vpn2.random_secret
+ type = "dynamic"
+ }
+ vpn_dynamic_config = {
+ local_bgp_asn = var.bgp_asn.onprem1
+ local_bgp_address = local.bgp_interface_onprem1
+ peer_bgp_asn = var.bgp_asn.gcp1
+ peer_bgp_address = local.bgp_interface_gcp1
+ local_bgp_asn2 = var.bgp_asn.onprem2
+ local_bgp_address2 = local.bgp_interface_onprem2
+ peer_bgp_asn2 = var.bgp_asn.gcp2
+ peer_bgp_address2 = local.bgp_interface_gcp2
+ }
+}
+
+module "service-account-onprem" {
+ source = "../../../modules/iam-service-account"
+ project_id = var.project_id
+ name = "gce-onprem"
+ iam_project_roles = {
+ (var.project_id) = [
+ "roles/compute.viewer",
+ "roles/logging.logWriter",
+ "roles/monitoring.metricWriter",
+ ]
+ }
+}
+
+module "vm-onprem" {
+ source = "../../../modules/compute-vm"
+ project_id = var.project_id
+ zone = "${var.region.gcp1}-b"
+ instance_type = "f1-micro"
+ name = "onprem"
+ boot_disk = {
+ image = "ubuntu-os-cloud/ubuntu-1804-lts"
+ }
+ metadata = {
+ user-data = module.config-onprem.cloud_config
+ }
+ network_interfaces = [{
+ network = module.vpc.name
+ subnetwork = module.vpc.subnet_self_links["${var.region.gcp1}/subnet1"]
+ }]
+ service_account = module.service-account-onprem.email
+ service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ tags = ["ssh"]
+}
diff --git a/examples/networking/onprem-google-access-dns/outputs.tf b/blueprints/networking/__need_fixing/onprem-google-access-dns/outputs.tf
similarity index 100%
rename from examples/networking/onprem-google-access-dns/outputs.tf
rename to blueprints/networking/__need_fixing/onprem-google-access-dns/outputs.tf
diff --git a/examples/networking/onprem-google-access-dns/variables.tf b/blueprints/networking/__need_fixing/onprem-google-access-dns/variables.tf
similarity index 100%
rename from examples/networking/onprem-google-access-dns/variables.tf
rename to blueprints/networking/__need_fixing/onprem-google-access-dns/variables.tf
diff --git a/blueprints/networking/__need_fixing/onprem-google-access-dns/versions.tf b/blueprints/networking/__need_fixing/onprem-google-access-dns/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/networking/__need_fixing/onprem-google-access-dns/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/networking/decentralized-firewall/README.md b/blueprints/networking/decentralized-firewall/README.md
new file mode 100644
index 0000000000..64a3e41caa
--- /dev/null
+++ b/blueprints/networking/decentralized-firewall/README.md
@@ -0,0 +1,43 @@
+# Decentralized firewall management
+
+This example shows how a decentralized firewall management can be organized using the [firewall factory](../../factories/net-vpc-firewall-yaml/README.md).
+
+This approach is a good fit when Shared VPCs are used across multiple application/infrastructure teams. A central repository keeps environment/team
+specific folders with firewall definitions in `yaml` format.
+
+In the current blueprint multiple teams can define their [VPC Firewall Rules](https://cloud.google.com/vpc/docs/firewalls)
+for [dev](./firewall/dev) and [prod](./firewall/prod) environments using team specific subfolders. Rules defined in the
+[common](./firewall/common) folder are applied to both dev and prod environments.
+
+> **_NOTE:_** Common rules are meant to be used for situations where [hierarchical rules](https://cloud.google.com/vpc/docs/firewall-policies)
+do not map precisely to requirements (e.g. SA, etc.)
+
+This is the high level diagram:
+
+![High-level diagram](diagram.png "High-level diagram")
+
+The rules can be validated either using an automated process or a manual process (or a combination of
+the two). There is an blueprint of a YAML-based validator using [Yamale](https://github.com/23andMe/Yamale)
+in the [`validator/`](validator/) subdirectory, which can be integrated as part of a CI/CD pipeline.
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [billing_account_id](variables.tf#L15) | Billing account id used as default for new projects. | string
| ✓ | |
+| [prefix](variables.tf#L29) | Prefix used for resource names. | string
| ✓ | |
+| [root_node](variables.tf#L54) | Hierarchy node where projects will be created, 'organizations/org_id' or 'folders/folder_id'. | string
| ✓ | |
+| [ip_ranges](variables.tf#L20) | Subnet IP CIDR ranges. | map(string)
| | {…}
|
+| [project_services](variables.tf#L38) | Service APIs enabled by default in new projects. | list(string)
| | […]
|
+| [region](variables.tf#L48) | Region used. | string
| | "europe-west1"
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [fw_rules](outputs.tf#L15) | Firewall rules. | |
+| [projects](outputs.tf#L33) | Project ids. | |
+| [vpc](outputs.tf#L41) | Shared VPCs. | |
+
+
diff --git a/examples/networking/hub-and-spoke-peering/backend.tf.sample b/blueprints/networking/decentralized-firewall/backend.tf.sample
similarity index 100%
rename from examples/networking/hub-and-spoke-peering/backend.tf.sample
rename to blueprints/networking/decentralized-firewall/backend.tf.sample
diff --git a/examples/networking/decentralized-firewall/diagram.png b/blueprints/networking/decentralized-firewall/diagram.png
similarity index 100%
rename from examples/networking/decentralized-firewall/diagram.png
rename to blueprints/networking/decentralized-firewall/diagram.png
diff --git a/examples/networking/decentralized-firewall/firewall/common/common-egress.yaml b/blueprints/networking/decentralized-firewall/firewall/common/common-egress.yaml
similarity index 100%
rename from examples/networking/decentralized-firewall/firewall/common/common-egress.yaml
rename to blueprints/networking/decentralized-firewall/firewall/common/common-egress.yaml
diff --git a/examples/networking/decentralized-firewall/firewall/common/iap-access.yaml b/blueprints/networking/decentralized-firewall/firewall/common/iap-access.yaml
similarity index 100%
rename from examples/networking/decentralized-firewall/firewall/common/iap-access.yaml
rename to blueprints/networking/decentralized-firewall/firewall/common/iap-access.yaml
diff --git a/examples/networking/decentralized-firewall/firewall/common/lb-access.yaml b/blueprints/networking/decentralized-firewall/firewall/common/lb-access.yaml
similarity index 100%
rename from examples/networking/decentralized-firewall/firewall/common/lb-access.yaml
rename to blueprints/networking/decentralized-firewall/firewall/common/lb-access.yaml
diff --git a/examples/networking/decentralized-firewall/firewall/dev/app-1/app1-rules.yaml b/blueprints/networking/decentralized-firewall/firewall/dev/app-1/app1-rules.yaml
similarity index 100%
rename from examples/networking/decentralized-firewall/firewall/dev/app-1/app1-rules.yaml
rename to blueprints/networking/decentralized-firewall/firewall/dev/app-1/app1-rules.yaml
diff --git a/examples/networking/decentralized-firewall/firewall/dev/app-2/app2-rules.yaml b/blueprints/networking/decentralized-firewall/firewall/dev/app-2/app2-rules.yaml
similarity index 100%
rename from examples/networking/decentralized-firewall/firewall/dev/app-2/app2-rules.yaml
rename to blueprints/networking/decentralized-firewall/firewall/dev/app-2/app2-rules.yaml
diff --git a/examples/networking/decentralized-firewall/firewall/prod/app-1/app1-rules.yaml b/blueprints/networking/decentralized-firewall/firewall/prod/app-1/app1-rules.yaml
similarity index 100%
rename from examples/networking/decentralized-firewall/firewall/prod/app-1/app1-rules.yaml
rename to blueprints/networking/decentralized-firewall/firewall/prod/app-1/app1-rules.yaml
diff --git a/blueprints/networking/decentralized-firewall/main.tf b/blueprints/networking/decentralized-firewall/main.tf
new file mode 100644
index 0000000000..a05a104ffa
--- /dev/null
+++ b/blueprints/networking/decentralized-firewall/main.tf
@@ -0,0 +1,132 @@
+# 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.
+
+###############################################################################
+# Shared VPC Host projects #
+###############################################################################
+
+module "project-host-prod" {
+ source = "../../../modules/project"
+ parent = var.root_node
+ billing_account = var.billing_account_id
+ prefix = var.prefix
+ name = "prod-host"
+ services = var.project_services
+
+ shared_vpc_host_config = {
+ enabled = true
+ }
+}
+
+module "project-host-dev" {
+ source = "../../../modules/project"
+ parent = var.root_node
+ billing_account = var.billing_account_id
+ prefix = var.prefix
+ name = "dev-host"
+ services = var.project_services
+
+ shared_vpc_host_config = {
+ enabled = true
+ }
+}
+
+################################################################################
+# Networking #
+################################################################################
+
+module "vpc-prod" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project-host-prod.project_id
+ name = "prod-vpc"
+ subnets = [
+ {
+ ip_cidr_range = var.ip_ranges.prod
+ name = "prod"
+ region = var.region
+ }
+ ]
+}
+
+module "vpc-dev" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project-host-dev.project_id
+ name = "dev-vpc"
+ subnets = [
+ {
+ ip_cidr_range = var.ip_ranges.dev
+ name = "dev"
+ region = var.region
+ }
+ ]
+}
+
+###############################################################################
+# Private Google Access DNS #
+###############################################################################
+
+module "dns-api-prod" {
+ source = "../../../modules/dns"
+ project_id = module.project-host-prod.project_id
+ type = "private"
+ name = "googleapis"
+ domain = "googleapis.com."
+ client_networks = [module.vpc-prod.self_link]
+ recordsets = {
+ "CNAME *" = { records = ["private.googleapis.com."] }
+ }
+}
+
+module "dns-api-dev" {
+ source = "../../../modules/dns"
+ project_id = module.project-host-dev.project_id
+ type = "private"
+ name = "googleapis"
+ domain = "googleapis.com."
+ client_networks = [module.vpc-dev.self_link]
+ recordsets = {
+ "CNAME *" = { records = ["private.googleapis.com."] }
+ }
+}
+
+###############################################################################
+# Distributed Firewall #
+###############################################################################
+
+module "vpc-firewall-prod" {
+ source = "../../factories/net-vpc-firewall-yaml"
+
+ project_id = module.project-host-prod.project_id
+ network = module.vpc-prod.name
+ config_directories = [
+ "${path.module}/firewall/common",
+ "${path.module}/firewall/prod"
+ ]
+
+ # Enable Firewall Logging for the production fwl rules
+ log_config = {
+ metadata = "INCLUDE_ALL_METADATA"
+ }
+}
+
+module "vpc-firewall-dev" {
+ source = "../../factories/net-vpc-firewall-yaml"
+
+ project_id = module.project-host-dev.project_id
+ network = module.vpc-dev.name
+ config_directories = [
+ "${path.module}/firewall/common",
+ "${path.module}/firewall/dev"
+ ]
+}
diff --git a/examples/networking/decentralized-firewall/outputs.tf b/blueprints/networking/decentralized-firewall/outputs.tf
similarity index 100%
rename from examples/networking/decentralized-firewall/outputs.tf
rename to blueprints/networking/decentralized-firewall/outputs.tf
diff --git a/examples/networking/decentralized-firewall/validator/Dockerfile b/blueprints/networking/decentralized-firewall/validator/Dockerfile
similarity index 100%
rename from examples/networking/decentralized-firewall/validator/Dockerfile
rename to blueprints/networking/decentralized-firewall/validator/Dockerfile
diff --git a/examples/networking/decentralized-firewall/validator/README.md b/blueprints/networking/decentralized-firewall/validator/README.md
similarity index 100%
rename from examples/networking/decentralized-firewall/validator/README.md
rename to blueprints/networking/decentralized-firewall/validator/README.md
diff --git a/examples/networking/decentralized-firewall/validator/action.yml b/blueprints/networking/decentralized-firewall/validator/action.yml
similarity index 100%
rename from examples/networking/decentralized-firewall/validator/action.yml
rename to blueprints/networking/decentralized-firewall/validator/action.yml
diff --git a/examples/networking/decentralized-firewall/validator/firewallSchema.yaml b/blueprints/networking/decentralized-firewall/validator/firewallSchema.yaml
similarity index 100%
rename from examples/networking/decentralized-firewall/validator/firewallSchema.yaml
rename to blueprints/networking/decentralized-firewall/validator/firewallSchema.yaml
diff --git a/examples/networking/decentralized-firewall/validator/firewallSchemaAutoApprove.yaml b/blueprints/networking/decentralized-firewall/validator/firewallSchemaAutoApprove.yaml
similarity index 100%
rename from examples/networking/decentralized-firewall/validator/firewallSchemaAutoApprove.yaml
rename to blueprints/networking/decentralized-firewall/validator/firewallSchemaAutoApprove.yaml
diff --git a/examples/networking/decentralized-firewall/validator/firewallSchemaSettings.yaml b/blueprints/networking/decentralized-firewall/validator/firewallSchemaSettings.yaml
similarity index 100%
rename from examples/networking/decentralized-firewall/validator/firewallSchemaSettings.yaml
rename to blueprints/networking/decentralized-firewall/validator/firewallSchemaSettings.yaml
diff --git a/examples/networking/decentralized-firewall/validator/requirements.txt b/blueprints/networking/decentralized-firewall/validator/requirements.txt
similarity index 100%
rename from examples/networking/decentralized-firewall/validator/requirements.txt
rename to blueprints/networking/decentralized-firewall/validator/requirements.txt
diff --git a/examples/networking/decentralized-firewall/validator/validator.py b/blueprints/networking/decentralized-firewall/validator/validator.py
similarity index 100%
rename from examples/networking/decentralized-firewall/validator/validator.py
rename to blueprints/networking/decentralized-firewall/validator/validator.py
diff --git a/blueprints/networking/decentralized-firewall/variables.tf b/blueprints/networking/decentralized-firewall/variables.tf
new file mode 100644
index 0000000000..cf48e23c0d
--- /dev/null
+++ b/blueprints/networking/decentralized-firewall/variables.tf
@@ -0,0 +1,57 @@
+# 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.
+
+variable "billing_account_id" {
+ description = "Billing account id used as default for new projects."
+ type = string
+}
+
+variable "ip_ranges" {
+ description = "Subnet IP CIDR ranges."
+ type = map(string)
+ default = {
+ prod = "10.0.16.0/24"
+ dev = "10.0.32.0/24"
+ }
+}
+
+variable "prefix" {
+ description = "Prefix used for resource names."
+ type = string
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty."
+ }
+}
+
+variable "project_services" {
+ description = "Service APIs enabled by default in new projects."
+ type = list(string)
+ default = [
+ "container.googleapis.com",
+ "dns.googleapis.com",
+ "stackdriver.googleapis.com",
+ ]
+}
+
+variable "region" {
+ description = "Region used."
+ type = string
+ default = "europe-west1"
+}
+
+variable "root_node" {
+ description = "Hierarchy node where projects will be created, 'organizations/org_id' or 'folders/folder_id'."
+ type = string
+}
diff --git a/blueprints/networking/decentralized-firewall/versions.tf b/blueprints/networking/decentralized-firewall/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/networking/decentralized-firewall/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/networking/filtering-proxy-psc/README.md b/blueprints/networking/filtering-proxy-psc/README.md
new file mode 100644
index 0000000000..61631af587
--- /dev/null
+++ b/blueprints/networking/filtering-proxy-psc/README.md
@@ -0,0 +1,28 @@
+# Network filtering with Squid with isolated VPCs using Private Service Connect
+
+This blueprint shows how to deploy a filtering HTTP proxy to restrict Internet access. Here we show one way to do this using isolated VPCs and Private Service Connect:
+
+- The `app` subnet hosts the consumer VMs that will have their Internet access tightly controlled by a non-caching filtering forward proxy.
+- The `proxy` subnet hosts a Cloud NAT instance and a [Squid](http://www.squid-cache.org/) server.
+- The `psc` subnet is reserved for the Private Service Connect.
+
+The reason for using Privat Service Connect in this setup is to have a common proxy setup between all environments without having to share a VPC between projects. This allows us to enforce the `compute.vmExternalIpAccess` [organization policy](https://cloud.google.com/resource-manager/docs/organization-policy/org-policy-constraints), which prevents the service projects from having external IPs, thus forcing all outbound Internet connections through the proxy.
+
+To allow Internet connectivity to the proxy subnet, a Cloud NAT instance is configured to allow usage from [that subnet only](https://cloud.google.com/nat/docs/using-nat#specify_subnet_ranges_for_nat). All other subnets are not allowed to use the Cloud NAT instance.
+
+To simplify the usage of the proxy, a Cloud DNS private zone is created in each consumer VPC and the IP address of the proxy is exposed with the FQDN `proxy.internal`. In addition, system-wide `http_proxy` and `https_proxy` environment variables and an APT configuration are rolled out via a [startup script](startup.sh).
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [prefix](variables.tf#L44) | Prefix used for resource names. | string
| ✓ | |
+| [project_id](variables.tf#L70) | Project id used for all resources. | string
| ✓ | |
+| [allowed_domains](variables.tf#L17) | List of domains allowed by the squid proxy. | list(string)
| | […]
|
+| [cidrs](variables.tf#L28) | CIDR ranges for subnets. | map(string)
| | {…}
|
+| [nat_logging](variables.tf#L38) | Enables Cloud NAT logging if not null, value is one of 'ERRORS_ONLY', 'TRANSLATIONS_ONLY', 'ALL'. | string
| | "ERRORS_ONLY"
|
+| [project_create](variables.tf#L53) | Set to non null if project needs to be created. | object({…})
| | null
|
+| [region](variables.tf#L75) | Default region for resources. | string
| | "europe-west1"
|
+
+
diff --git a/blueprints/networking/filtering-proxy-psc/consumer.tf b/blueprints/networking/filtering-proxy-psc/consumer.tf
new file mode 100644
index 0000000000..5bcf033562
--- /dev/null
+++ b/blueprints/networking/filtering-proxy-psc/consumer.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.
+ */
+
+###############################################################################
+# Consumer project and VPC #
+###############################################################################
+
+module "vpc-consumer" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = "${var.prefix}-app"
+ subnets = [
+ {
+ name = "${var.prefix}-app"
+ ip_cidr_range = var.cidrs.app
+ region = var.region
+ }
+ ]
+}
+
+###############################################################################
+# Test VM #
+###############################################################################
+
+module "test-vm-consumer" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project.project_id
+ zone = "${var.region}-b"
+ name = "${var.prefix}-test-vm"
+ instance_type = "e2-micro"
+ tags = ["ssh"]
+ network_interfaces = [{
+ network = module.vpc-consumer.self_link
+ subnetwork = module.vpc-consumer.subnet_self_links["${var.region}/${var.prefix}-app"]
+ nat = false
+ addresses = null
+ }]
+ boot_disk = {
+ image = "debian-cloud/debian-10"
+ type = "pd-standard"
+ size = 10
+ }
+ service_account_create = true
+ metadata = {
+ startup-script = templatefile("${path.module}/startup.sh", { proxy_url = "http://proxy.internal:3128" })
+ }
+}
+
+###############################################################################
+# PSC Consuner #
+###############################################################################
+
+resource "google_compute_address" "psc_endpoint_address" {
+ name = "${var.prefix}-psc-proxy-address"
+ project = module.project.project_id
+ address_type = "INTERNAL"
+ subnetwork = module.vpc-consumer.subnet_self_links["${var.region}/${var.prefix}-app"]
+ region = var.region
+}
+
+resource "google_compute_forwarding_rule" "psc_ilb_consumer" {
+ name = "${var.prefix}-psc-proxy-fw-rule"
+ project = module.project.project_id
+ region = var.region
+ target = google_compute_service_attachment.service_attachment.id
+ load_balancing_scheme = ""
+ network = module.vpc-consumer.self_link
+ ip_address = google_compute_address.psc_endpoint_address.id
+}
+
+###############################################################################
+# DNS and Firewall #
+###############################################################################
+
+module "private-dns" {
+ source = "../../../modules/dns"
+ project_id = module.project.project_id
+ type = "private"
+ name = "${var.prefix}-internal"
+ domain = "internal."
+ client_networks = [module.vpc-consumer.self_link]
+ recordsets = {
+ "A squid" = { ttl = 60, records = [google_compute_address.psc_endpoint_address.address] }
+ "CNAME proxy" = { ttl = 3600, records = ["squid.internal."] }
+ }
+}
+
+module "firewall-consumer" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.project.project_id
+ network = module.vpc-consumer.name
+}
diff --git a/blueprints/networking/filtering-proxy-psc/main.tf b/blueprints/networking/filtering-proxy-psc/main.tf
new file mode 100644
index 0000000000..097097a74a
--- /dev/null
+++ b/blueprints/networking/filtering-proxy-psc/main.tf
@@ -0,0 +1,218 @@
+/**
+ * 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.
+ */
+
+###############################################################################
+# Host project and VPC resources #
+###############################################################################
+
+module "project" {
+ source = "../../../modules/project"
+ project_create = var.project_create != null
+ billing_account = try(var.project_create.billing_account, null)
+ parent = try(var.project_create.parent, null)
+ name = var.project_id
+ services = [
+ "dns.googleapis.com",
+ "compute.googleapis.com",
+ "logging.googleapis.com",
+ "monitoring.googleapis.com"
+ ]
+}
+
+module "vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = "${var.prefix}-vpc"
+ subnets = [
+ {
+ name = "proxy"
+ ip_cidr_range = var.cidrs.proxy
+ region = var.region
+ }
+ ]
+ subnets_psc = [
+ {
+ name = "psc"
+ ip_cidr_range = var.cidrs.psc
+ region = var.region
+ }
+ ]
+}
+
+module "firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.project.project_id
+ network = module.vpc.name
+ ingress_rules = {
+ allow-ingress-squid = {
+ description = "Allow squid ingress traffic"
+ source_ranges = [
+ var.cidrs.psc, "35.191.0.0/16", "130.211.0.0/22"
+ ]
+ targets = [module.service-account-squid.email]
+ use_service_accounts = true
+ rules = [{
+ protocol = "tcp"
+ ports = [3128]
+ }]
+ }
+ }
+}
+
+module "nat" {
+ source = "../../../modules/net-cloudnat"
+ project_id = module.project.project_id
+ region = var.region
+ name = "default"
+ router_network = module.vpc.name
+ config_source_subnets = "LIST_OF_SUBNETWORKS"
+ # 64512/11 = 5864 . 11 is the number of usable IPs in the proxy subnet
+ config_min_ports_per_vm = 5864
+ subnetworks = [
+ {
+ self_link = module.vpc.subnet_self_links["${var.region}/proxy"]
+ config_source_ranges = ["ALL_IP_RANGES"]
+ secondary_ranges = null
+ }
+ ]
+ logging_filter = var.nat_logging
+}
+
+###############################################################################
+# PSC resources #
+###############################################################################
+
+resource "google_compute_service_attachment" "service_attachment" {
+ name = "psc"
+ project = module.project.project_id
+ region = var.region
+ enable_proxy_protocol = true
+ connection_preference = "ACCEPT_MANUAL"
+ nat_subnets = [module.vpc.subnets_psc["${var.region}/psc"].self_link]
+ target_service = module.squid-ilb.forwarding_rule_self_link
+ consumer_accept_lists {
+ project_id_or_num = module.project.project_id
+ connection_limit = 10
+ }
+}
+
+###############################################################################
+# Squid resources #
+###############################################################################
+
+module "service-account-squid" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.project.project_id
+ name = "svc-squid"
+ iam_project_roles = {
+ (module.project.project_id) = [
+ "roles/logging.logWriter",
+ "roles/monitoring.metricWriter",
+ ]
+ }
+}
+
+module "cos-squid" {
+ source = "../../../modules/cloud-config-container/squid"
+ allow = var.allowed_domains
+ clients = [var.cidrs.app]
+ squid_config = "${path.module}/squid.conf"
+ config_variables = {
+ psc_cidr = var.cidrs.psc
+ }
+}
+
+module "squid-vm" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project.project_id
+ zone = "${var.region}-b"
+ name = "squid-vm"
+ instance_type = "e2-medium"
+ create_template = true
+ network_interfaces = [{
+ network = module.vpc.self_link
+ subnetwork = module.vpc.subnet_self_links["${var.region}/proxy"]
+ }]
+ boot_disk = {
+ image = "cos-cloud/cos-stable"
+ }
+ service_account = module.service-account-squid.email
+ service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ metadata = {
+ user-data = module.cos-squid.cloud_config
+ google-logging-enabled = true
+ }
+}
+
+module "squid-mig" {
+ source = "../../../modules/compute-mig"
+ project_id = module.project.project_id
+ location = "${var.region}-b"
+ name = "squid-mig"
+ instance_template = module.squid-vm.template.self_link
+ target_size = 1
+ auto_healing_policies = {
+ initial_delay_sec = 60
+ }
+ autoscaler_config = {
+ max_replicas = 10
+ min_replicas = 1
+ cooldown_period = 30
+ scaling_signals = {
+ cpu_utilization = {
+ target = 0.65
+ }
+ }
+ }
+ health_check_config = {
+ enable_logging = true
+ tcp = {
+ port = 3128
+ proxy_header = "PROXY_V1"
+ }
+ }
+ update_policy = {
+ minimal_action = "REPLACE"
+ type = "PROACTIVE"
+ max_surge = {
+ fixed = 3
+ }
+ min_ready_sec = 60
+ }
+}
+
+module "squid-ilb" {
+ source = "../../../modules/net-ilb"
+ project_id = module.project.project_id
+ region = var.region
+ name = "squid-ilb"
+ ports = [3128]
+ service_label = "squid-ilb"
+ vpc_config = {
+ network = module.vpc.self_link
+ subnetwork = module.vpc.subnet_self_links["${var.region}/proxy"]
+ }
+ backends = [{
+ group = module.squid-mig.group_manager.instance_group
+ }]
+ health_check_config = {
+ enable_logging = true
+ tcp = {
+ port = 3128
+ proxy_header = "PROXY_V1"
+ }
+ }
+}
diff --git a/blueprints/networking/filtering-proxy-psc/squid.conf b/blueprints/networking/filtering-proxy-psc/squid.conf
new file mode 100644
index 0000000000..85483c254f
--- /dev/null
+++ b/blueprints/networking/filtering-proxy-psc/squid.conf
@@ -0,0 +1,60 @@
+# bind to port 3128 and require PROXY protocol
+http_port 0.0.0.0:3128 require-proxy-header
+
+# only proxy, don't cache
+cache deny all
+
+# redirect all logs to /dev/stdout
+logfile_rotate 0
+cache_log stdio:/dev/stdout
+access_log stdio:/dev/stdout
+cache_store_log stdio:/dev/stdout
+
+pid_filename /var/run/squid/squid.pid
+
+acl ssl_ports port 443
+acl safe_ports port 80
+acl safe_ports port 443
+acl CONNECT method CONNECT
+acl to_metadata dst 169.254.169.254
+acl from_healthchecks src 130.211.0.0/22 35.191.0.0/16
+acl psc src ${psc_cidr}
+
+# read client CIDR ranges from clients.txt
+acl clients src "/etc/squid/clients.txt"
+
+# read allowed domains from allowlist.txt
+acl allowlist dstdomain "/etc/squid/allowlist.txt"
+
+# read denied domains from denylist.txt
+acl denylist dstdomain "/etc/squid/denylist.txt"
+
+# allow PROXY protocol from the PSC subnet
+proxy_protocol_access allow psc
+
+# allow PROXY protocol from the LB health checks
+proxy_protocol_access allow from_healthchecks
+
+# deny access to anything other than ports 80 and 443
+http_access deny !safe_ports
+
+# deny CONNECT if connection is not using ssl
+http_access deny CONNECT !ssl_ports
+
+# deny acccess to cachemgr
+http_access deny manager
+
+# deny access to localhost through the proxy
+http_access deny to_localhost
+
+# deny access to the local metadata server through the proxy
+http_access deny to_metadata
+
+# deny connection from allowed clients to any denied domains
+http_access deny clients denylist
+
+# allow connection from allowed clients only to the allowed domains
+http_access allow clients allowlist
+
+# deny everything else
+http_access ${default_action} all
diff --git a/blueprints/networking/filtering-proxy-psc/startup.sh b/blueprints/networking/filtering-proxy-psc/startup.sh
new file mode 100644
index 0000000000..bdc2b9cbcb
--- /dev/null
+++ b/blueprints/networking/filtering-proxy-psc/startup.sh
@@ -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.
+
+cat <string
| ✓ | |
+| [prefix](variables.tf#L52) | Prefix used for resource names. | string
| ✓ | |
+| [root_node](variables.tf#L67) | Root node for the new hierarchy, either 'organizations/org_id' or 'folders/folder_id'. | string
| ✓ | |
+| [allowed_domains](variables.tf#L17) | List of domains allowed by the squid proxy. | list(string)
| | […]
|
+| [cidrs](variables.tf#L31) | CIDR ranges for subnets. | map(string)
| | {…}
|
+| [mig](variables.tf#L40) | Enables the creation of an autoscaling managed instance group of squid instances. | bool
| | false
|
+| [nat_logging](variables.tf#L46) | Enables Cloud NAT logging if not null, value is one of 'ERRORS_ONLY', 'TRANSLATIONS_ONLY', 'ALL'. | string
| | "ERRORS_ONLY"
|
+| [region](variables.tf#L61) | Default region for resources. | string
| | "europe-west1"
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [squid-address](outputs.tf#L17) | IP address of the Squid proxy. | |
+
+
diff --git a/blueprints/networking/filtering-proxy/main.tf b/blueprints/networking/filtering-proxy/main.tf
new file mode 100644
index 0000000000..06efa81475
--- /dev/null
+++ b/blueprints/networking/filtering-proxy/main.tf
@@ -0,0 +1,270 @@
+/**
+ * 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 {
+ squid_address = (
+ var.mig
+ ? module.squid-ilb.0.forwarding_rule_address
+ : module.squid-vm.internal_ip
+ )
+}
+
+###############################################################################
+# Folder with network-related resources #
+###############################################################################
+
+module "folder-netops" {
+ source = "../../../modules/folder"
+ parent = var.root_node
+ name = "netops"
+}
+
+###############################################################################
+# Host project and shared VPC resources #
+###############################################################################
+
+module "project-host" {
+ source = "../../../modules/project"
+ billing_account = var.billing_account
+ name = "host"
+ parent = module.folder-netops.id
+ prefix = var.prefix
+ services = [
+ "compute.googleapis.com",
+ "dns.googleapis.com",
+ "logging.googleapis.com"
+ ]
+ shared_vpc_host_config = {
+ enabled = true
+ }
+}
+
+module "vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project-host.project_id
+ name = "vpc"
+ subnets = [
+ {
+ name = "apps"
+ ip_cidr_range = var.cidrs.apps
+ region = var.region
+ },
+ {
+ name = "proxy"
+ ip_cidr_range = var.cidrs.proxy
+ region = var.region
+ }
+ ]
+}
+
+module "firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.project-host.project_id
+ network = module.vpc.name
+ ingress_rules = {
+ allow-ingress-squid = {
+ description = "Allow squid ingress traffic"
+ source_ranges = [
+ var.cidrs.apps, "35.191.0.0/16", "130.211.0.0/22"
+ ]
+ targets = [module.service-account-squid.email]
+ use_service_accounts = true
+ rules = [{
+ protocol = "tcp"
+ ports = [3128]
+ }]
+ }
+ }
+}
+
+module "nat" {
+ source = "../../../modules/net-cloudnat"
+ project_id = module.project-host.project_id
+ region = var.region
+ name = "default"
+ router_network = module.vpc.name
+ config_source_subnets = "LIST_OF_SUBNETWORKS"
+ # 64512/11 = 5864 . 11 is the number of usable IPs in the proxy subnet
+ config_min_ports_per_vm = 5864
+ subnetworks = [
+ {
+ self_link = module.vpc.subnet_self_links["${var.region}/proxy"]
+ config_source_ranges = ["ALL_IP_RANGES"]
+ secondary_ranges = null
+ }
+ ]
+ logging_filter = var.nat_logging
+}
+
+module "private-dns" {
+ source = "../../../modules/dns"
+ project_id = module.project-host.project_id
+ type = "private"
+ name = "internal"
+ domain = "internal."
+ client_networks = [module.vpc.self_link]
+ recordsets = {
+ "A squid" = { ttl = 60, records = [local.squid_address] }
+ "CNAME proxy" = { ttl = 3600, records = ["squid.internal."] }
+ }
+}
+
+###############################################################################
+# Squid resources #
+###############################################################################
+
+module "service-account-squid" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.project-host.project_id
+ name = "svc-squid"
+ iam_project_roles = {
+ (module.project-host.project_id) = [
+ "roles/logging.logWriter",
+ "roles/monitoring.metricWriter",
+ ]
+ }
+}
+
+module "cos-squid" {
+ source = "../../../modules/cloud-config-container/squid"
+ allow = var.allowed_domains
+ clients = [var.cidrs.apps]
+}
+
+module "squid-vm" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project-host.project_id
+ zone = "${var.region}-b"
+ name = "squid-vm"
+ instance_type = "e2-medium"
+ create_template = var.mig
+ network_interfaces = [{
+ network = module.vpc.self_link
+ subnetwork = module.vpc.subnet_self_links["${var.region}/proxy"]
+ }]
+ boot_disk = {
+ image = "cos-cloud/cos-stable"
+ }
+ service_account = module.service-account-squid.email
+ service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ metadata = {
+ user-data = module.cos-squid.cloud_config
+ }
+}
+
+module "squid-mig" {
+ count = var.mig ? 1 : 0
+ source = "../../../modules/compute-mig"
+ project_id = module.project-host.project_id
+ location = "${var.region}-b"
+ name = "squid-mig"
+ instance_template = module.squid-vm.template.self_link
+ target_size = 1
+ auto_healing_policies = {
+ initial_delay_sec = 60
+ }
+ autoscaler_config = {
+ max_replicas = 10
+ min_replicas = 1
+ cooldown_period = 30
+ scaling_signals = {
+ cpu_utilization = {
+ target = 0.65
+ }
+ }
+ }
+ health_check_config = {
+ enable_logging = true
+ tcp = {
+ port = 3128
+ }
+ }
+}
+
+module "squid-ilb" {
+ count = var.mig ? 1 : 0
+ source = "../../../modules/net-ilb"
+ project_id = module.project-host.project_id
+ region = var.region
+ name = "squid-ilb"
+ ports = [3128]
+ service_label = "squid-ilb"
+ vpc_config = {
+ network = module.vpc.self_link
+ subnetwork = module.vpc.subnet_self_links["${var.region}/proxy"]
+ }
+ backends = [{
+ group = module.squid-mig.0.group_manager.instance_group
+ }]
+ health_check_config = {
+ enable_logging = true
+ tcp = {
+ port = 3128
+ }
+ }
+}
+
+###############################################################################
+# Service project #
+###############################################################################
+
+module "folder-apps" {
+ source = "../../../modules/folder"
+ parent = var.root_node
+ name = "apps"
+ org_policies = {
+ # prevent VMs with public IPs in the apps folder
+ "constraints/compute.vmExternalIpAccess" = {
+ deny = { all = true }
+ }
+ }
+}
+
+module "project-app" {
+ source = "../../../modules/project"
+ billing_account = var.billing_account
+ name = "app1"
+ parent = module.folder-apps.id
+ prefix = var.prefix
+ services = ["compute.googleapis.com"]
+ shared_vpc_service_config = {
+ host_project = module.project-host.project_id
+ service_identity_iam = {
+ "roles/compute.networkUser" = ["cloudservices"]
+ }
+ }
+}
+
+module "test-vm" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project-app.project_id
+ zone = "${var.region}-b"
+ name = "test-vm"
+ instance_type = "e2-micro"
+ tags = ["ssh"]
+ network_interfaces = [{
+ network = module.vpc.self_link
+ subnetwork = module.vpc.subnet_self_links["${var.region}/apps"]
+ nat = false
+ addresses = null
+ }]
+ boot_disk = {
+ image = "debian-cloud/debian-10"
+ type = "pd-standard"
+ size = 10
+ }
+ service_account_create = true
+}
diff --git a/examples/networking/filtering-proxy/outputs.tf b/blueprints/networking/filtering-proxy/outputs.tf
similarity index 100%
rename from examples/networking/filtering-proxy/outputs.tf
rename to blueprints/networking/filtering-proxy/outputs.tf
diff --git a/examples/networking/filtering-proxy/squid.png b/blueprints/networking/filtering-proxy/squid.png
similarity index 100%
rename from examples/networking/filtering-proxy/squid.png
rename to blueprints/networking/filtering-proxy/squid.png
diff --git a/blueprints/networking/filtering-proxy/variables.tf b/blueprints/networking/filtering-proxy/variables.tf
new file mode 100644
index 0000000000..a578eb1281
--- /dev/null
+++ b/blueprints/networking/filtering-proxy/variables.tf
@@ -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.
+ */
+
+variable "allowed_domains" {
+ description = "List of domains allowed by the squid proxy."
+ type = list(string)
+ default = [
+ ".google.com",
+ ".github.com"
+ ]
+}
+
+variable "billing_account" {
+ description = "Billing account id used as default for new projects."
+ type = string
+}
+
+variable "cidrs" {
+ description = "CIDR ranges for subnets."
+ type = map(string)
+ default = {
+ apps = "10.0.0.0/24"
+ proxy = "10.0.1.0/28"
+ }
+}
+
+variable "mig" {
+ description = "Enables the creation of an autoscaling managed instance group of squid instances."
+ type = bool
+ default = false
+}
+
+variable "nat_logging" {
+ description = "Enables Cloud NAT logging if not null, value is one of 'ERRORS_ONLY', 'TRANSLATIONS_ONLY', 'ALL'."
+ type = string
+ default = "ERRORS_ONLY"
+}
+
+variable "prefix" {
+ description = "Prefix used for resource names."
+ type = string
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty."
+ }
+}
+
+variable "region" {
+ description = "Default region for resources."
+ type = string
+ default = "europe-west1"
+}
+
+variable "root_node" {
+ description = "Root node for the new hierarchy, either 'organizations/org_id' or 'folders/folder_id'."
+ type = string
+}
diff --git a/blueprints/networking/filtering-proxy/versions.tf b/blueprints/networking/filtering-proxy/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/networking/filtering-proxy/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/networking/glb-and-armor/README.md b/blueprints/networking/glb-and-armor/README.md
new file mode 100644
index 0000000000..8385beab1e
--- /dev/null
+++ b/blueprints/networking/glb-and-armor/README.md
@@ -0,0 +1,139 @@
+# HTTP Load Balancer with Cloud Armor
+
+## Introduction
+
+This blueprint contains all necessary Terraform modules to build a multi-regional infrastructure with horizontally scalable managed instance group backends, HTTP load balancing and Google’s advanced WAF security tool (Cloud Armor) on top to securely deploy an application at global scale.
+
+This tutorial is general enough to fit in a variety of use-cases, from hosting a mobile app's backend to deploy proprietary workloads at scale.
+
+## Use cases
+
+Even though there are many ways to implement an architecture, some workloads require high compute power or specific licenses while making sure the services are secured by a managed service and highly available across multiple regions. An architecture consisting of Managed Instance Groups in multiple regions available through an HTTP Load Balancer with Cloud Armor enabled is suitable for such use-cases.
+
+This architecture caters to multiple workloads ranging from the ones requiring compliance with specific data access restrictions to compute-specific proprietary applications with specific licensing and OS requirements. Descriptions of some possible use-cases are as follows:
+
+* __Proprietary OS workloads__: Some applications require specific Operating systems (enterprise grade Linux distributions for example) with specific licensing requirements or low-level access to the kernel. In such cases, since the applications cannot be containerised and horizontal scaling is required, multi-region Managed Instance Group (MIG) with custom instance images are the ideal implementation.
+* __Industry-specific applications__: Other applications may require high compute power alongside a sophisticated layer of networking security. This architecture satisfies both these requirements by promising configurable compute power on the instances backed by various features offered by Cloud Armor such as traffic restriction, DDoS protection etc.
+* __Workloads requiring GDPR compliance__: Most applications require restricting data access and usage from outside a certain region (mostly to comply with data residency requirements). This architecture caters to such workloads as Cloud Armor allows you to lock access to your workloads from various fine-grained identifiers.
+* __Medical Queuing systems__: Another great example usage for this architecture will be applications requiring high compute power, availability and limited memory access requirements such as a medical queuing system.
+* __DDoS Protection and WAF__: Applications and workloads exposed to the internet expose themselves to the risk of DDoS attacks. While L3/L4 and protocol based attacks are handled at Google’s edge, L7 attacks can still be effective with botnets. A setup of an external Cloud Load Balancer with Cloud Armor and appropriate WAF rules can mitigate such attacks.
+* __Geofencing__: If you want to restrict content served on your application due to licensing restrictions (similar to OTT content in the US), Geofencing allows you to create a virtual perimeter to stop the service from being accessed outside the region. The architecture of using a Cloud Load Balancer with Cloud Armor enables you to implement geofencing around your applications and services.
+
+## Architecture
+
++ +The main components that we would be setting up are (to learn more about these products, click on the hyperlinks): + +* [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. +* [Cloud Load Balancer](https://cloud.google.com/load-balancing) - When your app usage spikes, it is important to scale, optimize and secure the app. Cloud Load Balancing is a fully distributed solution that balances user traffic to multiple backends to avoid congestion, reduce latency and increase security. Some important features it offers that we use here are: + * Single global anycast IP and autoscaling - CLB acts as a frontend to all your backend instances across all regions. It provides cross-region load balancing, automatic multi-region failover and scales to support increase in resources. + * Global Forwarding Rule - To route traffic to different regions, global load balancers use global forwarding rules, which bind the global IP address and a single target proxy. + * Target Proxy - For external HTTP(S) load balancers, proxies route incoming requests to a URL map. This is essentially how you can handle the connections. + * URL Map - URL Maps are used to route requests to a backend service based on the rules that you define for the host and path of an incoming URL. + * Backend Service - A Backend Service defines CLB distributes traffic. The backend service configuration consists of a set of values - protocols to connect to backends, session settings, health checks and timeouts. + * Health Check - Health check is a method provided to determine if the corresponding backends respond to traffic. Health checks connect to backends on a configurable, periodic basis. Each connection attempt is called a probe. Google Cloud records the success or failure of each probe. +* [Firewall Rules](https://cloud.google.com/vpc/docs/firewalls) - Firewall rules let you allow or deny connections to or from your VM instances based on a configuration you specify. +* [Managed Instance Groups (MIG)](https://cloud.google.com/compute/docs/instance-groups) - Instance group is a collection of VM instances that you can manage as a single entity. MIGs allow you to operate apps and workloads on multiple identical VMs. You can also leverage the various features like autoscaling, autohealing, regional / multi-zone deployments. + +## Costs + +Pricing Estimates - We have created a sample estimate based on some usage we see from new startups looking to scale. This estimate would give you an idea of how much this deployment would essentially cost per month at this scale and you extend it to the scale you further prefer. Here's the [link](https://cloud.google.com/products/calculator/#id=3105bbf2-4ee0-4289-978e-9ab6855d37ed). + +## Setup + +This solution assumes you already have a project created and set up where you wish to host these resources. If not, and you would like for the project to create a new project as well, please refer to the [github repository](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/tree/master/blueprints/data-solutions/gcs-to-bq-with-least-privileges) for instructions. + +### Prerequisites + +* Have an [organization](https://cloud.google.com/resource-manager/docs/creating-managing-organization) set up in Google cloud. +* Have a [billing account](https://cloud.google.com/billing/docs/how-to/manage-billing-account) set up. +* Have 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). + +### Roles & Permissions + +In order to spin up this architecture, you will need to be a user with the “__Project owner__” [IAM](https://cloud.google.com/iam) role on the existing 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 + +#### Step 1: Cloning the repository + +Click on the button below, sign in if required and when the prompt appears, click on “confirm”. + +[![Open Cloudshell](../../../assets/images/cloud-shell-button.png)](https://goo.gle/GoCloudArmor) + +This will clone the repository to your cloud shell and a screen like this one will appear: + +![cloud_shell](cloud_shell.png) + +Before we deploy the architecture, you will need the following information: + +* The __project ID__. + +#### Step 2: Deploying the resources + +1. After cloning the repo, and going through the prerequisites, head back to the cloud shell editor. +2. Make sure you’re in the following directory. if not, you can change your directory to it via the ‘cd’ command: + + cloudshell_open/cloud-foundation-fabric/blueprints/cloud-operations/glb_and_armor + +3. Run the following command to initialize the terraform working directory: + + terraform init + +4. Copy the following command into a console and replace __[my-project-id]__ with your project’s ID. Then run the following command to run the terraform script and create all relevant resources for this architecture: + + terraform apply -var project_id=[my-project-id] + +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. + +__Congratulations__! You have successfully deployed an HTTP Load Balancer with two Managed Instance Group backends and Cloud Armor security. + +## Testing your architecture + +1. Connect to the siege VM using SSH (from Cloud Console or CLI) and run the following command: + + siege -c 250 -t150s http://$LB_IP + +2. In the Cloud Console, on the Navigation menu, click __Network Services > Load balancing__. +3. Click __Backends__, then click __http-backend__ and navigate to __http-lb__ +4. Click on the __Monitoring__ tab. +5. Monitor the Frontend Location (Total inbound traffic) between North America and the two backends for 2 to 3 minutes. At first, traffic should just be directed to __us-east1-mig__ but as the RPS increases, traffic is also directed to __europe-west1-mig__. This demonstrates that by default traffic is forwarded to the closest backend but if the load is very high, traffic can be distributed across the backends. +6. Now, to test the IP deny-listing, rerun terraform as follows: + + terraform apply -var project_id=my-project-id -var enforce_security_policy=true + +This, applies a security policy to denylist the IP address of the siege VM + +7. To test this, from the siege VM run the following command and verify that you get a __403 Forbidden__ error code back. + + curl http://$LB_IP + +## Cleaning up your environment + +The easiest way to remove all the deployed resources is to run the following command in Cloud Shell: + + terraform destroy + +The above command will delete the associated resources so there will be no billable charges made afterwards. + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [prefix](variables.tf#L23) | Prefix used for resource names. |
string
| ✓ | |
+| [project_id](variables.tf#L41) | Identifier of the project. | string
| ✓ | |
+| [enforce_security_policy](variables.tf#L17) | Enforce security policy. | bool
| | true
|
+| [project_create](variables.tf#L32) | Parameters for the creation of the new project. | object({…})
| | null
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [glb_ip_address](outputs.tf#L18) | Load balancer IP address. | |
+| [vm_siege_external_ip](outputs.tf#L23) | Siege VM external IP address. | |
+
+
diff --git a/blueprints/networking/glb-and-armor/architecture.png b/blueprints/networking/glb-and-armor/architecture.png
new file mode 100644
index 0000000000..64b1e186d5
Binary files /dev/null and b/blueprints/networking/glb-and-armor/architecture.png differ
diff --git a/blueprints/networking/glb-and-armor/cloud_shell.png b/blueprints/networking/glb-and-armor/cloud_shell.png
new file mode 100644
index 0000000000..21bb72e018
Binary files /dev/null and b/blueprints/networking/glb-and-armor/cloud_shell.png differ
diff --git a/blueprints/networking/glb-and-armor/main.tf b/blueprints/networking/glb-and-armor/main.tf
new file mode 100644
index 0000000000..9f33572cbb
--- /dev/null
+++ b/blueprints/networking/glb-and-armor/main.tf
@@ -0,0 +1,245 @@
+/**
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+locals {
+ prefix = var.prefix == null ? "" : "${var.prefix}-"
+}
+
+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
+ )
+ prefix = var.project_create == null ? null : var.prefix
+ name = var.project_id
+ services = [
+ "compute.googleapis.com"
+ ]
+ project_create = var.project_create != null
+}
+
+
+module "vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = "${var.prefix}-vpc"
+ subnets = [
+ {
+ ip_cidr_range = "10.0.1.0/24"
+ name = "subnet-ew1"
+ region = "europe-west1"
+ },
+ {
+ ip_cidr_range = "10.0.2.0/24"
+ name = "subnet-ue1"
+ region = "us-east1"
+ },
+ {
+ ip_cidr_range = "10.0.3.0/24"
+ name = "subnet-uw1"
+ region = "us-west1"
+ }
+ ]
+}
+
+module "firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.project.project_id
+ network = module.vpc.name
+}
+
+module "nat_ew1" {
+ source = "../../../modules/net-cloudnat"
+ project_id = module.project.project_id
+ region = "europe-west1"
+ name = "${var.prefix}-nat-eu1"
+ router_network = module.vpc.name
+}
+
+module "nat_ue1" {
+ source = "../../../modules/net-cloudnat"
+ project_id = module.project.project_id
+ region = "us-east1"
+ name = "${var.prefix}-nat-ue1"
+ router_network = module.vpc.name
+}
+
+module "instance_template_ew1" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project.project_id
+ zone = "europe-west1-b"
+ name = "${var.prefix}-europe-west1-template"
+ instance_type = "n1-standard-2"
+ network_interfaces = [{
+ network = module.vpc.self_link
+ subnetwork = module.vpc.subnet_self_links["europe-west1/subnet-ew1"]
+ }]
+ boot_disk = {
+ image = "projects/debian-cloud/global/images/family/debian-11"
+ }
+ metadata = {
+ startup-script-url = "gs://cloud-training/gcpnet/httplb/startup.sh"
+ }
+ create_template = true
+ tags = [
+ "http-server"
+ ]
+}
+
+module "instance_template_ue1" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project.project_id
+ zone = "us-east1-b"
+ name = "${var.prefix}-us-east1-template"
+ network_interfaces = [{
+ network = module.vpc.self_link
+ subnetwork = module.vpc.subnet_self_links["us-east1/subnet-ue1"]
+ }]
+ boot_disk = {
+ image = "projects/debian-cloud/global/images/family/debian-11"
+ }
+ metadata = {
+ startup-script-url = "gs://cloud-training/gcpnet/httplb/startup.sh"
+ }
+ create_template = true
+ tags = [
+ "http-server"
+ ]
+}
+
+module "vm_siege" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project.project_id
+ zone = "us-west1-c"
+ name = "siege-vm"
+ instance_type = "n1-standard-2"
+ network_interfaces = [{
+ network = module.vpc.self_link
+ subnetwork = module.vpc.subnet_self_links["us-west1/subnet-uw1"]
+ nat = true
+ }]
+ boot_disk = {
+ image = "projects/debian-cloud/global/images/family/debian-11"
+ }
+ metadata = {
+ startup-script = <string
| ✓ | |
+| [project_id](variables.tf#L69) | Project id used for all resources. | string
| ✓ | |
+| [ip_ranges](variables.tf#L15) | IP CIDR ranges. | map(string)
| | {…}
|
+| [ip_secondary_ranges](variables.tf#L25) | Secondary IP CIDR ranges. | map(string)
| | {…}
|
+| [private_service_ranges](variables.tf#L43) | Private service IP CIDR ranges. | map(string)
| | {…}
|
+| [project_create](variables.tf#L51) | Set to non null if project needs to be created. | object({…})
| | null
|
+| [region](variables.tf#L74) | VPC region. | string
| | "europe-west1"
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [project](outputs.tf#L15) | Project id. | |
+| [vms](outputs.tf#L20) | GCE VMs. | |
+
+
diff --git a/examples/networking/hub-and-spoke-vpn/backend.tf.sample b/blueprints/networking/hub-and-spoke-peering/backend.tf.sample
similarity index 100%
rename from examples/networking/hub-and-spoke-vpn/backend.tf.sample
rename to blueprints/networking/hub-and-spoke-peering/backend.tf.sample
diff --git a/examples/networking/hub-and-spoke-peering/diagram-network.png b/blueprints/networking/hub-and-spoke-peering/diagram-network.png
similarity index 100%
rename from examples/networking/hub-and-spoke-peering/diagram-network.png
rename to blueprints/networking/hub-and-spoke-peering/diagram-network.png
diff --git a/examples/networking/hub-and-spoke-peering/diagram.png b/blueprints/networking/hub-and-spoke-peering/diagram.png
similarity index 100%
rename from examples/networking/hub-and-spoke-peering/diagram.png
rename to blueprints/networking/hub-and-spoke-peering/diagram.png
diff --git a/blueprints/networking/hub-and-spoke-peering/main.tf b/blueprints/networking/hub-and-spoke-peering/main.tf
new file mode 100644
index 0000000000..999858941a
--- /dev/null
+++ b/blueprints/networking/hub-and-spoke-peering/main.tf
@@ -0,0 +1,330 @@
+# 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.
+
+locals {
+ vm-instances = [
+ module.vm-hub.instance,
+ module.vm-spoke-1.instance,
+ module.vm-spoke-2.instance
+ ]
+ vm-startup-script = join("\n", [
+ "#! /bin/bash",
+ "apt-get update && apt-get install -y bash-completion dnsutils kubectl"
+ ])
+}
+
+###############################################################################
+# project #
+###############################################################################
+
+module "project" {
+ source = "../../../modules/project"
+ project_create = var.project_create != null
+ billing_account = try(var.project_create.billing_account, null)
+ oslogin = try(var.project_create.oslogin, false)
+ parent = try(var.project_create.parent, null)
+ name = var.project_id
+ services = [
+ "compute.googleapis.com",
+ "container.googleapis.com"
+ ]
+}
+
+################################################################################
+# Hub networking #
+################################################################################
+
+module "vpc-hub" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = "${var.prefix}-hub"
+ subnets = [
+ {
+ ip_cidr_range = var.ip_ranges.hub
+ name = "${var.prefix}-hub-1"
+ region = var.region
+ }
+ ]
+}
+
+module "nat-hub" {
+ source = "../../../modules/net-cloudnat"
+ project_id = module.project.project_id
+ region = var.region
+ name = "${var.prefix}-hub"
+ router_name = "${var.prefix}-hub"
+ router_network = module.vpc-hub.self_link
+}
+
+module "vpc-hub-firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = var.project_id
+ network = module.vpc-hub.name
+ default_rules_config = {
+ admin_ranges = values(var.ip_ranges)
+ }
+}
+
+################################################################################
+# Spoke 1 networking #
+################################################################################
+
+module "vpc-spoke-1" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = "${var.prefix}-spoke-1"
+ subnets = [
+ {
+ ip_cidr_range = var.ip_ranges.spoke-1
+ name = "${var.prefix}-spoke-1-1"
+ region = var.region
+ }
+ ]
+}
+
+module "vpc-spoke-1-firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.project.project_id
+ network = module.vpc-spoke-1.name
+ default_rules_config = {
+ admin_ranges = values(var.ip_ranges)
+ }
+}
+
+module "nat-spoke-1" {
+ source = "../../../modules/net-cloudnat"
+ project_id = module.project.project_id
+ region = var.region
+ name = "${var.prefix}-spoke-1"
+ router_name = "${var.prefix}-spoke-1"
+ router_network = module.vpc-spoke-1.self_link
+}
+
+module "hub-to-spoke-1-peering" {
+ source = "../../../modules/net-vpc-peering"
+ local_network = module.vpc-hub.self_link
+ peer_network = module.vpc-spoke-1.self_link
+ export_local_custom_routes = true
+ export_peer_custom_routes = false
+}
+
+################################################################################
+# Spoke 2 networking #
+################################################################################
+
+module "vpc-spoke-2" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = "${var.prefix}-spoke-2"
+ subnets = [
+ {
+ ip_cidr_range = var.ip_ranges.spoke-2
+ name = "${var.prefix}-spoke-2-1"
+ region = var.region
+ secondary_ip_ranges = {
+ pods = var.ip_secondary_ranges.spoke-2-pods
+ services = var.ip_secondary_ranges.spoke-2-services
+ }
+ }
+ ]
+}
+
+module "vpc-spoke-2-firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.project.project_id
+ network = module.vpc-spoke-2.name
+ default_rules_config = {
+ admin_ranges = values(var.ip_ranges)
+ }
+}
+
+module "nat-spoke-2" {
+ source = "../../../modules/net-cloudnat"
+ project_id = module.project.project_id
+ region = var.region
+ name = "${var.prefix}-spoke-2"
+ router_name = "${var.prefix}-spoke-2"
+ router_network = module.vpc-spoke-2.self_link
+}
+
+module "hub-to-spoke-2-peering" {
+ source = "../../../modules/net-vpc-peering"
+ local_network = module.vpc-hub.self_link
+ peer_network = module.vpc-spoke-2.self_link
+ export_local_custom_routes = true
+ export_peer_custom_routes = false
+ depends_on = [module.hub-to-spoke-1-peering]
+}
+
+################################################################################
+# Test VMs #
+################################################################################
+
+module "vm-hub" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project.project_id
+ zone = "${var.region}-b"
+ name = "${var.prefix}-hub"
+ network_interfaces = [{
+ network = module.vpc-hub.self_link
+ subnetwork = module.vpc-hub.subnet_self_links["${var.region}/${var.prefix}-hub-1"]
+ nat = false
+ addresses = null
+ }]
+ metadata = { startup-script = local.vm-startup-script }
+ service_account = module.service-account-gce.email
+ service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ tags = ["ssh"]
+}
+
+module "vm-spoke-1" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project.project_id
+ zone = "${var.region}-b"
+ name = "${var.prefix}-spoke-1"
+ network_interfaces = [{
+ network = module.vpc-spoke-1.self_link
+ subnetwork = module.vpc-spoke-1.subnet_self_links["${var.region}/${var.prefix}-spoke-1-1"]
+ nat = false
+ addresses = null
+ }]
+ metadata = { startup-script = local.vm-startup-script }
+ service_account = module.service-account-gce.email
+ service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ tags = ["ssh"]
+}
+
+module "vm-spoke-2" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project.project_id
+ zone = "${var.region}-b"
+ name = "${var.prefix}-spoke-2"
+ network_interfaces = [{
+ network = module.vpc-spoke-2.self_link
+ subnetwork = module.vpc-spoke-2.subnet_self_links["${var.region}/${var.prefix}-spoke-2-1"]
+ nat = false
+ addresses = null
+ }]
+ metadata = { startup-script = local.vm-startup-script }
+ service_account = module.service-account-gce.email
+ service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ tags = ["ssh"]
+}
+
+module "service-account-gce" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.project.project_id
+ name = "${var.prefix}-gce-test"
+ iam_project_roles = {
+ (var.project_id) = [
+ "roles/container.developer",
+ "roles/logging.logWriter",
+ "roles/monitoring.metricWriter",
+ ]
+ }
+}
+
+################################################################################
+# GKE #
+################################################################################
+
+module "cluster-1" {
+ source = "../../../modules/gke-cluster"
+ name = "${var.prefix}-cluster-1"
+ project_id = module.project.project_id
+ location = "${var.region}-b"
+ vpc_config = {
+ network = module.vpc-spoke-2.self_link
+ subnetwork = module.vpc-spoke-2.subnet_self_links["${var.region}/${var.prefix}-spoke-2-1"]
+ master_authorized_ranges = {
+ for name, range in var.ip_ranges : name => range
+ }
+ master_ipv4_cidr_block = var.private_service_ranges.spoke-2-cluster-1
+ }
+ max_pods_per_node = 32
+ labels = {
+ environment = "test"
+ }
+ private_cluster_config = {
+ enable_private_endpoint = true
+ master_global_access = true
+ peering_config = {
+ export_routes = true
+ import_routes = false
+ }
+ }
+}
+
+module "cluster-1-nodepool-1" {
+ source = "../../../modules/gke-nodepool"
+ name = "${var.prefix}-nodepool-1"
+ project_id = module.project.project_id
+ location = module.cluster-1.location
+ cluster_name = module.cluster-1.name
+ service_account = {
+ email = module.service-account-gke-node.email
+ }
+}
+
+# roles assigned via this module use non-authoritative IAM bindings at the
+# project level, with no risk of conflicts with pre-existing roles
+
+module "service-account-gke-node" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.project.project_id
+ name = "${var.prefix}-gke-node"
+ iam_project_roles = {
+ (var.project_id) = [
+ "roles/logging.logWriter", "roles/monitoring.metricWriter",
+ ]
+ }
+}
+
+################################################################################
+# GKE peering VPN #
+################################################################################
+
+module "vpn-hub" {
+ source = "../../../modules/net-vpn-static"
+ project_id = module.project.project_id
+ region = var.region
+ network = module.vpc-hub.name
+ name = "${var.prefix}-hub"
+ remote_ranges = values(var.private_service_ranges)
+ tunnels = {
+ spoke-2 = {
+ peer_ip = module.vpn-spoke-2.address
+ shared_secret = ""
+ traffic_selectors = { local = ["0.0.0.0/0"], remote = null }
+ }
+ }
+}
+
+module "vpn-spoke-2" {
+ source = "../../../modules/net-vpn-static"
+ project_id = module.project.project_id
+ region = var.region
+ network = module.vpc-spoke-2.name
+ name = "${var.prefix}-spoke-2"
+ # use an aggregate of the remote ranges, so as to be less specific than the
+ # routes exchanged via peering
+ remote_ranges = ["10.0.0.0/8"]
+ tunnels = {
+ hub = {
+ peer_ip = module.vpn-hub.address
+ shared_secret = module.vpn-hub.random_secret
+ traffic_selectors = { local = ["0.0.0.0/0"], remote = null }
+ }
+ }
+}
diff --git a/examples/networking/hub-and-spoke-peering/outputs.tf b/blueprints/networking/hub-and-spoke-peering/outputs.tf
similarity index 100%
rename from examples/networking/hub-and-spoke-peering/outputs.tf
rename to blueprints/networking/hub-and-spoke-peering/outputs.tf
diff --git a/blueprints/networking/hub-and-spoke-peering/variables.tf b/blueprints/networking/hub-and-spoke-peering/variables.tf
new file mode 100644
index 0000000000..803b739642
--- /dev/null
+++ b/blueprints/networking/hub-and-spoke-peering/variables.tf
@@ -0,0 +1,78 @@
+# 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.
+
+variable "ip_ranges" {
+ description = "IP CIDR ranges."
+ type = map(string)
+ default = {
+ hub = "10.0.0.0/24"
+ spoke-1 = "10.0.16.0/24"
+ spoke-2 = "10.0.32.0/24"
+ }
+}
+
+variable "ip_secondary_ranges" {
+ description = "Secondary IP CIDR ranges."
+ type = map(string)
+ default = {
+ spoke-2-pods = "10.128.0.0/18"
+ spoke-2-services = "172.16.0.0/24"
+ }
+}
+
+variable "prefix" {
+ description = "Prefix used for resource names."
+ type = string
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty."
+ }
+}
+
+variable "private_service_ranges" {
+ description = "Private service IP CIDR ranges."
+ type = map(string)
+ default = {
+ spoke-2-cluster-1 = "192.168.0.0/28"
+ }
+}
+
+variable "project_create" {
+ description = "Set to non null if project needs to be created."
+ type = object({
+ billing_account = string
+ oslogin = bool
+ parent = string
+ })
+ default = null
+ validation {
+ condition = (
+ var.project_create == null
+ ? true
+ : can(regex("(organizations|folders)/[0-9]+", var.project_create.parent))
+ )
+ error_message = "Project parent must be of the form folders/folder_id or organizations/organization_id."
+ }
+}
+
+variable "project_id" {
+ description = "Project id used for all resources."
+ type = string
+}
+
+variable "region" {
+ description = "VPC region."
+ type = string
+ default = "europe-west1"
+}
diff --git a/blueprints/networking/hub-and-spoke-peering/versions.tf b/blueprints/networking/hub-and-spoke-peering/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/networking/hub-and-spoke-peering/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/networking/hub-and-spoke-vpn/README.md b/blueprints/networking/hub-and-spoke-vpn/README.md
new file mode 100644
index 0000000000..5f596142f0
--- /dev/null
+++ b/blueprints/networking/hub-and-spoke-vpn/README.md
@@ -0,0 +1,103 @@
+# Hub and Spoke via VPN
+
+This blueprint creates a simple **Hub and Spoke VPN** setup, where the VPC network connects satellite locations (spokes) through a single intermediary location (hub) via [IPsec HA VPN](https://cloud.google.com/network-connectivity/docs/vpn/concepts/overview#ha-vpn).
+
+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/).
+
+This is the high level diagram of this blueprint:
+
+![High-level diagram](diagram.png "High-level diagram")
+
+## Managed resources and services
+
+This sample creates several distinct groups of resources:
+
+- one VPC for each hub and each spoke
+- one set of firewall rules for each VPC
+- one HA VPN gateway with two tunnels and one Cloud Router for each spoke
+- two HA VPN gateways with two tunnels and a shared Cloud Routers for the hub
+- one DNS private zone in the hub
+- one DNS peering zone and one DNS private zone in each spoke
+- one test instance for the hub each spoke
+
+## Prerequisites
+
+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 provided project needs a valid billing account, the Compute and DNS APIs are enabled by the blueprint.
+
+You can easily create such a project by commenting turning on project creation in the project module contained in `main.tf`, as shown in this snippet:
+
+```hcl
+module "project" {
+ 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"
+ services = [
+ "compute.googleapis.com",
+ "dns.googleapis.com"
+ ]
+}
+
+# tftest skip
+```
+
+## Testing
+
+Once the blueprint is up, you can quickly test features by logging in to one of the test VMs:
+
+```bash
+gcloud compute ssh hs-ha-lnd-test-r1
+# test DNS resolution of the landing zone
+ping test-r1.example.com
+# test DNS resolution of the prod zone, and prod reachability
+ping test-r1.prod.example.com
+# test DNS resolution of the dev zone, and dev reachability via global routing
+ping test-r2.dev.example.com
+```
+
+
+
+
+## Files
+
+| name | description | modules |
+|---|---|---|
+| [main.tf](./main.tf) | Module-level locals and resources. | compute-vm
· project
|
+| [net-dev.tf](./net-dev.tf) | Development spoke VPC. | dns
· net-vpc
· net-vpc-firewall
|
+| [net-landing.tf](./net-landing.tf) | Landing hub VPC. | dns
· net-vpc
· net-vpc-firewall
|
+| [net-prod.tf](./net-prod.tf) | Production spoke VPC. | dns
· net-vpc
· net-vpc-firewall
|
+| [outputs.tf](./outputs.tf) | Module outputs. | |
+| [variables.tf](./variables.tf) | Module variables. | |
+| [versions.tf](./versions.tf) | Version pins. | |
+| [vpn-dev-r1.tf](./vpn-dev-r1.tf) | Landing to Development VPN for region 1. | net-vpn-ha
|
+| [vpn-prod-r1.tf](./vpn-prod-r1.tf) | Landing to Production VPN for region 1. | net-vpn-ha
|
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [prefix](variables.tf#L34) | Prefix used for resource names. | string
| ✓ | |
+| [project_id](variables.tf#L52) | Project id for all resources. | string
| ✓ | |
+| [ip_ranges](variables.tf#L15) | Subnet IP CIDR ranges. | map(string)
| | {…}
|
+| [ip_secondary_ranges](variables.tf#L28) | Subnet secondary ranges. | map(map(string))
| | {}
|
+| [project_create_config](variables.tf#L43) | Populate with billing account id to trigger project creation. | object({…})
| | null
|
+| [regions](variables.tf#L57) | VPC regions. | map(string)
| | {…}
|
+| [vpn_configs](variables.tf#L66) | VPN configurations. | map(object({…}))
| | {…}
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [subnets](outputs.tf#L15) | Subnet details. | |
+| [vms](outputs.tf#L39) | GCE VMs. | |
+
+
diff --git a/examples/networking/ilb-next-hop/backend.tf.sample b/blueprints/networking/hub-and-spoke-vpn/backend.tf.sample
similarity index 100%
rename from examples/networking/ilb-next-hop/backend.tf.sample
rename to blueprints/networking/hub-and-spoke-vpn/backend.tf.sample
diff --git a/blueprints/networking/hub-and-spoke-vpn/diagram.png b/blueprints/networking/hub-and-spoke-vpn/diagram.png
new file mode 100644
index 0000000000..6bddab62e5
Binary files /dev/null and b/blueprints/networking/hub-and-spoke-vpn/diagram.png differ
diff --git a/blueprints/networking/hub-and-spoke-vpn/main.tf b/blueprints/networking/hub-and-spoke-vpn/main.tf
new file mode 100644
index 0000000000..8810a71d6f
--- /dev/null
+++ b/blueprints/networking/hub-and-spoke-vpn/main.tf
@@ -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
+#
+# 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.
+
+# enable services in the project used
+
+module "project" {
+ source = "../../..//modules/project"
+ name = var.project_id
+ parent = try(var.project_create_config.parent, null)
+ billing_account = try(var.project_create_config.billing_account_id, null)
+ project_create = try(var.project_create_config.billing_account_id, null) != null
+ services = [
+ "compute.googleapis.com",
+ "dns.googleapis.com"
+ ]
+}
+
+# test VM in landing region 1
+
+module "landing-r1-vm" {
+ source = "../../../modules/compute-vm"
+ project_id = var.project_id
+ name = "${var.prefix}-lnd-test-r1"
+ zone = "${var.regions.r1}-b"
+ network_interfaces = [{
+ network = module.landing-vpc.self_link
+ subnetwork = module.landing-vpc.subnet_self_links["${var.regions.r1}/${var.prefix}-lnd-0"]
+ nat = false
+ addresses = null
+ }]
+ tags = ["ssh"]
+}
+
+# test VM in prod region 1
+
+module "prod-r1-vm" {
+ source = "../../../modules/compute-vm"
+ project_id = var.project_id
+ name = "${var.prefix}-prd-test-r1"
+ zone = "${var.regions.r1}-b"
+ network_interfaces = [{
+ network = module.prod-vpc.self_link
+ subnetwork = module.prod-vpc.subnet_self_links["${var.regions.r1}/${var.prefix}-prd-0"]
+ nat = false
+ addresses = null
+ }]
+ tags = ["ssh"]
+}
+
+# test VM in dev region 1
+
+module "dev-r2-vm" {
+ source = "../../../modules/compute-vm"
+ project_id = var.project_id
+ name = "${var.prefix}-dev-test-r2"
+ zone = "${var.regions.r2}-b"
+ network_interfaces = [{
+ network = module.dev-vpc.self_link
+ subnetwork = module.dev-vpc.subnet_self_links["${var.regions.r2}/${var.prefix}-dev-0"]
+ nat = false
+ addresses = null
+ }]
+ tags = ["ssh"]
+}
diff --git a/blueprints/networking/hub-and-spoke-vpn/net-dev.tf b/blueprints/networking/hub-and-spoke-vpn/net-dev.tf
new file mode 100644
index 0000000000..f7cf84dba2
--- /dev/null
+++ b/blueprints/networking/hub-and-spoke-vpn/net-dev.tf
@@ -0,0 +1,71 @@
+# 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.
+
+# tfdoc:file:description Development spoke VPC.
+
+module "dev-vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = var.project_id
+ name = "${var.prefix}-dev"
+ subnets = [
+ {
+ ip_cidr_range = var.ip_ranges.dev-0-r1
+ name = "${var.prefix}-dev-0"
+ region = var.regions.r1
+ secondary_ip_ranges = try(
+ var.ip_secondary_ranges.dev-0-r1, {}
+ )
+ },
+ {
+ ip_cidr_range = var.ip_ranges.dev-0-r2
+ name = "${var.prefix}-dev-0"
+ region = var.regions.r2
+ secondary_ip_ranges = try(
+ var.ip_secondary_ranges.dev-0-r2, {}
+ )
+ }
+ ]
+}
+
+module "dev-firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = var.project_id
+ network = module.dev-vpc.name
+ default_rules_config = {
+ admin_ranges = values(var.ip_ranges)
+ }
+}
+
+module "dev-dns-peering" {
+ source = "../../../modules/dns"
+ project_id = var.project_id
+ type = "peering"
+ name = "${var.prefix}-example-com-dev-peering"
+ domain = "example.com."
+ client_networks = [module.dev-vpc.self_link]
+ peer_network = module.landing-vpc.self_link
+}
+
+module "dev-dns-zone" {
+ source = "../../../modules/dns"
+ project_id = var.project_id
+ type = "private"
+ name = "${var.prefix}-dev-example-com"
+ domain = "dev.example.com."
+ client_networks = [module.landing-vpc.self_link]
+ recordsets = {
+ "A localhost" = { records = ["127.0.0.1"] }
+ "A test-r2" = { records = [module.dev-r2-vm.internal_ip] }
+ }
+}
diff --git a/blueprints/networking/hub-and-spoke-vpn/net-landing.tf b/blueprints/networking/hub-and-spoke-vpn/net-landing.tf
new file mode 100644
index 0000000000..31fdb85619
--- /dev/null
+++ b/blueprints/networking/hub-and-spoke-vpn/net-landing.tf
@@ -0,0 +1,61 @@
+# 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.
+
+# tfdoc:file:description Landing hub VPC.
+
+module "landing-vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = var.project_id
+ name = "${var.prefix}-lnd"
+ subnets = [
+ {
+ ip_cidr_range = var.ip_ranges.land-0-r1
+ name = "${var.prefix}-lnd-0"
+ region = var.regions.r1
+ secondary_ip_ranges = try(
+ var.ip_secondary_ranges.land-0-r1, {}
+ )
+ },
+ {
+ ip_cidr_range = var.ip_ranges.land-0-r2
+ name = "${var.prefix}-lnd-0"
+ region = var.regions.r2
+ secondary_ip_ranges = try(
+ var.ip_secondary_ranges.land-0-r2, {}
+ )
+ }
+ ]
+}
+
+module "landing-firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = var.project_id
+ network = module.landing-vpc.name
+ default_rules_config = {
+ admin_ranges = values(var.ip_ranges)
+ }
+}
+
+module "landing-dns-zone" {
+ source = "../../../modules/dns"
+ project_id = var.project_id
+ type = "private"
+ name = "${var.prefix}-example-com"
+ domain = "example.com."
+ client_networks = [module.landing-vpc.self_link]
+ recordsets = {
+ "A localhost" = { records = ["127.0.0.1"] }
+ "A test-r1" = { records = [module.landing-r1-vm.internal_ip] }
+ }
+}
diff --git a/blueprints/networking/hub-and-spoke-vpn/net-prod.tf b/blueprints/networking/hub-and-spoke-vpn/net-prod.tf
new file mode 100644
index 0000000000..ec3260215e
--- /dev/null
+++ b/blueprints/networking/hub-and-spoke-vpn/net-prod.tf
@@ -0,0 +1,71 @@
+# 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.
+
+# tfdoc:file:description Production spoke VPC.
+
+module "prod-vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = var.project_id
+ name = "${var.prefix}-prd"
+ subnets = [
+ {
+ ip_cidr_range = var.ip_ranges.prod-0-r1
+ name = "${var.prefix}-prd-0"
+ region = var.regions.r1
+ secondary_ip_ranges = try(
+ var.ip_secondary_ranges.prod-0-r1, {}
+ )
+ },
+ {
+ ip_cidr_range = var.ip_ranges.prod-0-r2
+ name = "${var.prefix}-prd-0"
+ region = var.regions.r2
+ secondary_ip_ranges = try(
+ var.ip_secondary_ranges.prod-0-r2, {}
+ )
+ }
+ ]
+}
+
+module "prod-firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = var.project_id
+ network = module.prod-vpc.name
+ default_rules_config = {
+ admin_ranges = values(var.ip_ranges)
+ }
+}
+
+module "prod-dns-peering" {
+ source = "../../../modules/dns"
+ project_id = var.project_id
+ type = "peering"
+ name = "${var.prefix}-example-com-prd-peering"
+ domain = "example.com."
+ client_networks = [module.prod-vpc.self_link]
+ peer_network = module.landing-vpc.self_link
+}
+
+module "prod-dns-zone" {
+ source = "../../../modules/dns"
+ project_id = var.project_id
+ type = "private"
+ name = "${var.prefix}-prd-example-com"
+ domain = "prd.example.com."
+ client_networks = [module.landing-vpc.self_link]
+ recordsets = {
+ "A localhost" = { records = ["127.0.0.1"] }
+ "A test-r1" = { records = [module.prod-r1-vm.internal_ip] }
+ }
+}
diff --git a/blueprints/networking/hub-and-spoke-vpn/outputs.tf b/blueprints/networking/hub-and-spoke-vpn/outputs.tf
new file mode 100644
index 0000000000..befd20ff83
--- /dev/null
+++ b/blueprints/networking/hub-and-spoke-vpn/outputs.tf
@@ -0,0 +1,45 @@
+# 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.
+
+output "subnets" {
+ description = "Subnet details."
+ value = {
+ dev = {
+ for k, v in module.dev-vpc.subnets : k => {
+ id = v.id
+ ip_cidr_range = v.ip_cidr_range
+ }
+ }
+ landing = {
+ for k, v in module.landing-vpc.subnets : k => {
+ id = v.id
+ ip_cidr_range = v.ip_cidr_range
+ }
+ }
+ prod = {
+ for k, v in module.prod-vpc.subnets : k => {
+ id = v.id
+ ip_cidr_range = v.ip_cidr_range
+ }
+ }
+ }
+}
+
+output "vms" {
+ description = "GCE VMs."
+ value = {
+ for mod in [module.landing-r1-vm, module.dev-r2-vm, module.prod-r1-vm] :
+ mod.instance.name => mod.internal_ip
+ }
+}
diff --git a/blueprints/networking/hub-and-spoke-vpn/variables.tf b/blueprints/networking/hub-and-spoke-vpn/variables.tf
new file mode 100644
index 0000000000..90fbd3593a
--- /dev/null
+++ b/blueprints/networking/hub-and-spoke-vpn/variables.tf
@@ -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
+#
+# 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.
+
+variable "ip_ranges" {
+ description = "Subnet IP CIDR ranges."
+ type = map(string)
+ default = {
+ land-0-r1 = "10.0.0.0/24"
+ land-0-r2 = "10.0.8.0/24"
+ dev-0-r1 = "10.0.16.0/24"
+ dev-0-r2 = "10.0.24.0/24"
+ prod-0-r1 = "10.0.32.0/24"
+ prod-0-r2 = "10.0.40.0/24"
+ }
+}
+
+variable "ip_secondary_ranges" {
+ description = "Subnet secondary ranges."
+ type = map(map(string))
+ default = {}
+}
+
+variable "prefix" {
+ description = "Prefix used for resource names."
+ type = string
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty."
+ }
+}
+
+variable "project_create_config" {
+ description = "Populate with billing account id to trigger project creation."
+ type = object({
+ billing_account_id = string
+ parent_id = string
+ })
+ default = null
+}
+
+variable "project_id" {
+ description = "Project id for all resources."
+ type = string
+}
+
+variable "regions" {
+ description = "VPC regions."
+ type = map(string)
+ default = {
+ r1 = "europe-west1"
+ r2 = "europe-west4"
+ }
+}
+
+variable "vpn_configs" {
+ description = "VPN configurations."
+ type = map(object({
+ asn = number
+ custom_ranges = map(string)
+ }))
+ default = {
+ land-r1 = {
+ asn = 64513
+ custom_ranges = {
+ "10.0.0.0/8" = "internal default"
+ }
+ }
+ dev-r1 = {
+ asn = 64514
+ custom_ranges = null
+ }
+ prod-r1 = {
+ asn = 64515
+ custom_ranges = null
+ }
+ }
+}
diff --git a/blueprints/networking/hub-and-spoke-vpn/versions.tf b/blueprints/networking/hub-and-spoke-vpn/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/networking/hub-and-spoke-vpn/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/networking/hub-and-spoke-vpn/vpn-dev-r1.tf b/blueprints/networking/hub-and-spoke-vpn/vpn-dev-r1.tf
new file mode 100644
index 0000000000..4d1236bb8b
--- /dev/null
+++ b/blueprints/networking/hub-and-spoke-vpn/vpn-dev-r1.tf
@@ -0,0 +1,87 @@
+# 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.
+
+# tfdoc:file:description Landing to Development VPN for region 1.
+
+module "landing-to-dev-vpn-r1" {
+ source = "../../../modules/net-vpn-ha"
+ project_id = var.project_id
+ network = module.landing-vpc.self_link
+ region = var.regions.r1
+ name = "${var.prefix}-lnd-to-dev-r1"
+ # router is created and managed by the production VPN module
+ # so we don't configure advertisements here
+ router_config = {
+ create = false
+ name = "${var.prefix}-lnd-vpn-r1"
+ asn = 64514
+ }
+ peer_gateway = { gcp = module.dev-to-landing-vpn-r1.self_link }
+ tunnels = {
+ 0 = {
+ bgp_peer = {
+ address = "169.254.2.2"
+ asn = var.vpn_configs.dev-r1.asn
+ }
+ bgp_session_range = "169.254.2.1/30"
+ vpn_gateway_interface = 0
+ }
+ 1 = {
+ bgp_peer = {
+ address = "169.254.2.6"
+ asn = var.vpn_configs.dev-r1.asn
+ }
+ bgp_session_range = "169.254.2.5/30"
+ vpn_gateway_interface = 1
+ }
+ }
+}
+
+module "dev-to-landing-vpn-r1" {
+ source = "../../../modules/net-vpn-ha"
+ project_id = var.project_id
+ network = module.dev-vpc.self_link
+ region = var.regions.r1
+ name = "${var.prefix}-dev-to-lnd-r1"
+ router_config = {
+ name = "${var.prefix}-dev-vpn-r1"
+ asn = var.vpn_configs.dev-r1.asn
+ custom_advertise = {
+ all_subnets = false
+ ip_ranges = coalesce(var.vpn_configs.dev-r1.custom_ranges, {})
+ mode = "CUSTOM"
+ }
+ }
+ peer_gateway = { gcp = module.landing-to-dev-vpn-r1.self_link }
+ tunnels = {
+ 0 = {
+ bgp_peer = {
+ address = "169.254.2.1"
+ asn = var.vpn_configs.land-r1.asn
+ }
+ bgp_session_range = "169.254.2.2/30"
+ shared_secret = module.landing-to-dev-vpn-r1.random_secret
+ vpn_gateway_interface = 0
+ }
+ 1 = {
+ bgp_peer = {
+ address = "169.254.2.5"
+ asn = var.vpn_configs.land-r1.asn
+ }
+ bgp_session_range = "169.254.2.6/30"
+ shared_secret = module.landing-to-dev-vpn-r1.random_secret
+ vpn_gateway_interface = 1
+ }
+ }
+}
diff --git a/blueprints/networking/hub-and-spoke-vpn/vpn-prod-r1.tf b/blueprints/networking/hub-and-spoke-vpn/vpn-prod-r1.tf
new file mode 100644
index 0000000000..8e633686fa
--- /dev/null
+++ b/blueprints/networking/hub-and-spoke-vpn/vpn-prod-r1.tf
@@ -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
+#
+# 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 Landing to Production VPN for region 1.
+
+module "landing-to-prod-vpn-r1" {
+ source = "../../../modules/net-vpn-ha"
+ project_id = var.project_id
+ network = module.landing-vpc.self_link
+ region = var.regions.r1
+ name = "${var.prefix}-lnd-to-prd-r1"
+ router_config = {
+ name = "${var.prefix}-lnd-vpn-r1"
+ asn = var.vpn_configs.land-r1.asn
+ custom_advertise = {
+ all_subnets = false
+ ip_ranges = coalesce(var.vpn_configs.land-r1.custom_ranges, {})
+ }
+ }
+ peer_gateway = { gcp = module.prod-to-landing-vpn-r1.self_link }
+ tunnels = {
+ 0 = {
+ bgp_peer = {
+ address = "169.254.0.2"
+ asn = var.vpn_configs.prod-r1.asn
+ }
+ bgp_session_range = "169.254.0.1/30"
+ vpn_gateway_interface = 0
+ }
+ 1 = {
+ bgp_peer = {
+ address = "169.254.0.6"
+ asn = var.vpn_configs.prod-r1.asn
+ }
+ bgp_session_range = "169.254.0.5/30"
+ vpn_gateway_interface = 1
+ }
+ }
+}
+
+module "prod-to-landing-vpn-r1" {
+ source = "../../../modules/net-vpn-ha"
+ project_id = var.project_id
+ network = module.prod-vpc.self_link
+ region = var.regions.r1
+ name = "${var.prefix}-prd-to-lnd-r1"
+ router_config = {
+ name = "${var.prefix}-prd-vpn-r1"
+ asn = var.vpn_configs.prod-r1.asn
+ # the router is managed here but shared with the dev VPN
+ custom_advertise = {
+ all_subnets = false
+ ip_ranges = coalesce(var.vpn_configs.prod-r1.custom_ranges, {})
+ }
+ }
+ peer_gateway = { gcp = module.landing-to-prod-vpn-r1.self_link }
+ tunnels = {
+ 0 = {
+ bgp_peer = {
+ address = "169.254.0.1"
+ asn = var.vpn_configs.land-r1.asn
+ }
+ bgp_session_range = "169.254.0.2/30"
+ shared_secret = module.landing-to-prod-vpn-r1.random_secret
+ vpn_gateway_interface = 0
+ }
+ 1 = {
+ bgp_peer = {
+ address = "169.254.0.5"
+ asn = var.vpn_configs.land-r1.asn
+ }
+ bgp_session_range = "169.254.0.6/30"
+ shared_secret = module.landing-to-prod-vpn-r1.random_secret
+ vpn_gateway_interface = 1
+ }
+ }
+}
diff --git a/blueprints/networking/ilb-next-hop/README.md b/blueprints/networking/ilb-next-hop/README.md
new file mode 100644
index 0000000000..c3091558ca
--- /dev/null
+++ b/blueprints/networking/ilb-next-hop/README.md
@@ -0,0 +1,88 @@
+# Internal Load Balancer as Next Hop
+
+This blueprint bootstraps a minimal infrastructure for testing [ILB as next hop](https://cloud.google.com/load-balancing/docs/internal/ilb-next-hop-overview), using simple Linux gateway VMS between two VPCs to emulate virtual appliances.
+
+The following diagram shows the resources created by this blueprint
+
+![High-level diagram](diagram.png "High-level diagram")
+
+Two ILBs are configured on the primary and secondary interfaces of gateway VMs with active health checks, but only a single one is used as next hop by default to simplify testing. The second (right-side) VPC has default routes that point to the gateway VMs, to also use the right-side ILB as next hop set the `ilb_right_enable` variable to `true`.
+
+## Testing
+
+This setup can be used to test and verify new ILB features like [forwards all protocols on ILB as next hops](https://cloud.google.com/load-balancing/docs/internal/ilb-next-hop-overview#all-traffic) and [symmetric hashing](https://cloud.google.com/load-balancing/docs/internal/ilb-next-hop-overview#symmetric-hashing), using simple `curl` and `ping` tests on clients. To make this practical, test VMs on both VPCs have `nginx` pre-installed and active on port 80.
+
+On the gateways, `iftop` and `tcpdump` are installed by default to quickly monitor traffic passing forwarded across VPCs.
+
+Session affinity on the ILB backend services can be changed using `gcloud compute backend-services update` on each of the ILBs, or by setting the `ilb_session_affinity` variable to update both ILBs.
+
+Simple `/root/start.sh` and `/root/stop.sh` scripts are pre-installed on both gateways to configure `iptables` so that health check requests are rejected and re-enabled, to quickly simulate removing instances from the ILB backends.
+
+Some scenarios to test:
+
+- short-lived connections with session affinity set to the default of `NONE`, then to `CLIENT_IP`
+- long-lived connections, failing health checks on the active gateway while the connection is active
+
+### Useful commands
+
+Basic commands to SSH to VMs and monitor backend health are provided in the Terraform outputs, and they already match input variables so that names, zones, etc. are correct. Other testing commands are provided below, adjust names to match your setup.
+
+Create a large file on a destination VM (eg `ilb-test-vm-right-1`) to test long-running connections.
+
+```bash
+dd if=/dev/zero of=/var/www/html/test.txt bs=10M count=100 status=progress
+```
+
+Run curl from a source VM (eg `ilb-test-vm-left-1`) to send requests to a destination VM artificially slowing traffic.
+
+```bash
+curl -0 --output /dev/null --limit-rate 10k 10.0.1.3/test.txt
+```
+
+Monitor traffic from a source VM (eg `ilb-test-vm-left-1`) on the gateways.
+
+```bash
+iftop -n -F 10.0.0.3/32
+```
+
+Poll summary health status for a backend.
+
+```bash
+watch '\
+ gcloud compute backend-services get-health ilb-test-ilb-right \
+ --region europe-west1 \
+ --flatten status.healthStatus \
+ --format "value(status.healthStatus.ipAddress, status.healthStatus.healthState)" \
+'
+```
+
+A sample testing session using `tmux`:
+
+
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [prefix](variables.tf#L38) | Prefix used for resource names. | string
| ✓ | |
+| [project_id](variables.tf#L53) | Existing project id. | string
| ✓ | |
+| [ilb_right_enable](variables.tf#L17) | Route right to left traffic through ILB. | bool
| | false
|
+| [ilb_session_affinity](variables.tf#L23) | Session affinity configuration for ILBs. | string
| | "CLIENT_IP"
|
+| [ip_ranges](variables.tf#L29) | IP CIDR ranges used for VPC subnets. | map(string)
| | {…}
|
+| [project_create](variables.tf#L47) | Create project instead of using an existing one. | bool
| | false
|
+| [region](variables.tf#L58) | Region used for resources. | string
| | "europe-west1"
|
+| [zones](variables.tf#L64) | Zone suffixes used for instances. | list(string)
| | ["b", "c"]
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [addresses](outputs.tf#L17) | IP addresses. | |
+| [backend_health_left](outputs.tf#L28) | Command-line health status for left ILB backends. | |
+| [backend_health_right](outputs.tf#L38) | Command-line health status for right ILB backends. | |
+| [ssh_gw](outputs.tf#L48) | Command-line login to gateway VMs. | |
+| [ssh_vm_left](outputs.tf#L56) | Command-line login to left VMs. | |
+| [ssh_vm_right](outputs.tf#L64) | Command-line login to right VMs. | |
+
+
diff --git a/examples/networking/ilb-next-hop/assets/gw.yaml b/blueprints/networking/ilb-next-hop/assets/gw.yaml
similarity index 100%
rename from examples/networking/ilb-next-hop/assets/gw.yaml
rename to blueprints/networking/ilb-next-hop/assets/gw.yaml
diff --git a/examples/networking/onprem-google-access-dns/backend.tf.sample b/blueprints/networking/ilb-next-hop/backend.tf.sample
similarity index 100%
rename from examples/networking/onprem-google-access-dns/backend.tf.sample
rename to blueprints/networking/ilb-next-hop/backend.tf.sample
diff --git a/examples/networking/ilb-next-hop/diagram.png b/blueprints/networking/ilb-next-hop/diagram.png
similarity index 100%
rename from examples/networking/ilb-next-hop/diagram.png
rename to blueprints/networking/ilb-next-hop/diagram.png
diff --git a/blueprints/networking/ilb-next-hop/gateways.tf b/blueprints/networking/ilb-next-hop/gateways.tf
new file mode 100644
index 0000000000..3a1dcffb2e
--- /dev/null
+++ b/blueprints/networking/ilb-next-hop/gateways.tf
@@ -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.
+ */
+
+module "gw" {
+ source = "../../../modules/compute-vm"
+ for_each = local.zones
+ project_id = module.project.project_id
+ zone = each.value
+ name = "${var.prefix}-gw-${each.key}"
+ instance_type = "f1-micro"
+
+ boot_disk = {
+ image = "projects/ubuntu-os-cloud/global/images/family/ubuntu-2004-lts",
+ type = "pd-ssd",
+ size = 10
+ }
+
+ network_interfaces = [
+ {
+ network = module.vpc-left.self_link
+ subnetwork = values(module.vpc-left.subnet_self_links)[0],
+ nat = false,
+ addresses = null
+ },
+ {
+ network = module.vpc-right.self_link
+ subnetwork = values(module.vpc-right.subnet_self_links)[0],
+ nat = false,
+ addresses = null
+ }
+ ]
+ tags = ["ssh"]
+ can_ip_forward = true
+ metadata = {
+ user-data = templatefile("${path.module}/assets/gw.yaml", {
+ gw_right = cidrhost(var.ip_ranges.right, 1)
+ ip_cidr_right = var.ip_ranges.right
+ })
+ }
+ service_account = try(
+ module.service-accounts.emails["${var.prefix}-gce-vm"], null
+ )
+ service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ group = { named_ports = null }
+}
+
+module "ilb-left" {
+ source = "../../../modules/net-ilb"
+ project_id = module.project.project_id
+ region = var.region
+ name = "${var.prefix}-ilb-left"
+ vpc_config = {
+ network = module.vpc-left.self_link
+ subnetwork = values(module.vpc-left.subnet_self_links)[0]
+ }
+ address = local.addresses.ilb-left
+ backend_service_config = {
+ session_affinity = var.ilb_session_affinity
+ }
+ backends = [for z, mod in module.gw : {
+ group = mod.group.self_link
+ }]
+ health_check_config = {
+ enable_logging = true
+ tcp = {
+ port = 22
+ }
+ }
+}
+
+module "ilb-right" {
+ source = "../../../modules/net-ilb"
+ project_id = module.project.project_id
+ region = var.region
+ name = "${var.prefix}-ilb-right"
+ vpc_config = {
+ network = module.vpc-right.self_link
+ subnetwork = values(module.vpc-right.subnet_self_links)[0]
+ }
+ address = local.addresses.ilb-right
+ backend_service_config = {
+ session_affinity = var.ilb_session_affinity
+ }
+ backends = [for z, mod in module.gw : {
+ group = mod.group.self_link
+ }]
+ health_check_config = {
+ enable_logging = true
+ tcp = {
+ port = 22
+ }
+ }
+}
diff --git a/blueprints/networking/ilb-next-hop/main.tf b/blueprints/networking/ilb-next-hop/main.tf
new file mode 100644
index 0000000000..0f7cfe0e5c
--- /dev/null
+++ b/blueprints/networking/ilb-next-hop/main.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.
+ */
+
+locals {
+ addresses = {
+ for k, v in module.addresses.internal_addresses :
+ trimprefix(k, "${var.prefix}-") => v.address
+ }
+ zones = { for z in var.zones : z => "${var.region}-${z}" }
+}
+
+module "project" {
+ source = "../../../modules/project"
+ name = var.project_id
+ project_create = var.project_create
+ services = [
+ "compute.googleapis.com",
+ "dns.googleapis.com",
+ ]
+}
+
+module "service-accounts" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.project.project_id
+ name = "${var.prefix}-gce-vm"
+ iam_project_roles = {
+ (var.project_id) = [
+ "roles/logging.logWriter",
+ "roles/monitoring.metricWriter",
+ ]
+ }
+}
+
+module "addresses" {
+ source = "../../../modules/net-address"
+ project_id = module.project.project_id
+ internal_addresses = {
+ "${var.prefix}-ilb-left" = {
+ region = var.region,
+ subnetwork = values(module.vpc-left.subnet_self_links)[0]
+ },
+ "${var.prefix}-ilb-right" = {
+ region = var.region,
+ subnetwork = values(module.vpc-right.subnet_self_links)[0]
+ }
+ }
+}
diff --git a/blueprints/networking/ilb-next-hop/outputs.tf b/blueprints/networking/ilb-next-hop/outputs.tf
new file mode 100644
index 0000000000..c00282ae8e
--- /dev/null
+++ b/blueprints/networking/ilb-next-hop/outputs.tf
@@ -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.
+ */
+
+output "addresses" {
+ description = "IP addresses."
+ value = {
+ gw = [for z, mod in module.gw : mod.internal_ip]
+ ilb-left = module.ilb-left.forwarding_rule_address
+ ilb-right = module.ilb-right.forwarding_rule_address
+ vm-left = [for z, mod in module.vm-left : mod.internal_ip]
+ vm-right = [for z, mod in module.vm-right : mod.internal_ip]
+ }
+}
+
+output "backend_health_left" {
+ description = "Command-line health status for left ILB backends."
+ value = <<-EOT
+ gcloud compute backend-services get-health ${var.prefix}-ilb-left \
+ --region ${var.region} \
+ --flatten status.healthStatus \
+ --format "value(status.healthStatus.ipAddress, status.healthStatus.healthState)"
+ EOT
+}
+
+output "backend_health_right" {
+ description = "Command-line health status for right ILB backends."
+ value = <<-EOT
+ gcloud compute backend-services get-health ${var.prefix}-ilb-right \
+ --region ${var.region} \
+ --flatten status.healthStatus \
+ --format "value(status.healthStatus.ipAddress, status.healthStatus.healthState)"
+ EOT
+}
+
+output "ssh_gw" {
+ description = "Command-line login to gateway VMs."
+ value = [
+ for z, mod in module.gw :
+ "gcloud compute ssh ${mod.instance.name} --project ${var.project_id} --zone ${mod.instance.zone}"
+ ]
+}
+
+output "ssh_vm_left" {
+ description = "Command-line login to left VMs."
+ value = [
+ for z, mod in module.vm-left :
+ "gcloud compute ssh ${mod.instance.name} --project ${var.project_id} --zone ${mod.instance.zone}"
+ ]
+}
+
+output "ssh_vm_right" {
+ description = "Command-line login to right VMs."
+ value = [
+ for z, mod in module.vm-right :
+ "gcloud compute ssh ${mod.instance.name} --project ${var.project_id} --zone ${mod.instance.zone}"
+ ]
+}
diff --git a/examples/networking/ilb-next-hop/test_session.png b/blueprints/networking/ilb-next-hop/test_session.png
similarity index 100%
rename from examples/networking/ilb-next-hop/test_session.png
rename to blueprints/networking/ilb-next-hop/test_session.png
diff --git a/blueprints/networking/ilb-next-hop/variables.tf b/blueprints/networking/ilb-next-hop/variables.tf
new file mode 100644
index 0000000000..51a7c03ef2
--- /dev/null
+++ b/blueprints/networking/ilb-next-hop/variables.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.
+ */
+
+variable "ilb_right_enable" {
+ description = "Route right to left traffic through ILB."
+ type = bool
+ default = false
+}
+
+variable "ilb_session_affinity" {
+ description = "Session affinity configuration for ILBs."
+ type = string
+ default = "CLIENT_IP"
+}
+
+variable "ip_ranges" {
+ description = "IP CIDR ranges used for VPC subnets."
+ type = map(string)
+ default = {
+ left = "10.0.0.0/24"
+ right = "10.0.1.0/24"
+ }
+}
+
+variable "prefix" {
+ description = "Prefix used for resource names."
+ type = string
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty."
+ }
+}
+
+variable "project_create" {
+ description = "Create project instead of using an existing one."
+ type = bool
+ default = false
+}
+
+variable "project_id" {
+ description = "Existing project id."
+ type = string
+}
+
+variable "region" {
+ description = "Region used for resources."
+ type = string
+ default = "europe-west1"
+}
+
+variable "zones" {
+ description = "Zone suffixes used for instances."
+ type = list(string)
+ default = ["b", "c"]
+}
diff --git a/blueprints/networking/ilb-next-hop/versions.tf b/blueprints/networking/ilb-next-hop/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/networking/ilb-next-hop/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/examples/networking/ilb-next-hop/vms.tf b/blueprints/networking/ilb-next-hop/vms.tf
similarity index 89%
rename from examples/networking/ilb-next-hop/vms.tf
rename to blueprints/networking/ilb-next-hop/vms.tf
index fe15c2b460..a71a60a07e 100644
--- a/examples/networking/ilb-next-hop/vms.tf
+++ b/blueprints/networking/ilb-next-hop/vms.tf
@@ -27,14 +27,12 @@ module "vm-left" {
for_each = local.zones
project_id = module.project.project_id
zone = each.value
- name = "${local.prefix}vm-left-${each.key}"
+ name = "${var.prefix}-vm-left-${each.key}"
instance_type = "f1-micro"
network_interfaces = [
{
network = module.vpc-left.self_link
subnetwork = values(module.vpc-left.subnet_self_links)[0]
- nat = false
- addresses = null
}
]
tags = ["ssh"]
@@ -52,14 +50,12 @@ module "vm-right" {
for_each = local.zones
project_id = module.project.project_id
zone = each.value
- name = "${local.prefix}vm-right-${each.key}"
+ name = "${var.prefix}-vm-right-${each.key}"
instance_type = "f1-micro"
network_interfaces = [
{
network = module.vpc-right.self_link
subnetwork = values(module.vpc-right.subnet_self_links)[0]
- nat = false
- addresses = null
}
]
tags = ["ssh"]
diff --git a/blueprints/networking/ilb-next-hop/vpc-left.tf b/blueprints/networking/ilb-next-hop/vpc-left.tf
new file mode 100644
index 0000000000..4cc73159c7
--- /dev/null
+++ b/blueprints/networking/ilb-next-hop/vpc-left.tf
@@ -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.
+ */
+
+module "vpc-left" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = "${var.prefix}-left"
+ subnets = [
+ {
+ ip_cidr_range = var.ip_ranges.left
+ name = "${var.prefix}-left"
+ region = var.region
+ },
+ ]
+ routes = {
+ to-right = {
+ dest_range = var.ip_ranges.right
+ next_hop_type = "ilb"
+ next_hop = module.ilb-left.forwarding_rule.self_link
+ }
+ }
+}
+
+module "firewall-left" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.project.project_id
+ network = module.vpc-left.name
+ default_rules_config = {
+ admin_ranges = values(var.ip_ranges)
+ ssh_ranges = ["35.235.240.0/20", "35.191.0.0/16", "130.211.0.0/22"]
+ }
+}
+
+module "nat-left" {
+ source = "../../../modules/net-cloudnat"
+ project_id = module.project.project_id
+ region = var.region
+ name = "${var.prefix}-left"
+ router_network = module.vpc-left.name
+}
diff --git a/blueprints/networking/ilb-next-hop/vpc-right.tf b/blueprints/networking/ilb-next-hop/vpc-right.tf
new file mode 100644
index 0000000000..5483d34a5e
--- /dev/null
+++ b/blueprints/networking/ilb-next-hop/vpc-right.tf
@@ -0,0 +1,64 @@
+/**
+ * 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 "vpc-right" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = "${var.prefix}-right"
+ subnets = [
+ {
+ ip_cidr_range = var.ip_ranges.right
+ name = "${var.prefix}-right"
+ region = var.region
+ },
+ ]
+ routes = {
+ to-left-ilb = {
+ dest_range = var.ip_ranges.left
+ priority = var.ilb_right_enable ? 900 : 1100
+ next_hop_type = "ilb"
+ next_hop = module.ilb-right.forwarding_rule.self_link
+ }
+ to-left-gw-1 = {
+ dest_range = var.ip_ranges.left
+ next_hop_type = "instance"
+ next_hop = module.gw[var.zones[0]].self_link
+ }
+ to-left-gw-2 = {
+ dest_range = var.ip_ranges.left
+ next_hop_type = "instance"
+ next_hop = module.gw[var.zones[1]].self_link
+ }
+ }
+}
+
+module "firewall-right" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.project.project_id
+ network = module.vpc-right.name
+ default_rules_config = {
+ admin_ranges = values(var.ip_ranges)
+ ssh_ranges = ["35.235.240.0/20", "35.191.0.0/16", "130.211.0.0/22"]
+ }
+}
+
+module "nat-right" {
+ source = "../../../modules/net-cloudnat"
+ project_id = module.project.project_id
+ region = var.region
+ name = "${var.prefix}-right"
+ router_network = module.vpc-right.name
+}
diff --git a/examples/networking/private-cloud-function-from-onprem/README.md b/blueprints/networking/private-cloud-function-from-onprem/README.md
similarity index 100%
rename from examples/networking/private-cloud-function-from-onprem/README.md
rename to blueprints/networking/private-cloud-function-from-onprem/README.md
diff --git a/examples/networking/private-cloud-function-from-onprem/assets/main.py b/blueprints/networking/private-cloud-function-from-onprem/assets/main.py
similarity index 100%
rename from examples/networking/private-cloud-function-from-onprem/assets/main.py
rename to blueprints/networking/private-cloud-function-from-onprem/assets/main.py
diff --git a/examples/networking/private-cloud-function-from-onprem/diagram.png b/blueprints/networking/private-cloud-function-from-onprem/diagram.png
similarity index 100%
rename from examples/networking/private-cloud-function-from-onprem/diagram.png
rename to blueprints/networking/private-cloud-function-from-onprem/diagram.png
diff --git a/blueprints/networking/private-cloud-function-from-onprem/main.tf b/blueprints/networking/private-cloud-function-from-onprem/main.tf
new file mode 100644
index 0000000000..1848e95314
--- /dev/null
+++ b/blueprints/networking/private-cloud-function-from-onprem/main.tf
@@ -0,0 +1,230 @@
+/**
+ * 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 {
+ psc_name = replace(var.name, "-", "")
+}
+
+module "project" {
+ source = "../../../modules/project"
+ name = var.project_id
+ project_create = var.project_create == null ? false : true
+ billing_account = try(var.project_create.billing_account_id, null)
+ parent = try(var.project_create.parent, null)
+ services = [
+ "cloudfunctions.googleapis.com",
+ "cloudbuild.googleapis.com",
+ "compute.googleapis.com",
+ "dns.googleapis.com"
+ ]
+}
+
+###############################################################################
+# VPCs #
+###############################################################################
+
+module "vpc-onprem" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = "${var.name}-onprem"
+ subnets = [
+ {
+ ip_cidr_range = var.ip_ranges.onprem
+ name = "${var.name}-onprem"
+ region = var.region
+ enable_private_access = false
+ }
+ ]
+}
+
+module "firewall-onprem" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.project.project_id
+ network = module.vpc-onprem.name
+}
+
+module "vpc-hub" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = "${var.name}-hub"
+ subnets = [
+ {
+ ip_cidr_range = var.ip_ranges.hub
+ name = "${var.name}-hub"
+ region = var.region
+ }
+ ]
+}
+
+###############################################################################
+# VPNs #
+###############################################################################
+
+module "vpn-onprem" {
+ source = "../../../modules/net-vpn-ha"
+ project_id = module.project.project_id
+ region = var.region
+ network = module.vpc-onprem.self_link
+ name = "${var.name}-onprem-to-hub"
+ router_config = {
+ asn = 65001
+ custom_advertise = {
+ all_subnets = true
+ ip_ranges = {}
+ }
+ }
+ peer_gateway = { gcp = module.vpn-hub.self_link }
+ tunnels = {
+ tunnel-0 = {
+ bgp_peer = {
+ address = "169.254.0.2"
+ asn = 65002
+ }
+ bgp_session_range = "169.254.0.1/30"
+ vpn_gateway_interface = 0
+ }
+ tunnel-1 = {
+ bgp_peer = {
+ address = "169.254.0.6"
+ asn = 65002
+ }
+ bgp_session_range = "169.254.0.5/30"
+ vpn_gateway_interface = 1
+ }
+ }
+}
+
+module "vpn-hub" {
+ source = "../../../modules/net-vpn-ha"
+ project_id = module.project.project_id
+ region = var.region
+ network = module.vpc-hub.name
+ name = "${var.name}-hub-to-onprem"
+ router_config = {
+ asn = 65002
+ custom_advertise = {
+ all_subnets = true
+ ip_ranges = {
+ (var.psc_endpoint) = "to-psc-endpoint"
+ }
+ }
+ }
+ peer_gateway = { gcp = module.vpn-onprem.self_link }
+
+ tunnels = {
+ tunnel-0 = {
+ bgp_peer = {
+ address = "169.254.0.1"
+ asn = 65001
+ }
+ bgp_session_range = "169.254.0.2/30"
+ vpn_gateway_interface = 0
+ shared_secret = module.vpn-onprem.random_secret
+ }
+ tunnel-1 = {
+ bgp_peer = {
+ address = "169.254.0.5"
+ asn = 65001
+ }
+ bgp_session_range = "169.254.0.6/30"
+ vpn_gateway_interface = 1
+ shared_secret = module.vpn-onprem.random_secret
+ }
+ }
+}
+
+###############################################################################
+# VMs #
+###############################################################################
+
+module "test-vm" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project.project_id
+ zone = "${var.region}-b"
+ name = "${var.name}-test"
+ instance_type = "e2-micro"
+ boot_disk = {
+ image = "projects/ubuntu-os-cloud/global/images/family/ubuntu-2104"
+ }
+ network_interfaces = [{
+ network = module.vpc-onprem.self_link
+ subnetwork = module.vpc-onprem.subnet_self_links["${var.region}/${var.name}-onprem"]
+ }]
+ tags = ["ssh"]
+}
+
+###############################################################################
+# Cloud Function #
+###############################################################################
+
+module "function-hello" {
+ source = "../../../modules/cloud-function"
+ project_id = module.project.project_id
+ name = var.name
+ bucket_name = "${var.name}-tf-cf-deploy"
+ ingress_settings = "ALLOW_INTERNAL_ONLY"
+ bundle_config = {
+ source_dir = "${path.module}/assets"
+ output_path = "bundle.zip"
+ }
+ bucket_config = {
+ location = var.region
+ }
+ iam = {
+ "roles/cloudfunctions.invoker" = ["allUsers"]
+ }
+}
+
+###############################################################################
+# DNS #
+###############################################################################
+
+module "private-dns-onprem" {
+ source = "../../../modules/dns"
+ project_id = module.project.project_id
+ type = "private"
+ name = var.name
+ domain = "${var.region}-${module.project.project_id}.cloudfunctions.net."
+ client_networks = [module.vpc-onprem.self_link]
+ recordsets = {
+ "A " = { records = [module.addresses.psc_addresses[local.psc_name].address] }
+ }
+}
+
+###############################################################################
+# PSCs #
+###############################################################################
+
+module "addresses" {
+ source = "../../../modules/net-address"
+ project_id = module.project.project_id
+ psc_addresses = {
+ (local.psc_name) = {
+ address = var.psc_endpoint
+ network = module.vpc-hub.self_link
+ }
+ }
+}
+
+resource "google_compute_global_forwarding_rule" "psc-endpoint" {
+ provider = google-beta
+ project = module.project.project_id
+ name = local.psc_name
+ network = module.vpc-hub.self_link
+ ip_address = module.addresses.psc_addresses[local.psc_name].self_link
+ target = "vpc-sc"
+ load_balancing_scheme = ""
+}
diff --git a/examples/networking/private-cloud-function-from-onprem/outputs.tf b/blueprints/networking/private-cloud-function-from-onprem/outputs.tf
similarity index 100%
rename from examples/networking/private-cloud-function-from-onprem/outputs.tf
rename to blueprints/networking/private-cloud-function-from-onprem/outputs.tf
diff --git a/examples/networking/private-cloud-function-from-onprem/variables.tf b/blueprints/networking/private-cloud-function-from-onprem/variables.tf
similarity index 100%
rename from examples/networking/private-cloud-function-from-onprem/variables.tf
rename to blueprints/networking/private-cloud-function-from-onprem/variables.tf
diff --git a/blueprints/networking/private-cloud-function-from-onprem/versions.tf b/blueprints/networking/private-cloud-function-from-onprem/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/networking/private-cloud-function-from-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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/networking/psc-hybrid/README.md b/blueprints/networking/psc-hybrid/README.md
new file mode 100644
index 0000000000..579c9ff4cf
--- /dev/null
+++ b/blueprints/networking/psc-hybrid/README.md
@@ -0,0 +1,55 @@
+# Hybrid connectivity to on-premise services through PSC
+
+The sample allows to connect to an on-prem service leveraging Private Service Connect (PSC).
+
+It creates:
+
+* A [producer](./psc-producer/README.md): a VPC exposing a PSC Service Attachment (SA), connecting to an internal regional TCP proxy load balancer, using a hybrid NEG backend that connects to an on-premises service (IP address + port)
+
+* A [consumer](./psc-consumer/README.md): a VPC with a PSC endpoint pointing to the PSC SA exposed by the producer. The endpoint is accessible by clients through a local IP address on the consumer VPC.
+
+![High-level diagram](diagram.png "High-level diagram")
+
+## Sample modules
+
+The blueprint makes use of the modules [psc-producer](psc-producer) and [psc-consumer](psc-consumer) contained in this folder. This is done so you can build on top of these building blocks, in order to support more complex scenarios.
+
+## Prerequisites
+
+Before applying this Terraform
+
+- On-premises
+ - Allow ingress from *35.191.0.0/16* and *130.211.0.0/22* CIDRs (for HCs)
+ - Allow ingress from the proxy-only subnet CIDR
+- GCP
+ - Advertise from GCP to on-prem *35.191.0.0/16* and *130.211.0.0/22* CIDRs
+ - Advertise from GCP to on-prem the proxy-only subnet CIDRs
+
+## Relevant Links
+
+* [Private Service Connect](https://cloud.google.com/vpc/docs/private-service-connect)
+
+* [Hybrid connectivity Network Endpoint Groups](https://cloud.google.com/load-balancing/docs/negs/hybrid-neg-concepts)
+
+* [Regional TCP Proxy with Hybrid NEGs](https://cloud.google.com/load-balancing/docs/tcp/set-up-int-tcp-proxy-hybrid)
+
+* [PSC approval](https://cloud.google.com/vpc/docs/configure-private-service-connect-producer#publish-service-explicit)
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [dest_ip_address](variables.tf#L17) | On-prem service destination IP address. | string
| ✓ | |
+| [prefix](variables.tf#L28) | Prefix used for resource names. | string
| ✓ | |
+| [producer](variables.tf#L37) | Producer configuration. | object({…})
| ✓ | |
+| [project_id](variables.tf#L53) | When referncing existing projects, the id of the project where resources will be created. | string
| ✓ | |
+| [region](variables.tf#L58) | Region where resources will be created. | string
| ✓ | |
+| [subnet_consumer](variables.tf#L63) | Consumer subnet CIDR. | string # CIDR
| ✓ | |
+| [zone](variables.tf#L102) | Zone where resources will be created. | string
| ✓ | |
+| [dest_port](variables.tf#L22) | On-prem service destination port. | string
| | "80"
|
+| [project_create](variables.tf#L47) | Whether to automatically create a project. | bool
| | false
|
+| [vpc_config](variables.tf#L68) | VPC and subnet ids, in case existing VPCs are used. | object({…})
| | {…}
|
+| [vpc_create](variables.tf#L96) | Whether to automatically create VPCs. | bool
| | true
|
+
+
diff --git a/blueprints/networking/psc-hybrid/diagram.png b/blueprints/networking/psc-hybrid/diagram.png
new file mode 100644
index 0000000000..acb36be91d
Binary files /dev/null and b/blueprints/networking/psc-hybrid/diagram.png differ
diff --git a/blueprints/networking/psc-hybrid/main.tf b/blueprints/networking/psc-hybrid/main.tf
new file mode 100644
index 0000000000..39be8c9232
--- /dev/null
+++ b/blueprints/networking/psc-hybrid/main.tf
@@ -0,0 +1,135 @@
+/**
+ * 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_id = (
+ var.project_create
+ ? module.project.project_id
+ : var.project_id
+ )
+ vpc_producer_id = (
+ var.vpc_create
+ ? module.vpc_producer.network.id
+ : var.vpc_config["producer"]["id"]
+ )
+ vpc_producer_main = (
+ var.vpc_create
+ ? module.vpc_producer.subnets["${var.region}/${var.prefix}-main"].id
+ : var.vpc_config["producer"]["subnet_main_id"]
+ )
+ vpc_producer_proxy = (
+ var.vpc_create
+ ? module.vpc_producer.subnets_proxy_only["${var.region}/${var.prefix}-proxy"].id
+ : var.vpc_config["producer"]["subnet_proxy_id"]
+ )
+ vpc_producer_psc = (
+ var.vpc_create
+ ? module.vpc_producer.subnets_psc["${var.region}/${var.prefix}-psc"].id
+ : var.vpc_config["producer"]["subnet_psc_id"]
+ )
+ vpc_consumer_id = (
+ var.vpc_create
+ ? module.vpc_consumer.network.id
+ : var.vpc_config["consumer"]["id"]
+ )
+ vpc_consumer_main = (
+ var.vpc_create
+ ? module.vpc_consumer.subnets["${var.region}/${var.prefix}-consumer"].id
+ : var.vpc_config["consumer"]["subnet_main_id"]
+ )
+}
+
+module "project" {
+ source = "../../../modules/project"
+ name = var.project_id
+ project_create = var.project_create
+ services = [
+ "compute.googleapis.com"
+ ]
+}
+
+# Producer
+module "vpc_producer" {
+ source = "../../../modules/net-vpc"
+ project_id = local.project_id
+ name = "${var.prefix}-producer"
+ subnets = [
+ {
+ ip_cidr_range = var.producer["subnet_main"]
+ name = "${var.prefix}-main"
+ region = var.region
+ secondary_ip_range = {}
+ }
+ ]
+ subnets_proxy_only = [
+ {
+ ip_cidr_range = var.producer["subnet_proxy"]
+ name = "${var.prefix}-proxy"
+ region = var.region
+ active = true
+ }
+ ]
+ subnets_psc = [
+ {
+ ip_cidr_range = var.producer["subnet_psc"]
+ name = "${var.prefix}-psc"
+ region = var.region
+ }
+ ]
+}
+
+module "psc_producer" {
+ source = "./psc-producer"
+ project_id = local.project_id
+ name = "${var.prefix}-producer"
+ dest_ip_address = var.dest_ip_address
+ dest_port = var.dest_port
+ network = local.vpc_producer_id
+ region = var.region
+ zone = var.zone
+ subnet = local.vpc_producer_main
+ subnet_proxy = local.vpc_producer_proxy
+ subnets_psc = [
+ local.vpc_producer_psc
+ ]
+ accepted_limits = var.producer["accepted_limits"]
+}
+
+# Consumer
+
+module "vpc_consumer" {
+ source = "../../../modules/net-vpc"
+ project_id = local.project_id
+ name = "${var.prefix}-consumer"
+ subnets = [
+ {
+ ip_cidr_range = var.subnet_consumer
+ name = "${var.prefix}-consumer"
+ region = var.region
+ secondary_ip_range = {}
+ }
+ ]
+}
+
+module "psc_consumer" {
+ source = "./psc-consumer"
+ project_id = local.project_id
+ name = "${var.prefix}-consumer"
+ region = var.region
+ network = local.vpc_consumer_id
+ subnet = local.vpc_consumer_main
+ sa_id = module.psc_producer.service_attachment.id
+}
diff --git a/blueprints/networking/psc-hybrid/psc-consumer/README.md b/blueprints/networking/psc-hybrid/psc-consumer/README.md
new file mode 100644
index 0000000000..b681fb1e34
--- /dev/null
+++ b/blueprints/networking/psc-hybrid/psc-consumer/README.md
@@ -0,0 +1,17 @@
+# PSC Consumer
+
+The module creates a consumer VPC and a Private Service Connect (PSC) endpoint, pointing to the PSC Service Attachment (SA) specified.
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [name](variables.tf#L17) | Name of the resources created. | string
| ✓ | |
+| [network](variables.tf#L22) | Consumer network id. | string
| ✓ | |
+| [project_id](variables.tf#L27) | The ID of the project where this VPC will be created. | string
| ✓ | |
+| [region](variables.tf#L32) | Region where resources will be created. | string
| ✓ | |
+| [sa_id](variables.tf#L37) | PSC producer service attachment id. | string
| ✓ | |
+| [subnet](variables.tf#L42) | Subnetwork id where resources will be associated. | string
| ✓ | |
+
+
diff --git a/blueprints/networking/psc-hybrid/psc-consumer/main.tf b/blueprints/networking/psc-hybrid/psc-consumer/main.tf
new file mode 100644
index 0000000000..7967aa7a73
--- /dev/null
+++ b/blueprints/networking/psc-hybrid/psc-consumer/main.tf
@@ -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.
+ */
+
+resource "google_compute_address" "psc_endpoint_address" {
+ name = var.name
+ project = var.project_id
+ address_type = "INTERNAL"
+ subnetwork = var.subnet
+ region = var.region
+}
+
+resource "google_compute_forwarding_rule" "psc_ilb_consumer" {
+ name = var.name
+ project = var.project_id
+ region = var.region
+ target = var.sa_id
+ load_balancing_scheme = ""
+ network = var.network
+ ip_address = google_compute_address.psc_endpoint_address.id
+}
diff --git a/blueprints/networking/psc-hybrid/psc-consumer/variables.tf b/blueprints/networking/psc-hybrid/psc-consumer/variables.tf
new file mode 100644
index 0000000000..03f8c2fe32
--- /dev/null
+++ b/blueprints/networking/psc-hybrid/psc-consumer/variables.tf
@@ -0,0 +1,45 @@
+/**
+ * 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 "name" {
+ description = "Name of the resources created."
+ type = string
+}
+
+variable "network" {
+ description = "Consumer network id."
+ type = string
+}
+
+variable "project_id" {
+ description = "The ID of the project where this VPC will be created."
+ type = string
+}
+
+variable "region" {
+ description = "Region where resources will be created."
+ type = string
+}
+
+variable "sa_id" {
+ description = "PSC producer service attachment id."
+ type = string
+}
+
+variable "subnet" {
+ description = "Subnetwork id where resources will be associated."
+ type = string
+}
diff --git a/blueprints/networking/psc-hybrid/psc-producer/README.md b/blueprints/networking/psc-hybrid/psc-producer/README.md
new file mode 100644
index 0000000000..23f1a67094
--- /dev/null
+++ b/blueprints/networking/psc-hybrid/psc-producer/README.md
@@ -0,0 +1,32 @@
+# PSC Producer
+
+The module creates:
+
+- a producer VPC
+- an internal regional TCP proxy load balancer with a hybrid Network Endpoint Group (NEG) backend, pointing to an on-prem service (IP + port)
+- a Private Service Connect Service Attachment (PSC SA) exposing the service to [PSC consumers](../psc-consumer/README.md)
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [accepted_limits](variables.tf#L17) | Incoming accepted projects with endpoints limit. | map(number)
| ✓ | |
+| [dest_ip_address](variables.tf#L22) | On-prem service destination IP address. | string
| ✓ | |
+| [name](variables.tf#L33) | Name of the resources created. | string
| ✓ | |
+| [network](variables.tf#L38) | Producer network id. | string
| ✓ | |
+| [project_id](variables.tf#L43) | The ID of the project where this VPC will be created. | string
| ✓ | |
+| [region](variables.tf#L48) | Region where resources will be created. | string
| ✓ | |
+| [subnet](variables.tf#L53) | Subnetwork id where resources will be associated. | string
| ✓ | |
+| [subnet_proxy](variables.tf#L58) | L7 Regional load balancing subnet id. | string
| ✓ | |
+| [subnets_psc](variables.tf#L63) | PSC NAT subnets. | list(string)
| ✓ | |
+| [zone](variables.tf#L68) | Zone where resources will be created. | string
| ✓ | |
+| [dest_port](variables.tf#L27) | On-prem service destination port. | string
| | "80"
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [service_attachment](outputs.tf#L17) | The service attachment resource. | |
+
+
diff --git a/blueprints/networking/psc-hybrid/psc-producer/main.tf b/blueprints/networking/psc-hybrid/psc-producer/main.tf
new file mode 100644
index 0000000000..01212badb0
--- /dev/null
+++ b/blueprints/networking/psc-hybrid/psc-producer/main.tf
@@ -0,0 +1,107 @@
+/**
+ * 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.
+ */
+
+# Hybrid NEG
+
+resource "google_compute_network_endpoint_group" "neg" {
+ name = var.name
+ project = var.project_id
+ network = var.network
+ default_port = var.dest_port
+ zone = "${var.region}-${var.zone}"
+ network_endpoint_type = "NON_GCP_PRIVATE_IP_PORT"
+}
+
+resource "google_compute_network_endpoint" "endpoint" {
+ project = var.project_id
+ network_endpoint_group = google_compute_network_endpoint_group.neg.name
+ port = var.dest_port
+ ip_address = var.dest_ip_address
+ zone = "${var.region}-${var.zone}"
+}
+
+# TCP Proxy ILB
+
+resource "google_compute_region_health_check" "health_check" {
+ name = var.name
+ project = var.project_id
+ region = var.region
+ timeout_sec = 1
+ check_interval_sec = 1
+
+ tcp_health_check {
+ port = var.dest_port
+ }
+}
+
+resource "google_compute_region_backend_service" "backend_service" {
+ name = var.name
+ project = var.project_id
+ region = var.region
+ health_checks = [google_compute_region_health_check.health_check.id]
+ load_balancing_scheme = "INTERNAL_MANAGED"
+ protocol = "TCP"
+
+ backend {
+ group = google_compute_network_endpoint_group.neg.self_link
+ balancing_mode = "CONNECTION"
+ failover = false
+ capacity_scaler = 1.0
+ max_connections = 100
+ }
+}
+
+resource "google_compute_region_target_tcp_proxy" "target_proxy" {
+ provider = google-beta
+ name = var.name
+ region = var.region
+ project = var.project_id
+ backend_service = google_compute_region_backend_service.backend_service.id
+}
+
+resource "google_compute_forwarding_rule" "forwarding_rule" {
+ provider = google-beta
+ name = var.name
+ project = var.project_id
+ region = var.region
+ ip_protocol = "TCP"
+ load_balancing_scheme = "INTERNAL_MANAGED"
+ port_range = var.dest_port
+ target = google_compute_region_target_tcp_proxy.target_proxy.id
+ network = var.network
+ subnetwork = var.subnet
+ network_tier = "PREMIUM"
+}
+
+# PSC Service Attachment
+
+resource "google_compute_service_attachment" "service_attachment" {
+ name = var.name
+ project = var.project_id
+ region = var.region
+ enable_proxy_protocol = false
+ connection_preference = "ACCEPT_MANUAL"
+ nat_subnets = var.subnets_psc
+ target_service = google_compute_forwarding_rule.forwarding_rule.id
+
+ dynamic "consumer_accept_lists" {
+ for_each = var.accepted_limits
+ content {
+ project_id_or_num = consumer_accept_lists.key
+ connection_limit = consumer_accept_lists.value
+ }
+ }
+}
diff --git a/blueprints/networking/psc-hybrid/psc-producer/outputs.tf b/blueprints/networking/psc-hybrid/psc-producer/outputs.tf
new file mode 100644
index 0000000000..6539bcd73b
--- /dev/null
+++ b/blueprints/networking/psc-hybrid/psc-producer/outputs.tf
@@ -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.
+ */
+
+output "service_attachment" {
+ description = "The service attachment resource."
+ value = google_compute_service_attachment.service_attachment
+}
diff --git a/blueprints/networking/psc-hybrid/psc-producer/variables.tf b/blueprints/networking/psc-hybrid/psc-producer/variables.tf
new file mode 100644
index 0000000000..c4105e4a1a
--- /dev/null
+++ b/blueprints/networking/psc-hybrid/psc-producer/variables.tf
@@ -0,0 +1,71 @@
+/**
+ * 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 "accepted_limits" {
+ description = "Incoming accepted projects with endpoints limit."
+ type = map(number)
+}
+
+variable "dest_ip_address" {
+ description = "On-prem service destination IP address."
+ type = string
+}
+
+variable "dest_port" {
+ description = "On-prem service destination port."
+ type = string
+ default = "80"
+}
+
+variable "name" {
+ description = "Name of the resources created."
+ type = string
+}
+
+variable "network" {
+ description = "Producer network id."
+ type = string
+}
+
+variable "project_id" {
+ description = "The ID of the project where this VPC will be created."
+ type = string
+}
+
+variable "region" {
+ description = "Region where resources will be created."
+ type = string
+}
+
+variable "subnet" {
+ description = "Subnetwork id where resources will be associated."
+ type = string
+}
+
+variable "subnet_proxy" {
+ description = "L7 Regional load balancing subnet id."
+ type = string
+}
+
+variable "subnets_psc" {
+ description = "PSC NAT subnets."
+ type = list(string)
+}
+
+variable "zone" {
+ description = "Zone where resources will be created."
+ type = string
+}
diff --git a/blueprints/networking/psc-hybrid/variables.tf b/blueprints/networking/psc-hybrid/variables.tf
new file mode 100644
index 0000000000..d5d818a8d9
--- /dev/null
+++ b/blueprints/networking/psc-hybrid/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 "dest_ip_address" {
+ description = "On-prem service destination IP address."
+ type = string
+}
+
+variable "dest_port" {
+ description = "On-prem service destination port."
+ type = string
+ default = "80"
+}
+
+variable "prefix" {
+ description = "Prefix used for resource names."
+ type = string
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty."
+ }
+}
+
+variable "producer" {
+ description = "Producer configuration."
+ type = object({
+ subnet_main = string # CIDR
+ subnet_proxy = string # CIDR
+ subnet_psc = string # CIDR
+ accepted_limits = map(number) # Accepted project ids => PSC endpoint limit
+ })
+}
+
+variable "project_create" {
+ description = "Whether to automatically create a project."
+ type = bool
+ default = false
+}
+
+variable "project_id" {
+ description = "When referncing existing projects, the id of the project where resources will be created."
+ type = string
+}
+
+variable "region" {
+ description = "Region where resources will be created."
+ type = string
+}
+
+variable "subnet_consumer" {
+ description = "Consumer subnet CIDR."
+ type = string # CIDR
+}
+
+variable "vpc_config" {
+ description = "VPC and subnet ids, in case existing VPCs are used."
+ type = object({
+ producer = object({
+ id = string
+ subnet_main_id = string
+ subnet_proxy_id = string
+ subnet_psc_id = string
+ })
+ consumer = object({
+ id = string
+ subnet_main_id = string
+ })
+ })
+ default = {
+ producer = {
+ id = "xxx"
+ subnet_main_id = "xxx"
+ subnet_proxy_id = "xxx"
+ subnet_psc_id = "xxx"
+ }
+ consumer = {
+ id = "xxx"
+ subnet_main_id = "xxx"
+ }
+ }
+}
+
+variable "vpc_create" {
+ description = "Whether to automatically create VPCs."
+ type = bool
+ default = true
+}
+
+variable "zone" {
+ description = "Zone where resources will be created."
+ type = string
+}
diff --git a/blueprints/networking/shared-vpc-gke/README.md b/blueprints/networking/shared-vpc-gke/README.md
new file mode 100644
index 0000000000..858518bd89
--- /dev/null
+++ b/blueprints/networking/shared-vpc-gke/README.md
@@ -0,0 +1,72 @@
+# Shared VPC with optional GKE cluster
+
+This sample creates a basic [Shared VPC](https://cloud.google.com/vpc/docs/shared-vpc) setup using one host project and two service projects, each with a specific subnet in the shared VPC.
+
+The setup also includes the specific IAM-level configurations needed for [GKE on Shared VPC](https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-shared-vpc) in one of the two service projects, and optionally creates a cluster with a single nodepool.
+
+If you only need a basic Shared VPC, or prefer creating a cluster manually, set the `cluster_create` variable to `False`.
+
+The sample has been purposefully kept simple so that it can be used as a basis for different Shared VPC configurations. This is the high level diagram:
+
+![High-level diagram](diagram.png "High-level diagram")
+
+## Accessing the bastion instance and GKE cluster
+
+The bastion VM has no public address so access is mediated via [IAP](https://cloud.google.com/iap/docs), which is supported transparently in the `gcloud compute ssh` command. Authentication is via OS Login set as a project default.
+
+Cluster access from the bastion can leverage the instance service account's `container.developer` role: the only configuration needed is to fetch cluster credentials via `gcloud container clusters get-credentials` passing the correct cluster name, location and project via command options.
+
+For convenience, [Tinyproxy](http://tinyproxy.github.io/) is installed on the bastion host, allowing `kubectl` use via [IAP](https://cloud.google.com/iap/docs) from an external client:
+
+```bash
+gcloud container clusters get-credentials "${CLUSTER_NAME}" \
+ --zone "${CLUSTER_ZONE}" \
+ --project "${CLUSTER_PROJECT_NAME}"
+
+gcloud compute ssh "${BASTION_INSTANCE_NAME}" \
+ --project "${CLUSTER_PROJECT_NAME}" \
+ --zone "${CLUSTER_ZONE}" \
+ -- -L 8888:localhost:8888 -N -q -f
+
+# Run kubectl through the proxy
+HTTPS_PROXY=localhost:8888 kubectl get pods
+```
+
+An alias can also be created. For example:
+
+```bash
+alias k='HTTPS_PROXY=localhost:8888 kubectl $@'
+```
+
+## Destroying
+
+There's a minor glitch that can surface running `terraform destroy`, where the service project attachments to the Shared VPC will not get destroyed even with the relevant API call succeeding. We are investigating the issue, in the meantime just manually remove the attachment in the Cloud console or via the `gcloud beta compute shared-vpc associated-projects remove` command when `terraform destroy` fails, and then relaunch the command.
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [billing_account_id](variables.tf#L15) | Billing account id used as default for new projects. | string
| ✓ | |
+| [prefix](variables.tf#L62) | Prefix used for resource names. | string
| ✓ | |
+| [root_node](variables.tf#L94) | Hierarchy node where projects will be created, 'organizations/org_id' or 'folders/folder_id'. | string
| ✓ | |
+| [cluster_create](variables.tf#L20) | Create GKE cluster and nodepool. | bool
| | true
|
+| [ip_ranges](variables.tf#L26) | Subnet IP CIDR ranges. | map(string)
| | {…}
|
+| [ip_secondary_ranges](variables.tf#L35) | Secondary IP CIDR ranges. | map(string)
| | {…}
|
+| [owners_gce](variables.tf#L44) | GCE project owners, in IAM format. | list(string)
| | []
|
+| [owners_gke](variables.tf#L50) | GKE project owners, in IAM format. | list(string)
| | []
|
+| [owners_host](variables.tf#L56) | Host project owners, in IAM format. | list(string)
| | []
|
+| [private_service_ranges](variables.tf#L71) | Private service IP CIDR ranges. | map(string)
| | {…}
|
+| [project_services](variables.tf#L79) | Service APIs enabled by default in new projects. | list(string)
| | […]
|
+| [region](variables.tf#L88) | Region used. | string
| | "europe-west1"
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [gke_clusters](outputs.tf#L15) | GKE clusters information. | |
+| [projects](outputs.tf#L24) | Project ids. | |
+| [vms](outputs.tf#L33) | GCE VMs. | |
+| [vpc](outputs.tf#L40) | Shared VPC. | |
+
+
diff --git a/examples/networking/shared-vpc-gke/backend.tf.sample b/blueprints/networking/shared-vpc-gke/backend.tf.sample
similarity index 100%
rename from examples/networking/shared-vpc-gke/backend.tf.sample
rename to blueprints/networking/shared-vpc-gke/backend.tf.sample
diff --git a/examples/networking/shared-vpc-gke/diagram.gcpdraw b/blueprints/networking/shared-vpc-gke/diagram.gcpdraw
similarity index 100%
rename from examples/networking/shared-vpc-gke/diagram.gcpdraw
rename to blueprints/networking/shared-vpc-gke/diagram.gcpdraw
diff --git a/examples/networking/shared-vpc-gke/diagram.png b/blueprints/networking/shared-vpc-gke/diagram.png
similarity index 100%
rename from examples/networking/shared-vpc-gke/diagram.png
rename to blueprints/networking/shared-vpc-gke/diagram.png
diff --git a/blueprints/networking/shared-vpc-gke/main.tf b/blueprints/networking/shared-vpc-gke/main.tf
new file mode 100644
index 0000000000..97bf45d247
--- /dev/null
+++ b/blueprints/networking/shared-vpc-gke/main.tf
@@ -0,0 +1,234 @@
+# 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.
+
+###############################################################################
+# Host and service projects #
+###############################################################################
+
+# the container.hostServiceAgentUser role is needed for GKE on shared VPC
+# see: https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-shared-vpc#grant_host_service_agent_role
+
+module "project-host" {
+ source = "../../../modules/project"
+ parent = var.root_node
+ billing_account = var.billing_account_id
+ prefix = var.prefix
+ name = "net"
+ services = concat(var.project_services, ["dns.googleapis.com"])
+ shared_vpc_host_config = {
+ enabled = true
+ }
+ iam = {
+ "roles/owner" = var.owners_host
+ }
+}
+
+module "project-svc-gce" {
+ source = "../../../modules/project"
+ parent = var.root_node
+ billing_account = var.billing_account_id
+ prefix = var.prefix
+ name = "gce"
+ services = var.project_services
+ oslogin = true
+ oslogin_admins = var.owners_gce
+ shared_vpc_service_config = {
+ host_project = module.project-host.project_id
+ service_identity_iam = {
+ "roles/compute.networkUser" = ["cloudservices"]
+ }
+ }
+ iam = {
+ "roles/owner" = var.owners_gce
+ }
+}
+
+# the container.developer role assigned to the bastion instance service account
+# allows to fetch GKE credentials from bastion for clusters in this project
+
+module "project-svc-gke" {
+ source = "../../../modules/project"
+ parent = var.root_node
+ billing_account = var.billing_account_id
+ prefix = var.prefix
+ name = "gke"
+ services = var.project_services
+ shared_vpc_service_config = {
+ host_project = module.project-host.project_id
+ service_identity_iam = {
+ "roles/container.hostServiceAgentUser" = ["container-engine"]
+ "roles/compute.networkUser" = ["container-engine"]
+ }
+ }
+ iam = merge(
+ {
+ "roles/container.developer" = [module.vm-bastion.service_account_iam_email]
+ "roles/owner" = var.owners_gke
+ },
+ var.cluster_create
+ ? {
+ "roles/logging.logWriter" = [module.cluster-1-nodepool-1.0.service_account_iam_email]
+ "roles/monitoring.metricWriter" = [module.cluster-1-nodepool-1.0.service_account_iam_email]
+ }
+ : {}
+ )
+}
+
+################################################################################
+# Networking #
+################################################################################
+
+# subnet IAM bindings control which identities can use the individual subnets
+
+module "vpc-shared" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project-host.project_id
+ name = "shared-vpc"
+ subnets = [
+ {
+ ip_cidr_range = var.ip_ranges.gce
+ name = "gce"
+ region = var.region
+ },
+ {
+ ip_cidr_range = var.ip_ranges.gke
+ name = "gke"
+ region = var.region
+ secondary_ip_ranges = {
+ pods = var.ip_secondary_ranges.gke-pods
+ services = var.ip_secondary_ranges.gke-services
+ }
+ }
+ ]
+ subnet_iam = {
+ "${var.region}/gce" = {
+ "roles/compute.networkUser" = concat(var.owners_gce, [
+ "serviceAccount:${module.project-svc-gce.service_accounts.cloud_services}",
+ ])
+ }
+ "${var.region}/gke" = {
+ "roles/compute.networkUser" = concat(var.owners_gke, [
+ "serviceAccount:${module.project-svc-gke.service_accounts.cloud_services}",
+ "serviceAccount:${module.project-svc-gke.service_accounts.robots.container-engine}",
+ ])
+ "roles/compute.securityAdmin" = [
+ "serviceAccount:${module.project-svc-gke.service_accounts.robots.container-engine}",
+ ]
+ }
+ }
+}
+
+module "vpc-shared-firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.project-host.project_id
+ network = module.vpc-shared.name
+ default_rules_config = {
+ admin_ranges = values(var.ip_ranges)
+ }
+}
+
+module "nat" {
+ source = "../../../modules/net-cloudnat"
+ project_id = module.project-host.project_id
+ region = var.region
+ name = "vpc-shared"
+ router_create = true
+ router_network = module.vpc-shared.name
+}
+
+################################################################################
+# DNS #
+################################################################################
+
+module "host-dns" {
+ source = "../../../modules/dns"
+ project_id = module.project-host.project_id
+ type = "private"
+ name = "example"
+ domain = "example.com."
+ client_networks = [module.vpc-shared.self_link]
+ recordsets = {
+ "A localhost" = { records = ["127.0.0.1"] }
+ "A bastion" = { records = [module.vm-bastion.internal_ip] }
+ }
+}
+
+################################################################################
+# VM #
+################################################################################
+
+module "vm-bastion" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project-svc-gce.project_id
+ zone = "${var.region}-b"
+ name = "bastion"
+ network_interfaces = [{
+ network = module.vpc-shared.self_link
+ subnetwork = lookup(module.vpc-shared.subnet_self_links, "${var.region}/gce", null)
+ nat = false
+ addresses = null
+ }]
+ tags = ["ssh"]
+ metadata = {
+ startup-script = join("\n", [
+ "#! /bin/bash",
+ "apt-get update",
+ "apt-get install -y bash-completion kubectl dnsutils tinyproxy",
+ "grep -qxF 'Allow localhost' /etc/tinyproxy/tinyproxy.conf || echo 'Allow localhost' >> /etc/tinyproxy/tinyproxy.conf",
+ "service tinyproxy restart"
+ ])
+ }
+ service_account_create = true
+}
+
+################################################################################
+# GKE #
+################################################################################
+
+module "cluster-1" {
+ source = "../../../modules/gke-cluster"
+ count = var.cluster_create ? 1 : 0
+ name = "cluster-1"
+ project_id = module.project-svc-gke.project_id
+ location = "${var.region}-b"
+ vpc_config = {
+ network = module.vpc-shared.self_link
+ subnetwork = module.vpc-shared.subnet_self_links["${var.region}/gke"]
+ master_authorized_ranges = {
+ internal-vms = var.ip_ranges.gce
+ }
+ master_ipv4_cidr_block = var.private_service_ranges.cluster-1
+ }
+ max_pods_per_node = 32
+ private_cluster_config = {
+ enable_private_endpoint = true
+ master_global_access = true
+ }
+ labels = {
+ environment = "test"
+ }
+}
+
+module "cluster-1-nodepool-1" {
+ source = "../../../modules/gke-nodepool"
+ count = var.cluster_create ? 1 : 0
+ name = "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/examples/networking/shared-vpc-gke/outputs.tf b/blueprints/networking/shared-vpc-gke/outputs.tf
similarity index 100%
rename from examples/networking/shared-vpc-gke/outputs.tf
rename to blueprints/networking/shared-vpc-gke/outputs.tf
diff --git a/blueprints/networking/shared-vpc-gke/variables.tf b/blueprints/networking/shared-vpc-gke/variables.tf
new file mode 100644
index 0000000000..96ccfb0c27
--- /dev/null
+++ b/blueprints/networking/shared-vpc-gke/variables.tf
@@ -0,0 +1,97 @@
+# 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.
+
+variable "billing_account_id" {
+ description = "Billing account id used as default for new projects."
+ type = string
+}
+
+variable "cluster_create" {
+ description = "Create GKE cluster and nodepool."
+ type = bool
+ default = true
+}
+
+variable "ip_ranges" {
+ description = "Subnet IP CIDR ranges."
+ type = map(string)
+ default = {
+ gce = "10.0.16.0/24"
+ gke = "10.0.32.0/24"
+ }
+}
+
+variable "ip_secondary_ranges" {
+ description = "Secondary IP CIDR ranges."
+ type = map(string)
+ default = {
+ gke-pods = "10.128.0.0/18"
+ gke-services = "172.16.0.0/24"
+ }
+}
+
+variable "owners_gce" {
+ description = "GCE project owners, in IAM format."
+ type = list(string)
+ default = []
+}
+
+variable "owners_gke" {
+ description = "GKE project owners, in IAM format."
+ type = list(string)
+ default = []
+}
+
+variable "owners_host" {
+ description = "Host project owners, in IAM format."
+ type = list(string)
+ default = []
+}
+
+variable "prefix" {
+ description = "Prefix used for resource names."
+ type = string
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty."
+ }
+}
+
+variable "private_service_ranges" {
+ description = "Private service IP CIDR ranges."
+ type = map(string)
+ default = {
+ cluster-1 = "192.168.0.0/28"
+ }
+}
+
+variable "project_services" {
+ description = "Service APIs enabled by default in new projects."
+ type = list(string)
+ default = [
+ "container.googleapis.com",
+ "stackdriver.googleapis.com",
+ ]
+}
+
+variable "region" {
+ description = "Region used."
+ type = string
+ default = "europe-west1"
+}
+
+variable "root_node" {
+ description = "Hierarchy node where projects will be created, 'organizations/org_id' or 'folders/folder_id'."
+ type = string
+}
diff --git a/blueprints/networking/shared-vpc-gke/versions.tf b/blueprints/networking/shared-vpc-gke/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/networking/shared-vpc-gke/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/serverless/README.md b/blueprints/serverless/README.md
new file mode 100644
index 0000000000..44f39f0f66
--- /dev/null
+++ b/blueprints/serverless/README.md
@@ -0,0 +1,12 @@
+# Serverless blueprints
+
+The blueprints in this folder show implement **end-to-end scenarios** for Serveless topologies that show how to automate common configurations or leverage specific products.
+
+They are meant to be used as minimal but complete starting points to create actual infrastructure, and as playgrounds to experiment with Google Cloud features.
+
+## Blueprints
+
+### Multi-region deployments for API Gateway
+
+ This [blueprint](./api-gateway/) shows how to configure a load balancer to enable multi-region deployments for API Gateway. For more details on how this set up work have a look at the article [here](https://cloud.google.com/api-gateway/docs/multi-region-deployment)
+string
| ✓ | |
+| [regions](variables.tf#L31) | List of regions to deploy the proxy in. | list(string)
| ✓ | |
+| [project_create](variables.tf#L17) | Parameters for the creation of the new project. | object({…})
| | null
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [ip_address](outputs.tf#L17) | The reserved global IP address. | |
+
+
diff --git a/examples/serverless/api-gateway/architecture.png b/blueprints/serverless/api-gateway/diagram.png
similarity index 100%
rename from examples/serverless/api-gateway/architecture.png
rename to blueprints/serverless/api-gateway/diagram.png
diff --git a/examples/serverless/api-gateway/function/index.js b/blueprints/serverless/api-gateway/function/index.js
similarity index 100%
rename from examples/serverless/api-gateway/function/index.js
rename to blueprints/serverless/api-gateway/function/index.js
diff --git a/blueprints/serverless/api-gateway/function/package-lock.json b/blueprints/serverless/api-gateway/function/package-lock.json
new file mode 100644
index 0000000000..da027c38b8
--- /dev/null
+++ b/blueprints/serverless/api-gateway/function/package-lock.json
@@ -0,0 +1,2560 @@
+{
+ "name": "function",
+ "version": "1.0.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "function",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "@google-cloud/functions-framework": "^3.0.0",
+ "express": "^4.17.3"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.16.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz",
+ "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==",
+ "dependencies": {
+ "@babel/highlight": "^7.16.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.16.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
+ "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.16.10",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz",
+ "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.16.7",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@google-cloud/functions-framework": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-3.0.0.tgz",
+ "integrity": "sha512-+K9+y39/5ig4QrbnaCM8BOzt4+Qx5SRMu2dj5QDTNFc5s8f/Lubty8u3aBQN6JC86M0NuHL9zIj8xs8Awj7C+w==",
+ "dependencies": {
+ "body-parser": "^1.18.3",
+ "cloudevents": "^5.1.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/@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/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/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "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/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+ },
+ "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/body-parser": {
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz",
+ "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "http-errors": "1.8.1",
+ "iconv-lite": "0.4.24",
+ "on-finished": "~2.3.0",
+ "qs": "6.9.7",
+ "raw-body": "2.4.3",
+ "type-is": "~1.6.18"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "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/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/cloudevents": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/cloudevents/-/cloudevents-5.3.2.tgz",
+ "integrity": "sha512-ZjEFjx0BJnio8SED1TzD7GHA118zCk04Mz6aDMMii+4/ZvX5LPgn1D4lT5Jj7HodCbdeRS6dX88unH06Qc3mkA==",
+ "dependencies": {
+ "ajv": "~6.12.3",
+ "util": "^0.12.4",
+ "uuid": "~8.3.0"
+ }
+ },
+ "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/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+ },
+ "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.4.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
+ "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
+ "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": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+ },
+ "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/define-properties": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "dependencies": {
+ "object-keys": "^1.0.12"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "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/es-abstract": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz",
+ "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.1.1",
+ "get-symbol-description": "^1.0.0",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.2",
+ "internal-slot": "^1.0.3",
+ "is-callable": "^1.2.4",
+ "is-negative-zero": "^2.0.1",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.1",
+ "is-string": "^1.0.7",
+ "is-weakref": "^1.0.1",
+ "object-inspect": "^1.11.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.2",
+ "string.prototype.trimend": "^1.0.4",
+ "string.prototype.trimstart": "^1.0.4",
+ "unbox-primitive": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+ },
+ "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": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.17.3",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz",
+ "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.19.2",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.4.2",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.1.2",
+ "fresh": "0.5.2",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.9.7",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.17.2",
+ "serve-static": "1.14.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~1.5.0",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "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-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
+ },
+ "node_modules/finalhandler": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+ "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "statuses": "~1.5.0",
+ "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/foreach": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
+ "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k="
+ },
+ "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": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "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/get-intrinsic": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
+ "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
+ "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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-bigints": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
+ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
+ "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
+ "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": "1.8.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+ "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+ "dependencies": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.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/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/internal-slot": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
+ "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
+ "dependencies": {
+ "get-intrinsic": "^1.1.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
+ },
+ "node_modules/is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "dependencies": {
+ "has-bigints": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "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-callable": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
+ "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
+ "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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-negative-zero": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+ "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz",
+ "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "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-shared-array-buffer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz",
+ "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.8.tgz",
+ "integrity": "sha512-HqH41TNZq2fgtGT8WHVFVJhBVGuY3AnP3Q36K8JKXUxSxRgk/d+7NjmwG2vo2mYmXK8UYZKu0qH8bVP5gEisjA==",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "es-abstract": "^1.18.5",
+ "foreach": "^2.0.5",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "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/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": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+ },
+ "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/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/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/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
+ "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": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
+ "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.51.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
+ "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.34",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
+ "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
+ "dependencies": {
+ "mime-db": "1.51.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "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/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-inspect": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
+ "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
+ "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3",
+ "has-symbols": "^1.0.1",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "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-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": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ },
+ "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/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.9.7",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz",
+ "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==",
+ "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.4.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz",
+ "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "1.8.1",
+ "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/resolve": {
+ "version": "1.22.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
+ "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
+ "dependencies": {
+ "is-core-module": "^2.8.1",
+ "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/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/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.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.17.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
+ "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "1.8.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.1",
+ "statuses": "~1.5.0"
+ },
+ "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.14.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
+ "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==",
+ "dependencies": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.17.2"
+ },
+ "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/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.11",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz",
+ "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g=="
+ },
+ "node_modules/statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
+ "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
+ "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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/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/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/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/unbox-primitive": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
+ "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has-bigints": "^1.0.1",
+ "has-symbols": "^1.0.2",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
+ "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.4",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz",
+ "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "is-arguments": "^1.0.4",
+ "is-generator-function": "^1.0.7",
+ "is-typed-array": "^1.1.3",
+ "safe-buffer": "^5.1.2",
+ "which-typed-array": "^1.1.2"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
+ "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": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dependencies": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.7.tgz",
+ "integrity": "sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw==",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "es-abstract": "^1.18.5",
+ "foreach": "^2.0.5",
+ "has-tostringtag": "^1.0.0",
+ "is-typed-array": "^1.1.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ }
+ },
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.16.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz",
+ "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==",
+ "requires": {
+ "@babel/highlight": "^7.16.7"
+ }
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.16.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
+ "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw=="
+ },
+ "@babel/highlight": {
+ "version": "7.16.10",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz",
+ "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==",
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.16.7",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "@google-cloud/functions-framework": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-3.0.0.tgz",
+ "integrity": "sha512-+K9+y39/5ig4QrbnaCM8BOzt4+Qx5SRMu2dj5QDTNFc5s8f/Lubty8u3aBQN6JC86M0NuHL9zIj8xs8Awj7C+w==",
+ "requires": {
+ "body-parser": "^1.18.3",
+ "cloudevents": "^5.1.0",
+ "express": "^4.16.4",
+ "minimist": "^1.2.5",
+ "on-finished": "^2.3.0",
+ "read-pkg-up": "^7.0.1",
+ "semver": "^7.3.5"
+ }
+ },
+ "@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=="
+ },
+ "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"
+ }
+ },
+ "ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "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"
+ }
+ },
+ "array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+ },
+ "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=="
+ },
+ "body-parser": {
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz",
+ "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==",
+ "requires": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "http-errors": "1.8.1",
+ "iconv-lite": "0.4.24",
+ "on-finished": "~2.3.0",
+ "qs": "6.9.7",
+ "raw-body": "2.4.3",
+ "type-is": "~1.6.18"
+ }
+ },
+ "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"
+ }
+ },
+ "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"
+ }
+ },
+ "cloudevents": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/cloudevents/-/cloudevents-5.3.2.tgz",
+ "integrity": "sha512-ZjEFjx0BJnio8SED1TzD7GHA118zCk04Mz6aDMMii+4/ZvX5LPgn1D4lT5Jj7HodCbdeRS6dX88unH06Qc3mkA==",
+ "requires": {
+ "ajv": "~6.12.3",
+ "util": "^0.12.4",
+ "uuid": "~8.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": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+ },
+ "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.4.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
+ "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="
+ },
+ "cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+ },
+ "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"
+ }
+ },
+ "define-properties": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "requires": {
+ "object-keys": "^1.0.12"
+ }
+ },
+ "depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
+ },
+ "destroy": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+ },
+ "ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
+ },
+ "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"
+ }
+ },
+ "es-abstract": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz",
+ "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==",
+ "requires": {
+ "call-bind": "^1.0.2",
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.1.1",
+ "get-symbol-description": "^1.0.0",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.2",
+ "internal-slot": "^1.0.3",
+ "is-callable": "^1.2.4",
+ "is-negative-zero": "^2.0.1",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.1",
+ "is-string": "^1.0.7",
+ "is-weakref": "^1.0.1",
+ "object-inspect": "^1.11.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.2",
+ "string.prototype.trimend": "^1.0.4",
+ "string.prototype.trimstart": "^1.0.4",
+ "unbox-primitive": "^1.0.1"
+ }
+ },
+ "es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
+ },
+ "escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+ },
+ "etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+ },
+ "express": {
+ "version": "4.17.3",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz",
+ "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==",
+ "requires": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.19.2",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.4.2",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.1.2",
+ "fresh": "0.5.2",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.9.7",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.17.2",
+ "serve-static": "1.14.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~1.5.0",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ }
+ },
+ "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-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
+ },
+ "finalhandler": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+ "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+ "requires": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "statuses": "~1.5.0",
+ "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"
+ }
+ },
+ "foreach": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
+ "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k="
+ },
+ "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": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
+ "get-intrinsic": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
+ "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1"
+ }
+ },
+ "get-symbol-description": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
+ "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+ "requires": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ }
+ },
+ "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-bigints": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
+ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA=="
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+ },
+ "has-symbols": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
+ "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
+ },
+ "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": "1.8.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+ "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+ "requires": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.1"
+ }
+ },
+ "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"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "internal-slot": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
+ "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
+ "requires": {
+ "get-intrinsic": "^1.1.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ }
+ },
+ "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": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
+ },
+ "is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "requires": {
+ "has-bigints": "^1.0.1"
+ }
+ },
+ "is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-callable": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
+ "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w=="
+ },
+ "is-core-module": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
+ "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "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-negative-zero": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+ "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA=="
+ },
+ "is-number-object": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz",
+ "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==",
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-shared-array-buffer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz",
+ "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA=="
+ },
+ "is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
+ "is-typed-array": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.8.tgz",
+ "integrity": "sha512-HqH41TNZq2fgtGT8WHVFVJhBVGuY3AnP3Q36K8JKXUxSxRgk/d+7NjmwG2vo2mYmXK8UYZKu0qH8bVP5gEisjA==",
+ "requires": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "es-abstract": "^1.18.5",
+ "foreach": "^2.0.5",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-weakref": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "requires": {
+ "call-bind": "^1.0.2"
+ }
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "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": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+ },
+ "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=="
+ },
+ "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"
+ }
+ },
+ "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"
+ }
+ },
+ "media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+ },
+ "merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ },
+ "methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+ },
+ "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.51.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
+ "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="
+ },
+ "mime-types": {
+ "version": "2.1.34",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
+ "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
+ "requires": {
+ "mime-db": "1.51.0"
+ }
+ },
+ "minimist": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
+ },
+ "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-inspect": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
+ "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g=="
+ },
+ "object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
+ },
+ "object.assign": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
+ "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+ "requires": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3",
+ "has-symbols": "^1.0.1",
+ "object-keys": "^1.1.1"
+ }
+ },
+ "on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
+ "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-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": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ },
+ "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"
+ }
+ },
+ "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.9.7",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz",
+ "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw=="
+ },
+ "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.4.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz",
+ "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==",
+ "requires": {
+ "bytes": "3.1.2",
+ "http-errors": "1.8.1",
+ "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"
+ }
+ },
+ "resolve": {
+ "version": "1.22.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
+ "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
+ "requires": {
+ "is-core-module": "^2.8.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ }
+ },
+ "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=="
+ },
+ "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.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "send": {
+ "version": "0.17.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
+ "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
+ "requires": {
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "1.8.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.1",
+ "statuses": "~1.5.0"
+ },
+ "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.14.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
+ "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==",
+ "requires": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.17.2"
+ }
+ },
+ "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"
+ }
+ },
+ "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.11",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz",
+ "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g=="
+ },
+ "statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
+ },
+ "string.prototype.trimend": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
+ "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ },
+ "string.prototype.trimstart": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
+ "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ },
+ "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"
+ }
+ },
+ "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=="
+ },
+ "toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
+ },
+ "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"
+ }
+ },
+ "unbox-primitive": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
+ "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==",
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has-bigints": "^1.0.1",
+ "has-symbols": "^1.0.2",
+ "which-boxed-primitive": "^1.0.2"
+ }
+ },
+ "unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+ },
+ "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.4",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz",
+ "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==",
+ "requires": {
+ "inherits": "^2.0.3",
+ "is-arguments": "^1.0.4",
+ "is-generator-function": "^1.0.7",
+ "is-typed-array": "^1.1.3",
+ "safe-buffer": "^5.1.2",
+ "which-typed-array": "^1.1.2"
+ }
+ },
+ "utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+ },
+ "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": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+ },
+ "which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "requires": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ }
+ },
+ "which-typed-array": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.7.tgz",
+ "integrity": "sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw==",
+ "requires": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "es-abstract": "^1.18.5",
+ "foreach": "^2.0.5",
+ "has-tostringtag": "^1.0.0",
+ "is-typed-array": "^1.1.7"
+ }
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ }
+ }
+}
diff --git a/examples/serverless/api-gateway/function/package.json b/blueprints/serverless/api-gateway/function/package.json
similarity index 100%
rename from examples/serverless/api-gateway/function/package.json
rename to blueprints/serverless/api-gateway/function/package.json
diff --git a/blueprints/serverless/api-gateway/main.tf b/blueprints/serverless/api-gateway/main.tf
new file mode 100644
index 0000000000..fc1b4aa162
--- /dev/null
+++ b/blueprints/serverless/api-gateway/main.tf
@@ -0,0 +1,135 @@
+/**
+ * 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 {
+ api_id_prefix = "api"
+ function_name_prefix = "cf-hello"
+ specs = { for region in var.regions : region =>
+ templatefile("${path.module}/spec.yaml", {
+ api_id = "${local.api_id_prefix}-${region}"
+ function_name = "${local.function_name_prefix}-${region}"
+ region = region
+ project_id = var.project_id
+ })
+ }
+ backends = [for region in var.regions : {
+ group = google_compute_region_network_endpoint_group.serverless-negs[region].id
+ options = null
+ }
+ ]
+}
+
+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 = [
+ "apigateway.googleapis.com",
+ "cloudbuild.googleapis.com",
+ "cloudfunctions.googleapis.com",
+ "compute.googleapis.com",
+ "servicemanagement.googleapis.com",
+ "servicecontrol.googleapis.com"
+ ]
+ project_create = var.project_create != null
+}
+
+module "sa" {
+ source = "../../../modules/iam-service-account"
+ project_id = module.project.project_id
+ name = "sa-api"
+}
+
+
+module "functions" {
+ for_each = toset(var.regions)
+ source = "../../../modules/cloud-function"
+ project_id = module.project.project_id
+ name = "${local.function_name_prefix}-${each.value}"
+ bucket_name = "bkt-${module.project.project_id}-${each.value}"
+ region = each.value
+ ingress_settings = "ALLOW_ALL"
+ bucket_config = {
+ location = null
+ lifecycle_delete_age_days = 1
+ }
+ bundle_config = {
+ source_dir = "${path.module}/function"
+ output_path = "${path.module}/bundle.zip"
+ excludes = null
+ }
+ function_config = {
+ entry_point = "helloGET"
+ instances = null
+ memory = null
+ runtime = "nodejs16"
+ timeout = null
+ }
+ service_account_create = true
+ iam = {
+ "roles/cloudfunctions.invoker" = [module.sa.iam_email]
+ }
+}
+
+module "gateways" {
+ for_each = toset(var.regions)
+ source = "../../../modules/api-gateway"
+ project_id = module.project.project_id
+ api_id = "${local.api_id_prefix}-${each.value}"
+ region = each.value
+ spec = local.specs[each.value]
+ service_account_email = module.sa.email
+}
+
+module "glb" {
+ source = "../../../modules/net-glb"
+ project_id = module.project.project_id
+ name = "glb"
+ backend_service_configs = {
+ default = {
+ backends = [
+ for region in var.regions : {
+ backend = google_compute_region_network_endpoint_group.serverless-negs[region].id
+ }
+ ]
+ health_checks = []
+ }
+ }
+}
+
+resource "google_compute_region_network_endpoint_group" "serverless-negs" {
+ for_each = toset(var.regions)
+ provider = google-beta
+ name = "serverless-neg-${module.gateways[each.value].gateway_id}"
+ project = module.project.project_id
+ network_endpoint_type = "SERVERLESS"
+ region = each.value
+ serverless_deployment {
+ platform = "apigateway.googleapis.com"
+ resource = module.gateways[each.value].gateway_id
+ url_mask = ""
+ }
+ lifecycle {
+ create_before_destroy = true
+ }
+}
diff --git a/blueprints/serverless/api-gateway/outputs.tf b/blueprints/serverless/api-gateway/outputs.tf
new file mode 100644
index 0000000000..0eec77d89d
--- /dev/null
+++ b/blueprints/serverless/api-gateway/outputs.tf
@@ -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.
+ */
+
+output "ip_address" {
+ description = "The reserved global IP address."
+ value = module.glb.address
+}
diff --git a/examples/serverless/api-gateway/spec.yaml b/blueprints/serverless/api-gateway/spec.yaml
similarity index 100%
rename from examples/serverless/api-gateway/spec.yaml
rename to blueprints/serverless/api-gateway/spec.yaml
diff --git a/examples/serverless/api-gateway/variables.tf b/blueprints/serverless/api-gateway/variables.tf
similarity index 100%
rename from examples/serverless/api-gateway/variables.tf
rename to blueprints/serverless/api-gateway/variables.tf
diff --git a/blueprints/third-party-solutions/README.md b/blueprints/third-party-solutions/README.md
new file mode 100644
index 0000000000..c7cbec7379
--- /dev/null
+++ b/blueprints/third-party-solutions/README.md
@@ -0,0 +1,17 @@
+# Third Party Solutions
+
+The blueprints in this folder show how to automate installation of specific third party products on GCP, following typical best practices.
+
+## Blueprints
+
+### OpenShift cluster bootstrap on Shared VPC
+
+ This [example](./openshift/) shows how to quickly bootstrap an OpenShift 4.7 cluster on GCP, using typical enterprise features like Shared VPC and CMEK for instance disks.
+
+string
| ✓ | |
+| [domain](variables.tf#L39) | Domain name used to derive the DNS zone. | string
| ✓ | |
+| [fs_paths](variables.tf#L44) | Filesystem paths for commands and data, supports home path expansion. | object({…})
| ✓ | |
+| [host_project](variables.tf#L55) | Shared VPC project and network configuration. | object({…})
| ✓ | |
+| [service_project](variables.tf#L125) | Service project configuration. | object({…})
| ✓ | |
+| [allowed_ranges](variables.tf#L17) | Ranges that can SSH to the boostrap VM and API endpoint. | list(any)
| | ["10.0.0.0/8"]
|
+| [disk_encryption_key](variables.tf#L28) | Optional CMEK for disk encryption. | object({…})
| | null
|
+| [install_config_params](variables.tf#L68) | OpenShift cluster configuration. | object({…})
| | {…}
|
+| [post_bootstrap_config](variables.tf#L103) | Name of the service account for the machine operator. Removes bootstrap resources when set. | object({…})
| | null
|
+| [region](variables.tf#L111) | Region where resources will be created. | string
| | "europe-west1"
|
+| [rhcos_gcp_image](variables.tf#L117) | RHCOS image used. | string
| | "projects/rhcos-cloud/global/images/rhcos-47-83-202102090044-0-gcp-x86-64"
|
+| [tags](variables.tf#L132) | Additional tags for instances. | list(string)
| | ["ssh"]
|
+| [zones](variables.tf#L138) | Zones used for instances. | list(string)
| | ["b", "c", "d"]
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [backend-health](outputs.tf#L17) | Command to monitor API internal backend health. | |
+| [bootstrap-ssh](outputs.tf#L27) | Command to SSH to the bootstrap instance. | |
+| [masters-ssh](outputs.tf#L37) | Command to SSH to the master instances. | |
+
+
diff --git a/examples/third-party-solutions/openshift/tf/bootstrap.tf b/blueprints/third-party-solutions/openshift/tf/bootstrap.tf
similarity index 100%
rename from examples/third-party-solutions/openshift/tf/bootstrap.tf
rename to blueprints/third-party-solutions/openshift/tf/bootstrap.tf
diff --git a/examples/third-party-solutions/openshift/tf/dns.tf b/blueprints/third-party-solutions/openshift/tf/dns.tf
similarity index 100%
rename from examples/third-party-solutions/openshift/tf/dns.tf
rename to blueprints/third-party-solutions/openshift/tf/dns.tf
diff --git a/examples/third-party-solutions/openshift/tf/firewall.tf b/blueprints/third-party-solutions/openshift/tf/firewall.tf
similarity index 100%
rename from examples/third-party-solutions/openshift/tf/firewall.tf
rename to blueprints/third-party-solutions/openshift/tf/firewall.tf
diff --git a/examples/third-party-solutions/openshift/tf/iam.tf b/blueprints/third-party-solutions/openshift/tf/iam.tf
similarity index 100%
rename from examples/third-party-solutions/openshift/tf/iam.tf
rename to blueprints/third-party-solutions/openshift/tf/iam.tf
diff --git a/examples/third-party-solutions/openshift/tf/ilb.tf b/blueprints/third-party-solutions/openshift/tf/ilb.tf
similarity index 100%
rename from examples/third-party-solutions/openshift/tf/ilb.tf
rename to blueprints/third-party-solutions/openshift/tf/ilb.tf
diff --git a/examples/third-party-solutions/openshift/tf/main.tf b/blueprints/third-party-solutions/openshift/tf/main.tf
similarity index 100%
rename from examples/third-party-solutions/openshift/tf/main.tf
rename to blueprints/third-party-solutions/openshift/tf/main.tf
diff --git a/examples/third-party-solutions/openshift/tf/masters.tf b/blueprints/third-party-solutions/openshift/tf/masters.tf
similarity index 100%
rename from examples/third-party-solutions/openshift/tf/masters.tf
rename to blueprints/third-party-solutions/openshift/tf/masters.tf
diff --git a/examples/third-party-solutions/openshift/tf/outputs.tf b/blueprints/third-party-solutions/openshift/tf/outputs.tf
similarity index 100%
rename from examples/third-party-solutions/openshift/tf/outputs.tf
rename to blueprints/third-party-solutions/openshift/tf/outputs.tf
diff --git a/blueprints/third-party-solutions/openshift/tf/variables.tf b/blueprints/third-party-solutions/openshift/tf/variables.tf
new file mode 100644
index 0000000000..ee90bfef81
--- /dev/null
+++ b/blueprints/third-party-solutions/openshift/tf/variables.tf
@@ -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.
+ */
+
+variable "allowed_ranges" {
+ description = "Ranges that can SSH to the boostrap VM and API endpoint."
+ type = list(any)
+ default = ["10.0.0.0/8"]
+}
+
+variable "cluster_name" {
+ description = "Name used for the cluster and DNS zone."
+ type = string
+
+}
+variable "disk_encryption_key" {
+ description = "Optional CMEK for disk encryption."
+ type = object({
+ keyring = string
+ location = string
+ name = string
+ project_id = string
+ })
+ default = null
+}
+
+variable "domain" {
+ description = "Domain name used to derive the DNS zone."
+ type = string
+}
+
+variable "fs_paths" {
+ description = "Filesystem paths for commands and data, supports home path expansion."
+ type = object({
+ credentials = string
+ config_dir = string
+ openshift_install = string
+ pull_secret = string
+ ssh_key = string
+ })
+}
+
+variable "host_project" {
+ description = "Shared VPC project and network configuration."
+ type = object({
+ default_subnet_name = string
+ masters_subnet_name = string
+ project_id = string
+ vpc_name = string
+ workers_subnet_name = string
+ })
+}
+
+# https://github.com/openshift/installer/blob/master/docs/user/customization.md
+
+variable "install_config_params" {
+ description = "OpenShift cluster configuration."
+ type = object({
+ disk_size = number
+ labels = map(string)
+ network = object({
+ cluster = string
+ host_prefix = number
+ machine = string
+ service = string
+ })
+ proxy = object({
+ http = string
+ https = string
+ noproxy = string
+ })
+ })
+ default = {
+ disk_size = 16
+ labels = {}
+ network = {
+ cluster = "10.128.0.0/14"
+ host_prefix = 23
+ machine = "10.0.0.0/16"
+ service = "172.30.0.0/16"
+ }
+ proxy = null
+ }
+}
+
+
+# oc -n openshift-cloud-credential-operator get CredentialsRequest \
+# openshift-machine-api-gcp \
+# -o jsonpath='{.status.providerStatus.serviceAccountID}{"\n"}'
+
+variable "post_bootstrap_config" {
+ description = "Name of the service account for the machine operator. Removes bootstrap resources when set."
+ type = object({
+ machine_op_sa_prefix = string
+ })
+ default = null
+}
+
+variable "region" {
+ description = "Region where resources will be created."
+ type = string
+ default = "europe-west1"
+}
+
+variable "rhcos_gcp_image" {
+ description = "RHCOS image used."
+ type = string
+ # okd
+ # default = "projects/fedora-coreos-cloud/global/images/fedora-coreos-33-20210217-3-0-gcp-x86-64"
+ default = "projects/rhcos-cloud/global/images/rhcos-47-83-202102090044-0-gcp-x86-64"
+}
+
+variable "service_project" {
+ description = "Service project configuration."
+ type = object({
+ project_id = string
+ })
+}
+
+variable "tags" {
+ description = "Additional tags for instances."
+ type = list(string)
+ default = ["ssh"]
+}
+
+variable "zones" {
+ description = "Zones used for instances."
+ type = list(string)
+ default = ["b", "c", "d"]
+}
diff --git a/blueprints/third-party-solutions/openshift/tf/versions.tf b/blueprints/third-party-solutions/openshift/tf/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/blueprints/third-party-solutions/openshift/tf/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/blueprints/third-party-solutions/wordpress/cloudrun/README.md b/blueprints/third-party-solutions/wordpress/cloudrun/README.md
new file mode 100644
index 0000000000..0ffcc395c2
--- /dev/null
+++ b/blueprints/third-party-solutions/wordpress/cloudrun/README.md
@@ -0,0 +1,147 @@
+# Wordpress deployment on Cloud Run
+
+43% of the Web is built on Wordpress. Because of its simplicity and versatility, Wordpress can be used for internal websites as well as customer facing e-commerce platforms in small to large businesses, while still offering security.
+
+This repository contains the necessary Terraform files to deploy a functioning new Wordpress website exposed to the public internet with minimal technical overhead.
+
+This architecture can be used for the following use cases and more:
+
+* Blog
+* Intranet / internal Wiki
+* E-commerce platform
+
+# Architecture
+
+![Wordpress on Cloud Run](images/architecture.png "Wordpress on Cloud Run")
+
+The main components that are deployed in this architecture are the following (you can learn about them by following the hyperlinks):
+
+* [Cloud Run](https://cloud.google.com/run): serverless PaaS offering to host containers for web-oriented applications, while offering security, scalability and easy versioning
+* [Cloud SQL](https://cloud.google.com/sql): Managed solution for SQL databases
+* [VPC Serverless Connector](https://cloud.google.com/vpc/docs/serverless-vpc-access): Solution to access the CloudSQL VPC from Cloud Run, using only internal IP addresses
+
+# Setup
+
+## Prerequisites
+
+### Setting up the project for the deployment
+
+This example will deploy all its resources into the project defined by the `project_id` variable. Please note that we assume this project already exists. However, if you provide the appropriate values to the `project_create` variable, the project will be created as part of the deployment.
+
+If `project_create` is left to null, the identity performing the deployment needs the `owner` role on the project defined by the `project_id` variable. Otherwise, the identity performing the deployment needs `resourcemanager.projectCreator` on the resource hierarchy node specified by `project_create.parent` and `billing.user` on the billing account specified by `project_create.billing_account_id`.
+
+## Deployment
+
+### Step 0: Cloning the repository
+
+If you want to deploy from your Cloud Shell, click on the image below, sign in if required and when the prompt appears, click on “confirm”.
+
+[![Open Cloudshell](../../../../assets/images/cloud-shell-button.png)](https://shell.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2FGoogleCloudPlatform%2Fcloud-foundation-fabric&cloudshell_workspace=blueprints%2Fthird-party-solutions%2Fwordpress%2Fcloudrun)
+
+Otherwise, in your console of choice:
+
+```bash
+git clone https://github.com/GoogleCloudPlatform/cloud-foundation-fabric
+```
+
+Before you deploy the architecture, you will need at least the following information (for more precise configuration see the Variables section):
+
+* The project ID.
+* A Google Cloud Registry path to a Wordpress container image.
+
+### Step 1: Add Wordpress image
+
+In order to deploy the Wordpress service to Cloud Run, you need to store the [Wordpress image](https://hub.docker.com/r/bitnami/wordpress/) in Google Cloud Registry (GCR).
+
+Make sure that the Google Container Registry API is enabled and run the following commands in your Cloud Shell environment with your `project_id` in place of the `MY_PROJECT` placeholder:
+
+``` {shell}
+docker pull bitnami/wordpress:6.0.2
+docker tag bitnami/wordpress:6.0.2 gcr.io/MY_PROJECT/wordpress
+docker push gcr.io/MY_PROJECT/wordpress
+```
+
+**Note**: This example has been built for this particular Docker image. If you decide to use another one, this example might not work (or you can edit the variables in the Terraform files).
+
+### Step 2: Prepare the variables
+
+Once you have the required information, head back to your cloned repository. Make sure you’re in the directory of this tutorial (where this README is in).
+
+Configure the Terraform variables in your `terraform.tfvars` file. See [terraform.tfvars.sample](terraform.tfvars.sample) as starting point - just copy it to `terraform.tfvars` and edit the latter. See the variables documentation below.
+
+**Notes**:
+
+1. If you will want to change your admin password later on, please note that it will only work in the admin interface of Wordpress, but not with redeploying with Terraform, since Wordpress writes that password into the database upon installation and ignores the environment variables (that you can change with Terraform) after that.
+2. If you have the [domain restriction org. policy](https://cloud.google.com/resource-manager/docs/organization-policy/restricting-domains) on your organization, you have to edit the `cloud_run_invoker` variable and give it a value that will be accepted in accordance to your policy.
+
+### Step 3: Deploy resources
+
+Initialize your Terraform environment and deploy the resources:
+
+``` {shell}
+terraform init
+terraform apply
+```
+
+The resource creation will take a few minutes.
+
+**Note**: you might get the following error (or a similar one):
+
+``` {shell}
+│ Error: resource is in failed state "Ready:False", message: Revision '...' is not ready and cannot serve traffic.│
+```
+
+You might try to reapply at this point, the Cloud Run service just needs several minutes.
+
+### Step 4: Use the created resources
+
+Upon completion, you will see the output with the values for the Cloud Run service and the user and password to access the `/admin` part of the website. You can also view it later with:
+
+``` {shell}
+terraform output
+# or for the concrete variable:
+terraform output cloud_run_service
+```
+
+1. Open your browser at the URL that you get with that last command, and you will see your Wordpress installation.
+2. Add "/admin" in the end of the URL and log in to the admin interface, using the outputs "wp_user" and "wp_password".
+
+## Cleaning up your environment
+
+The easiest way to remove all the deployed resources is to run the following command in Cloud Shell:
+
+``` {shell}
+terraform destroy
+```
+
+The above command will delete the associated resources so there will be no billable charges made afterwards.
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [prefix](variables.tf#L57) | Prefix used for resource names. | string
| ✓ | |
+| [project_id](variables.tf#L81) | Project id, references existing project if `project_create` is null. | string
| ✓ | |
+| [wordpress_image](variables.tf#L92) | Image to run with Cloud Run, starts with \"gcr.io\". | string
| ✓ | |
+| [cloud_run_invoker](variables.tf#L18) | IAM member authorized to access the end-point (for example, 'user:YOUR_IAM_USER' for only you or 'allUsers' for everyone). | string
| | "allUsers"
|
+| [cloudsql_password](variables.tf#L24) | CloudSQL password (will be randomly generated by default). | string
| | null
|
+| [connector](variables.tf#L30) | Existing VPC serverless connector to use if not creating a new one. | string
| | null
|
+| [create_connector](variables.tf#L36) | Should a VPC serverless connector be created or not. | bool
| | true
|
+| [ip_ranges](variables.tf#L43) | CIDR blocks: VPC serverless connector, Private Service Access(PSA) for CloudSQL, CloudSQL VPC. | object({…})
| | {…}
|
+| [principals](variables.tf#L66) | List of users to give rights to (CloudSQL admin, client and instanceUser, Logging admin, Service Account User and TokenCreator), eg 'user@domain.com'. | list(string)
| | []
|
+| [project_create](variables.tf#L72) | Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…})
| | null
|
+| [region](variables.tf#L86) | Region for the created resources. | string
| | "europe-west4"
|
+| [wordpress_password](variables.tf#L97) | Password for the Wordpress user (will be randomly generated by default). | string
| | null
|
+| [wordpress_port](variables.tf#L103) | Port for the Wordpress image. | number
| | 8080
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [cloud_run_service](outputs.tf#L17) | CloudRun service URL. | ✓ |
+| [cloudsql_password](outputs.tf#L23) | CloudSQL password. | ✓ |
+| [wp_password](outputs.tf#L29) | Wordpress user password. | ✓ |
+| [wp_user](outputs.tf#L35) | Wordpress username. | |
+
+
diff --git a/blueprints/third-party-solutions/wordpress/cloudrun/cloudsql.tf b/blueprints/third-party-solutions/wordpress/cloudrun/cloudsql.tf
new file mode 100644
index 0000000000..39f40286ca
--- /dev/null
+++ b/blueprints/third-party-solutions/wordpress/cloudrun/cloudsql.tf
@@ -0,0 +1,66 @@
+/**
+ * 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 "random_password" "cloudsql_password" {
+ length = 8
+}
+
+# create a VPC for CloudSQL
+module "vpc" {
+ source = "../../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = "${var.prefix}-sql-vpc"
+ subnets = [
+ {
+ ip_cidr_range = var.ip_ranges.sql_vpc
+ name = "subnet"
+ region = var.region
+ }
+ ]
+ psa_config = {
+ ranges = {
+ cloud-sql = var.ip_ranges.psa
+ }
+ }
+}
+
+
+# create a VPC connector for the ClouSQL VPC
+resource "google_vpc_access_connector" "connector" {
+ count = var.create_connector ? 1 : 0
+ project = module.project.project_id
+ name = "${var.prefix}-wp-connector"
+ region = var.region
+ ip_cidr_range = var.ip_ranges.connector
+ network = module.vpc.self_link
+}
+
+
+# Set up CloudSQL
+module "cloudsql" {
+ source = "../../../../modules/cloudsql-instance"
+ project_id = module.project.project_id
+ network = module.vpc.self_link
+ name = "${var.prefix}-mysql"
+ region = var.region
+ database_version = local.cloudsql_conf.database_version
+ tier = local.cloudsql_conf.tier
+ databases = [local.cloudsql_conf.db]
+ users = {
+ "${local.cloudsql_conf.user}" = var.cloudsql_password
+ }
+}
diff --git a/blueprints/third-party-solutions/wordpress/cloudrun/images/architecture.png b/blueprints/third-party-solutions/wordpress/cloudrun/images/architecture.png
new file mode 100644
index 0000000000..ad914ecc25
Binary files /dev/null and b/blueprints/third-party-solutions/wordpress/cloudrun/images/architecture.png differ
diff --git a/blueprints/third-party-solutions/wordpress/cloudrun/main.tf b/blueprints/third-party-solutions/wordpress/cloudrun/main.tf
new file mode 100644
index 0000000000..04027790da
--- /dev/null
+++ b/blueprints/third-party-solutions/wordpress/cloudrun/main.tf
@@ -0,0 +1,119 @@
+/**
+ * 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 {
+ all_principals_iam = [for k in var.principals : "user:${k}"]
+ cloudsql_conf = {
+ database_version = "MYSQL_8_0"
+ tier = "db-g1-small"
+ db = "wp-mysql"
+ user = "admin"
+ }
+ iam = {
+ # CloudSQL
+ "roles/cloudsql.admin" = local.all_principals_iam
+ "roles/cloudsql.client" = local.all_principals_iam
+ "roles/cloudsql.instanceUser" = local.all_principals_iam
+ # common roles
+ "roles/logging.admin" = local.all_principals_iam
+ "roles/iam.serviceAccountUser" = local.all_principals_iam
+ "roles/iam.serviceAccountTokenCreator" = local.all_principals_iam
+ }
+ connector = var.connector == null ? google_vpc_access_connector.connector.0.self_link : var.connector
+ wp_user = "user"
+ wp_pass = var.wordpress_password == null ? random_password.wp_password.result : var.wordpress_password
+}
+
+
+# either create a project or set up the given one
+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.project_create == null ? null : var.prefix
+ iam = var.project_create != null ? local.iam : {}
+ iam_additive = var.project_create == null ? local.iam : {}
+ services = [
+ "run.googleapis.com",
+ "logging.googleapis.com",
+ "monitoring.googleapis.com",
+ "sqladmin.googleapis.com",
+ "sql-component.googleapis.com",
+ "vpcaccess.googleapis.com",
+ "servicenetworking.googleapis.com"
+ ]
+}
+
+
+resource "random_password" "wp_password" {
+ length = 8
+}
+
+
+# create the Cloud Run service
+module "cloud_run" {
+ source = "../../../../modules/cloud-run"
+ project_id = module.project.project_id
+ name = "${var.prefix}-cr-wordpress"
+ region = var.region
+
+ containers = [{
+ image = var.wordpress_image
+ ports = [{
+ name = "http1"
+ protocol = null
+ container_port = var.wordpress_port
+ }]
+ options = {
+ command = null
+ args = null
+ env_from = null
+ # set up the database connection
+ env = {
+ "APACHE_HTTP_PORT_NUMBER" : var.wordpress_port
+ "WORDPRESS_DATABASE_HOST" : module.cloudsql.ip
+ "WORDPRESS_DATABASE_NAME" : local.cloudsql_conf.db
+ "WORDPRESS_DATABASE_USER" : local.cloudsql_conf.user
+ "WORDPRESS_DATABASE_PASSWORD" : var.cloudsql_password == null ? module.cloudsql.user_passwords[local.cloudsql_conf.user] : var.cloudsql_password
+ "WORDPRESS_USERNAME" : local.wp_user
+ "WORDPRESS_PASSWORD" : local.wp_pass
+ }
+ }
+ resources = null
+ volume_mounts = null
+ }]
+
+ iam = {
+ "roles/run.invoker" : [var.cloud_run_invoker]
+ }
+
+ revision_annotations = {
+ autoscaling = {
+ min_scale = 1
+ max_scale = 2
+ }
+ # connect to CloudSQL
+ cloudsql_instances = [module.cloudsql.connection_name]
+ vpcaccess_connector = null
+ # allow all traffic
+ vpcaccess_egress = "all-traffic"
+ vpcaccess_connector = local.connector
+ }
+ ingress_settings = "all"
+}
diff --git a/blueprints/third-party-solutions/wordpress/cloudrun/outputs.tf b/blueprints/third-party-solutions/wordpress/cloudrun/outputs.tf
new file mode 100644
index 0000000000..b08642c7b1
--- /dev/null
+++ b/blueprints/third-party-solutions/wordpress/cloudrun/outputs.tf
@@ -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.
+ */
+
+output "cloud_run_service" {
+ description = "CloudRun service URL."
+ value = module.cloud_run.service.status[0].url
+ sensitive = true
+}
+
+output "cloudsql_password" {
+ description = "CloudSQL password."
+ value = var.cloudsql_password == null ? module.cloudsql.user_passwords[local.cloudsql_conf.user] : var.cloudsql_password
+ sensitive = true
+}
+
+output "wp_password" {
+ description = "Wordpress user password."
+ value = local.wp_pass
+ sensitive = true
+}
+
+output "wp_user" {
+ description = "Wordpress username."
+ value = local.wp_user
+}
diff --git a/blueprints/third-party-solutions/wordpress/cloudrun/terraform.tfvars.sample b/blueprints/third-party-solutions/wordpress/cloudrun/terraform.tfvars.sample
new file mode 100644
index 0000000000..5c71954c81
--- /dev/null
+++ b/blueprints/third-party-solutions/wordpress/cloudrun/terraform.tfvars.sample
@@ -0,0 +1,3 @@
+prefix = "wp"
+project_id = "my-wordpress-project"
+wordpress_image = "gcr.io/my-wordpress-project/wordpress"
diff --git a/blueprints/third-party-solutions/wordpress/cloudrun/variables.tf b/blueprints/third-party-solutions/wordpress/cloudrun/variables.tf
new file mode 100644
index 0000000000..abb00d2d1c
--- /dev/null
+++ b/blueprints/third-party-solutions/wordpress/cloudrun/variables.tf
@@ -0,0 +1,107 @@
+/**
+ * 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.
+ */
+
+# Documentation: https://cloud.google.com/run/docs/securing/managing-access#making_a_service_public
+variable "cloud_run_invoker" {
+ type = string
+ description = "IAM member authorized to access the end-point (for example, 'user:YOUR_IAM_USER' for only you or 'allUsers' for everyone)."
+ default = "allUsers"
+}
+
+variable "cloudsql_password" {
+ type = string
+ description = "CloudSQL password (will be randomly generated by default)."
+ default = null
+}
+
+variable "connector" {
+ type = string
+ description = "Existing VPC serverless connector to use if not creating a new one."
+ default = null
+}
+
+variable "create_connector" {
+ type = bool
+ description = "Should a VPC serverless connector be created or not."
+ default = true
+}
+
+# PSA: documentation: https://cloud.google.com/vpc/docs/configure-private-services-access#allocating-range
+variable "ip_ranges" {
+ description = "CIDR blocks: VPC serverless connector, Private Service Access(PSA) for CloudSQL, CloudSQL VPC."
+ type = object({
+ connector = string
+ psa = string
+ sql_vpc = string
+ })
+ default = {
+ connector = "10.8.0.0/28"
+ psa = "10.60.0.0/24"
+ sql_vpc = "10.0.0.0/20"
+ }
+}
+
+variable "prefix" {
+ description = "Prefix used for resource names."
+ type = string
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty."
+ }
+}
+
+variable "principals" {
+ description = "List of users to give rights to (CloudSQL admin, client and instanceUser, Logging admin, Service Account User and TokenCreator), eg 'user@domain.com'."
+ type = list(string)
+ default = []
+}
+
+variable "project_create" {
+ description = "Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format."
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = null
+}
+
+variable "project_id" {
+ description = "Project id, references existing project if `project_create` is null."
+ type = string
+}
+
+variable "region" {
+ type = string
+ description = "Region for the created resources."
+ default = "europe-west4"
+}
+
+variable "wordpress_image" {
+ type = string
+ description = "Image to run with Cloud Run, starts with \"gcr.io\"."
+}
+
+variable "wordpress_password" {
+ type = string
+ description = "Password for the Wordpress user (will be randomly generated by default)."
+ default = null
+}
+
+variable "wordpress_port" {
+ type = number
+ description = "Port for the Wordpress image."
+ default = 8080
+}
diff --git a/default-versions.tf b/default-versions.tf
index 290412687a..90b632f6d4 100644
--- a/default-versions.tf
+++ b/default-versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/examples/README.md b/examples/README.md
deleted file mode 100644
index 59de8b3b80..0000000000
--- a/examples/README.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# Terraform end-to-end examples for Google Cloud
-
-This section contains **[foundational examples](./foundations/)** that bootstrap the organizational hierarchy and automation prerequisites, **[networking examples](./networking/)** that implement core patterns or features, **[data solutions examples](./data-solutions/)** that demonstrate how to integrate data services in complete scenarios, **[cloud operations examples](./cloud-operations/)** that leverage specific products to meet specific operational needs and **[factories](./factories/)** that implement resource factories for the repetitive creation of specific resources.
-
-Currently available examples:
-
-- **cloud operations** - [Resource tracking and remediation via Cloud Asset feeds](./cloud-operations/asset-inventory-feed-remediation), [Granular Cloud DNS IAM via Service Directory](./cloud-operations/dns-fine-grained-iam), [Granular Cloud DNS IAM for Shared VPC](./cloud-operations/dns-shared-vpc), [Compute Engine quota monitoring](./cloud-operations/quota-monitoring), [Scheduled Cloud Asset Inventory Export to Bigquery](./cloud-operations/scheduled-asset-inventory-export-bq), [Packer image builder](./cloud-operations/packer-image-builder), [On-prem SA key management](./cloud-operations/onprem-sa-key-management), [TCP healthcheck for unmanaged GCE instances](./cloud-operations/unmanaged-instances-healthcheck)
-- **data solutions** - [GCE/GCS CMEK via centralized Cloud KMS](./data-solutions/gcs-to-bq-with-least-privileges/), [Cloud Storage to Bigquery with Cloud Dataflow with least privileges](./data-solutions/gcs-to-bq-with-least-privileges/), [Data Platform Foundations](./data-solutions/data-platform-foundations/)
-- **factories** - [The why and the how of resource factories](./factories/README.md)
-- **foundations** - [single level hierarchy](./foundations/environments/) (environments), [multiple level hierarchy](./foundations/business-units/) (business units + environments)
-- **networking** - [hub and spoke via peering](./networking/hub-and-spoke-peering/), [hub and spoke via VPN](./networking/hub-and-spoke-vpn/), [DNS and Google Private Access for on-premises](./networking/onprem-google-access-dns/), [Shared VPC with GKE support](./networking/shared-vpc-gke/), [ILB as next hop](./networking/ilb-next-hop), [PSC for on-premises Cloud Function invocation](./networking/private-cloud-function-from-onprem/), [decentralized firewall](./networking/decentralized-firewall)
-- **third party solutions** - [OpenShift cluster on Shared VPC](./third-party-solutions/openshift)
-
-For more information see the README files in the [foundations](./foundations/), [networking](./networking/), [data solutions](./data-solutions/), [cloud operations](./cloud-operations/) and [factories](./factories/) folders.
diff --git a/examples/cloud-operations/README.md b/examples/cloud-operations/README.md
deleted file mode 100644
index 79bac6a37f..0000000000
--- a/examples/cloud-operations/README.md
+++ /dev/null
@@ -1,64 +0,0 @@
-# Operations examples
-
-The examples in this folder show how to wire together different Google Cloud services to simplify operations, and are meant for testing, or as minimal but sufficiently complete starting points for actual use.
-
-## Resource tracking and remediation via Cloud Asset feeds
-
- This [example](./asset-inventory-feed-remediation) shows how to leverage [Cloud Asset Inventory feeds](https://cloud.google.com/asset-inventory/docs/monitoring-asset-changes) to stream resource changes in real time, and how to programmatically use the feed change notifications for alerting or remediation, via a Cloud Function wired to the feed PubSub queue.
-
-The example's feed tracks changes to Google Compute instances, and the Cloud Function enforces policy compliance on each change so that tags match a set of simple rules. The obious use case is when instance tags are used to scope firewall rules, but the example can easily be adapted to suit different use cases.
-
-string
| ✓ | |
-| [bundle_path](variables.tf#L17) | Path used to write the intermediate Cloud Function code bundle. | string
| | "./bundle.zip"
|
-| [name](variables.tf#L23) | Arbitrary string used to name created resources. | string
| | "asset-feed"
|
-| [project_create](variables.tf#L29) | Create project instead of using an existing one. | bool
| | false
|
-| [region](variables.tf#L40) | Compute region used in the example. | string
| | "europe-west1"
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [cf_logs](outputs.tf#L17) | Cloud Function logs read command. | |
-| [subscription_pull](outputs.tf#L29) | Subscription pull command. | |
-| [tag_add](outputs.tf#L39) | Instance add tag command. | |
-| [tag_show](outputs.tf#L49) | Instance add tag command. | |
-
-
diff --git a/examples/cloud-operations/asset-inventory-feed-remediation/main.tf b/examples/cloud-operations/asset-inventory-feed-remediation/main.tf
deleted file mode 100644
index 1569ee1b33..0000000000
--- a/examples/cloud-operations/asset-inventory-feed-remediation/main.tf
+++ /dev/null
@@ -1,133 +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 {
- role_id = "projects/${var.project_id}/roles/${local.role_name}"
- role_name = "feeds_cf"
-}
-
-module "project" {
- source = "../../../modules/project"
- name = var.project_id
- project_create = var.project_create
- services = [
- "cloudasset.googleapis.com",
- "cloudbuild.googleapis.com",
- "cloudfunctions.googleapis.com",
- "compute.googleapis.com"
- ]
- service_config = {
- disable_on_destroy = false,
- disable_dependent_services = false
- }
- custom_roles = {
- (local.role_name) = [
- "compute.instances.list",
- "compute.instances.setTags",
- "compute.zones.list",
- "compute.zoneOperations.get",
- "compute.zoneOperations.list"
- ]
- }
- iam = {
- (local.role_id) = [module.service-account.iam_email]
- }
-}
-
-module "vpc" {
- source = "../../../modules/net-vpc"
- project_id = module.project.project_id
- name = var.name
- subnets = [{
- ip_cidr_range = "192.168.0.0/24"
- name = "${var.name}-default"
- region = var.region
- secondary_ip_range = {}
- }]
-}
-
-module "pubsub" {
- source = "../../../modules/pubsub"
- project_id = module.project.project_id
- name = var.name
- subscriptions = { "${var.name}-default" = null }
- iam = {
- "roles/pubsub.publisher" = [
- "serviceAccount:${module.project.service_accounts.robots.cloudasset}"
- ]
- }
-}
-
-module "service-account" {
- source = "../../../modules/iam-service-account"
- project_id = module.project.project_id
- name = "${var.name}-cf"
- # iam_project_roles = { (module.project.project_id) = [local.role_id] }
-}
-
-module "cf" {
- source = "../../../modules/cloud-function"
- project_id = module.project.project_id
- name = var.name
- bucket_name = "${var.name}-${random_pet.random.id}"
- bucket_config = {
- location = var.region
- lifecycle_delete_age = null
- }
- bundle_config = {
- source_dir = "cf"
- output_path = var.bundle_path
- excludes = null
- }
- service_account = module.service-account.email
- trigger_config = {
- event = "google.pubsub.topic.publish"
- resource = module.pubsub.topic.id
- retry = null
- }
-}
-
-module "simple-vm-example" {
- source = "../../../modules/compute-vm"
- project_id = module.project.project_id
- zone = "${var.region}-b"
- name = var.name
- network_interfaces = [{
- network = module.vpc.self_link
- subnetwork = try(module.vpc.subnet_self_links["${var.region}/${var.name}-default"], "")
- nat = false
- addresses = null
- }]
- tags = ["${var.project_id}-test-feed", "shared-test-feed"]
-}
-
-resource "random_pet" "random" {
- length = 1
-}
-
-# Create a feed that sends notifications about instance updates.
-resource "google_cloud_asset_project_feed" "project_feed" {
- project = module.project.project_id
- feed_id = var.name
- content_type = "RESOURCE"
- asset_types = ["compute.googleapis.com/Instance"]
-
- feed_output_config {
- pubsub_destination {
- topic = module.pubsub.topic.id
- }
- }
-}
diff --git a/examples/cloud-operations/asset-inventory-feed-remediation/versions.tf b/examples/cloud-operations/asset-inventory-feed-remediation/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/cloud-operations/asset-inventory-feed-remediation/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/cloud-operations/dns-fine-grained-iam/README.md b/examples/cloud-operations/dns-fine-grained-iam/README.md
deleted file mode 100644
index 5b7c949b37..0000000000
--- a/examples/cloud-operations/dns-fine-grained-iam/README.md
+++ /dev/null
@@ -1,120 +0,0 @@
-# Fine-grained Cloud DNS IAM via Service Directory
-
-This example shows how to leverage [Service Directory](https://cloud.google.com/blog/products/networking/introducing-service-directory) and Cloud DNS Service Directory private zones, to implement fine-grained IAM controls on DNS by
-
-- creating a Service Directory namespace with two services and their endpoints
-- creating a Cloud DNS private zone that uses the namespace as its authoritative source
-- creating two service accounts and assigning them the `roles/servicedirectory.editor` role on the namespace and on one service respectively
-- creating two VMs and setting them to use the two service accounts, so that DNS queries and `gcloud` commands can be used to verify the setup
-
-The resources created in this example are shown in the high level diagram below:
-
-
-
-A [companion Medium article](https://medium.com/google-cloud/fine-grained-cloud-dns-iam-via-service-directory-446058b4362e) has been published for this example, you can refer to it for more details on the context, and the specifics of running the example.
-
-## Running the example
-
-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=examples%2Fcloud-operations%2Fdns-fine-grained-iam&cloudshell_open_in_editor=cloudshell_open%2Fcloud-foundation-fabric%2Fexamples%2Fcloud-operations%2Fdns-fine-grained-iam%2Fvariables.tf), then go through the following steps to create resources:
-
-- `terraform init`
-- `terraform apply -var project_id=my-project-id`
-
-Once done testing, you can clean up resources by running `terraform destroy`. To persist state, check out the `backend.tf.sample` file.
-
-## Testing the example
-
-The terraform outputs generate preset `gcloud compute ssh` commands that you can copy and run in the console to connect to each VM. Remember to adapt the testing commands below if you changed the default values for the `name`, `region`, or `zone_domain` variables.
-
-Connect via SSH to the `ns` VM and query the Service Directory namespace via DNS.
-
-```bash
-gcloud compute ssh dns-sd-test-ns-1 \
- --zone europe-west1-b \
- --tunnel-through-iap
-
-dig app1.svc.example.org +short
-# 127.0.0.2
-# 127.0.0.3
-# 127.0.0.7
-dig app2.svc.example.org +short
-# 127.0.0.4
-# 127.0.0.5
-dig _app1._tcp.app1.svc.example.org srv +short
-# 10 10 80 vm1.app1.svc.example.org.
-# 10 10 80 vm2.app1.svc.example.org.
-# 10 10 80 vm3.app1.svc.example.org.
-```
-
-The DNS answers should match the ones in the comments above, after each command. Note the special format used to query `SRV` records.
-
-If the above looks good, let's verify that the `ns` VM service account has edit rights on the namespace by creating a new service, and then verifying it via DNS.
-
-```bash
-gcloud beta service-directory services create app3 \
- --location europe-west1 \
- --namespace dns-sd-test
-# Created service [app3].
-
-gcloud beta service-directory endpoints create vm1 \
- --service app3 \
- --location europe-west1 \
- --namespace dns-sd-test \
- --address 127.0.0.6 \
- --port 80
-# Created endpoint [vm1].
-
-dig app3.svc.example.org +short
-# 127.0.0.6
-```
-
-Log out from the `ns` VM and log in to the `svc` VM, then verify that its service account has no permissions on the whole namespace.
-
-```bash
-gcloud compute ssh dns-sd-test-svc-1 \
- --zone europe-west1-b \
- --tunnel-through-iap
-
-gcloud beta service-directory services delete app3 \
- --location europe-west1 \
- --namespace dns-sd-test
-# Deleted service [app3].
-# ERROR: (gcloud.beta.service-directory.services.delete) PERMISSION_DENIED: Permission 'servicedirectory.services.delete' denied on resource 'projects/my-project/locations/europe-west1/namespaces/dns-sd-test/services/app3'.
-```
-
-Ignoring the `deleted` message which is clearly a bug (the service is still in beta after all), the error message shows that this identity has no rights to operate on the namespace. What it can do is operate on the single service we gave it access to.
-
-```bash
-gcloud beta service-directory endpoints create vm3 \
- --service app1 \
- --location europe-west1 \
- --namespace dns-sd-test \
- --address 127.0.0.7 \
- --port 80
-# Created endpoint [vm3].
-
-dig app1.svc.example.org +short
-# 127.0.0.2
-# 127.0.0.3
-# 127.0.0.7
-```
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [project_id](variables.tf#L29) | Existing project id. | string
| ✓ | |
-| [name](variables.tf#L17) | Arbitrary string used to name created resources. | string
| | "dns-sd-test"
|
-| [project_create](variables.tf#L23) | Create project instead ofusing an existing one. | bool
| | false
|
-| [region](variables.tf#L34) | Compute region used in the example. | string
| | "europe-west1"
|
-| [zone_domain](variables.tf#L40) | Domain name used for the DNS zone. | string
| | "svc.example.org."
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [gcloud_commands](outputs.tf#L17) | Commands used to SSH to the VMs. | |
-| [vms](outputs.tf#L25) | VM names. | |
-
-
diff --git a/examples/cloud-operations/dns-fine-grained-iam/main.tf b/examples/cloud-operations/dns-fine-grained-iam/main.tf
deleted file mode 100644
index 773bbd56a5..0000000000
--- a/examples/cloud-operations/dns-fine-grained-iam/main.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.
- */
-
-locals {
- startup-script = <string
| ✓ | |
-| [folder_id](variables.tf#L28) | Folder ID in which DNS projects will be created. | string
| ✓ | |
-| [shared_vpc_link](variables.tf#L48) | Shared VPC self link, used for DNS peering. | string
| ✓ | |
-| [dns_domain](variables.tf#L22) | DNS domain under which each application team DNS domain will be created. | string
| | "example.org"
|
-| [prefix](variables.tf#L33) | Customer name to use as prefix for resources' naming. | string
| | "test-dns"
|
-| [project_services](variables.tf#L39) | Service APIs enabled by default. | list(string)
| | […]
|
-| [teams](variables.tf#L53) | List of application teams requiring their own Cloud DNS instance. | list(string)
| | […]
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [teams](outputs.tf#L17) | Team resources. | |
-
-
diff --git a/examples/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/variables.tf b/examples/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/variables.tf
deleted file mode 100644
index 7c0f7ed923..0000000000
--- a/examples/cloud-operations/dns-shared-vpc/examples/shared-vpc-example/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 "host_project" {
- description = "Host project name."
- default = "host"
-}
-
-variable "service_projects" {
- description = "List of service project names."
- type = list(any)
- default = [
- "app-team1",
- "app-team2",
- ]
-}
-
-variable "region" {
- description = "Region in which to create the subnet."
- default = "europe-west1"
-}
-
-variable "project_services" {
- description = "Service APIs enabled by default in new projects."
- default = [
- "compute.googleapis.com",
- "dns.googleapis.com",
- ]
-}
-
-variable "organization_id" {
- description = "The organization ID."
-}
-
-variable "billing_account" {
- description = "The ID of the billing account to associate this project with."
-}
-
-variable "prefix" {
- description = "Customer name to use as prefix for resources' naming."
- default = "test-dns"
-}
-
-variable "dns_domain" {
- description = "DNS domain under which each application team DNS domain will be created."
- default = "prod.internal"
-}
-
-variable "teams" {
- description = "List of teams that require their own Cloud DNS instance."
- default = ["appteam1", "appteam2"]
-}
diff --git a/examples/cloud-operations/dns-shared-vpc/main.tf b/examples/cloud-operations/dns-shared-vpc/main.tf
deleted file mode 100644
index b13e7595f1..0000000000
--- a/examples/cloud-operations/dns-shared-vpc/main.tf
+++ /dev/null
@@ -1,66 +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 {
- projects = {
- for k, v in module.project : k => v.project_id
- }
- svpc_project_id = regex("/projects/(.*?)/.*", var.shared_vpc_link)[0]
-}
-
-module "project" {
- source = "../../../modules/project"
- for_each = toset(var.teams)
- billing_account = var.billing_account_id
- name = each.value
- parent = var.folder_id
- prefix = var.prefix
- services = var.project_services
- service_config = {
- disable_on_destroy = false,
- disable_dependent_services = false
- }
-}
-
-module "vpc" {
- source = "../../../modules/net-vpc"
- for_each = local.projects
- project_id = each.value
- name = "dns-vpc"
-}
-
-module "dns-private" {
- source = "../../../modules/dns"
- for_each = local.projects
- project_id = each.value
- type = "private"
- name = each.key
- domain = "${each.key}.${var.dns_domain}."
- description = "DNS zone for ${each.key}"
- client_networks = [module.vpc[each.key].self_link]
-}
-
-module "dns-peering" {
- source = "../../../modules/dns"
- for_each = local.projects
- project_id = local.svpc_project_id
- name = "peering-${each.key}"
- domain = "${each.key}.${var.dns_domain}."
- description = "DNS peering for ${each.key}"
- type = "peering"
- peer_network = module.vpc[each.key].self_link
- client_networks = [var.shared_vpc_link]
-}
diff --git a/examples/cloud-operations/dns-shared-vpc/variables.tf b/examples/cloud-operations/dns-shared-vpc/variables.tf
deleted file mode 100644
index f74acfde00..0000000000
--- a/examples/cloud-operations/dns-shared-vpc/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 "billing_account_id" {
- description = "Billing account associated with the GCP Projects that will be created for each team."
- type = string
-}
-
-variable "dns_domain" {
- description = "DNS domain under which each application team DNS domain will be created."
- type = string
- default = "example.org"
-}
-
-variable "folder_id" {
- description = "Folder ID in which DNS projects will be created."
- type = string
-}
-
-variable "prefix" {
- description = "Customer name to use as prefix for resources' naming."
- type = string
- default = "test-dns"
-}
-
-variable "project_services" {
- description = "Service APIs enabled by default."
- type = list(string)
- default = [
- "compute.googleapis.com",
- "dns.googleapis.com",
- ]
-}
-
-variable "shared_vpc_link" {
- description = "Shared VPC self link, used for DNS peering."
- type = string
-}
-
-variable "teams" {
- description = "List of application teams requiring their own Cloud DNS instance."
- type = list(string)
- default = [
- "team1",
- "team2",
- ]
-}
diff --git a/examples/cloud-operations/dns-shared-vpc/versions.tf b/examples/cloud-operations/dns-shared-vpc/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/cloud-operations/dns-shared-vpc/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/cloud-operations/iam-delegated-role-grants/README.md b/examples/cloud-operations/iam-delegated-role-grants/README.md
deleted file mode 100644
index b48eddbcb1..0000000000
--- a/examples/cloud-operations/iam-delegated-role-grants/README.md
+++ /dev/null
@@ -1,78 +0,0 @@
-# Delegated Role Grants
-
-This example shows two applications of [delegated role grants](https://cloud.google.com/iam/docs/setting-limits-on-granting-roles):
-
-- how to use them to restrict service usage in a GCP project
-- how to use them to allow administrative access to a service via a predefined role, while restricting administrators from minting other admins.
-
-## Restricting service usage
-
-In its default configuration, the example provisions two sets of permissions:
-
-- the roles listed in `direct_role_grants` will be granted unconditionally to the users listed in `project_administrators`.
-- additionally, `project_administrators` will be granted the role `roles/resourcemanager.projectIamAdmin` in a restricted fashion, allowing them to only grant the roles listed in `delegated_role_grants` to other users.
-
-By carefully choosing `direct_role_grants` and `delegated_role_grants`, you can restrict which services can be used within the project while still giving enough freedom to project administrators to still grant permissions to other principals within their projects.
-
-This diagram shows the resources and expected behaviour:
-
-
-
-
-A [Medium article](https://medium.com/@jccb/managing-gcp-service-usage-through-delegated-role-grants-a843610f2226) has been published for this example, refer to it for more details on the context and the specifics of running the example.
-
-## Restricting a predefined role
-
-By changing the `restricted_role_grant`, the example can be used to grant administrators a predefined role like `roles/compute.networkAdmin`, which allows setting IAM policies on service resources like subnetworks, but restrict the roles that those administrators are able to confer to other users.
-
-You can easily configure the example for this use case:
-
-```hcl
-# terraform.tfvars
-
-delegated_role_grants = ["roles/compute.networkUser"]
-direct_role_grants = []
-restricted_role_grant = "roles/compute.networkAdmin"
-```
-
-This diagram shows the resources and expected behaviour:
-
-
-
-## Running the example
-
-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=examples%2Fcloud-operations%2Fiam-delegated-role-grants), then go through the following steps to create resources:
-
-- `terraform init`
-- `terraform apply -var project_id=my-project-id 'project_administrators=["user:project-admin@example.com"]'`
-
-Once done testing, you can clean up resources by running `terraform destroy`.
-
-## Auditing Roles
-
-This example includes a python script that audits a list of roles to ensure you're not granting the `setIamPolicy` permission at the project, folder or organization level. To audit all the predefined compute roles, run it like this:
-
-```bash
-pip3 install -r requirements.txt
-gcloud iam roles list --filter="name:roles/compute. stage=GA" --format="get(name)" > roles.txt
-python3 audit.py roles.txt
-```
-
-If you get any warnings, check the roles and remove any of them granting any of the following permissions:
-- `resourcemanager.projects.setIamPolicy`
-- `resourcemanager.folders.setIamPolicy`
-- `resourcemanager.organizations.setIamPolicy`
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [project_administrators](variables.tf#L62) | List identities granted administrator permissions. | list(string)
| ✓ | |
-| [project_id](variables.tf#L73) | GCP project id where to grant direct and delegated roles to the users listed in project_administrators. | string
| ✓ | |
-| [delegated_role_grants](variables.tf#L17) | List of roles that project administrators will be allowed to grant/revoke. | list(string)
| | […]
|
-| [direct_role_grants](variables.tf#L53) | List of roles granted directly to project administrators. | list(string)
| | […]
|
-| [project_create](variables.tf#L67) | Create project instead of using an existing one. | bool
| | false
|
-| [restricted_role_grant](variables.tf#L78) | Role grant to which the restrictions will apply. | string
| | "roles/resourcemanager.projectIamAdmin"
|
-
-
diff --git a/examples/cloud-operations/iam-delegated-role-grants/versions.tf b/examples/cloud-operations/iam-delegated-role-grants/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/cloud-operations/iam-delegated-role-grants/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/cloud-operations/onprem-sa-key-management/README.md b/examples/cloud-operations/onprem-sa-key-management/README.md
deleted file mode 100644
index dfa5ce71d5..0000000000
--- a/examples/cloud-operations/onprem-sa-key-management/README.md
+++ /dev/null
@@ -1,80 +0,0 @@
-# Managing on-prem service account keys by uploading public keys
-
-When managing GCP Service Accounts with terraform, it's often a question on **how to avoid Service Account Key in the terraform state?**
-
-This example shows how to manage IAM Service Account Keys by manually generating a key pair and uploading the public part of the key to GCP. It has the following benefits:
-
- - no [passing keys between users](https://cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys#pass-between-users) or systems
- - no private keys stored in the terraform state (only public part of the key is in the state)
- - let keys [expire automatically](https://cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys#key-expiryhaving)
-
-
-## Running the example
-
-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=examples%2Fcloud-operations%2Fonprem-sa-key-management&cloudshell_open_in_editor=cloudshell_open%2Fcloud-foundation-fabric%2Fexamples%2Fcloud-operations%2Fonprem-sa-key-management%2Fvariables.tf), then go through the following steps to create resources:
-
-Cleaning up example keys
-```bash
-rm -f /public-keys/data-uploader/
-rm -f /public-keys/prisma-security/
-```
-
-Generate keys for service accounts
-```bash
-mkdir keys && cd keys
-openssl req -x509 -nodes -newkey rsa:2048 -days 30 \
- -keyout data_uploader_private_key.pem \
- -out ../public-keys/data-uploader/public_key.pem \
- -subj "/CN=unused"
-openssl req -x509 -nodes -newkey rsa:2048 -days 30 \
- -keyout prisma_security_private_key.pem \
- -out ../public-keys/prisma-security/public_key.pem \
- -subj "/CN=unused"
-```
-
-Deploy service accounts and keys
-```bash
-cd ..
-terraform init
-terraform apply -var project_id=$GOOGLE_CLOUD_PROJECT
-
-```
-
-Extract JSON credentials templates from terraform output and put the private part of the keys into templates
-```bash
-terraform show -json | jq '.values.outputs."sa-credentials".value."data-uploader"."public_key.pem" | fromjson' > data-uploader.json
-terraform show -json | jq '.values.outputs."sa-credentials".value."prisma-security"."public_key.pem" | fromjson' > prisma-security.json
-
-contents=$(jq --arg key "$(cat keys/data_uploader_private_key.pem)" '.private_key=$key' data-uploader.json) && echo "$contents" > data-uploader.json
-contents=$(jq --arg key "$(cat keys/prisma_security_private_key.pem)" '.private_key=$key' prisma-security.json) && echo "$contents" > prisma-security.json
-```
-
-## Testing the example
-Validate that service accounts json credentials are valid
-```bash
-gcloud auth activate-service-account --key-file prisma-security.json
-gcloud auth activate-service-account --key-file data-uploader.json
-```
-
-## Cleaning up
-```bash
-terraform destroy -var project_id=$GOOGLE_CLOUD_PROJECT
-```
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [project_id](variables.tf#L23) | Project id. | string
| ✓ | |
-| [project_create](variables.tf#L17) | Create project instead of using an existing one. | bool
| | false
|
-| [service_accounts](variables.tf#L28) | List of service accounts. | list(object({…}))
| | […]
|
-| [services](variables.tf#L56) | Service APIs to enable. | list(string)
| | []
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [sa-credentials](outputs.tf#L17) | SA json key templates. | |
-
-
diff --git a/examples/cloud-operations/onprem-sa-key-management/versions.tf b/examples/cloud-operations/onprem-sa-key-management/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/cloud-operations/onprem-sa-key-management/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/cloud-operations/packer-image-builder/README.md b/examples/cloud-operations/packer-image-builder/README.md
deleted file mode 100644
index 6bcee4d6b9..0000000000
--- a/examples/cloud-operations/packer-image-builder/README.md
+++ /dev/null
@@ -1,95 +0,0 @@
-# Compute Image builder with Hashicorp Packer
-
-This example shows how to deploy infrastructure for a Compute Engine image builder based on
-[Hashicorp's Packer tool](https://www.packer.io).
-
-![High-level diagram](diagram.png "High-level diagram")
-
-## Running the example
-
-Prerequisite: [Packer](https://www.packer.io/downloads) version >= v1.7.0
-
-Infrastructure setup (Terraform part):
-
-1. Set Terraform configuration variables
-2. Run `terraform init`
-3. Run `terraform apply`
-
-Building Compute Engine image (Packer part):
-
-1. Enter `packer` directory
-2. Set Packer configuration variables (see [Configuring Packer](#configuring-packer) below)
-3. Run `packer init .`
-4. Run `packer build .`
-
-## Using Packer's service account
-
-The following example leverages [service account impersonation](https://cloud.google.com/iam/docs/impersonating-service-accounts)
-to execute any operations on GCP as a dedicated Packer service account. Depending on how you execute
-the Packer tool, you need to grant your principal rights to impersonate Packer's service account.
-
-Set `packer_account_users` variable in Terraform configuration to grant roles required to impersonate
-Packer's service account to selected IAM principals.
-Example: allow default [Cloud Build](https://cloud.google.com/build) service account to impersonate
-Packer SA: `packer_account_users=["serviceAccount:myProjectNumber@cloudbuild.gserviceaccount.com"]`.
-
-## Configuring Packer
-
-Provided Packer build example uses [HCL2 configuration files](https://www.packer.io/guides/hcl) and
-requires configuration of some input variables *(i.e. service accounts emails)*.
-Values of those variables can be taken from the Terraform outputs.
-
-For your convenience, Terraform can populate Packer's variable file.
-You can enable this behavior by setting `create_packer_vars` configuration variable to `true`.
-Terraform will use template from `packer/build.pkrvars.tpl` file and generate `packer/build.auto.pkrvars.hcl`
-variable file for Packer.
-
-Read [Assigning Variables](https://www.packer.io/guides/hcl/variables#assigning-variables) chapter
-from [Packer's documentation](https://www.packer.io/docs) for more details on setting up Packer variables.
-
-## Accessing temporary VM
-
-Packer creates a temporary Compute Engine VM instance for provisioning. As we recommend using internal
-IP addresses only, communication with this VM has to either:
-
-* originate from the network routable on Packer's VPC *(i.e. peered VPC, over VPN or interconnect)*
-* use [Identity-Aware Proxy](https://cloud.google.com/iap/docs/using-tcp-forwarding) tunnel
-
-By default, this example assumes that IAP tunnel is needed to communicate with the temporary VM.
-This might be changed by setting `use_iap` variable to `false` in Terraform and Packer
-configurations respectively.
-
-**NOTE:** using IAP tunnel with Packer requires gcloud SDK installed on the system running Packer.
-
-## Accessing resources over the Internet
-
-The following example assumes that provisioning of a Compute Engine VM requires access to
-the resources over the Internet (i.e. to install OS packages). Since Compute VM has no public IP
-address for security reasons, Internet connectivity is done with [Cloud NAT](https://cloud.google.com/nat/docs/overview).
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [project_id](variables.tf#L55) | Project id that references existing project. | string
| ✓ | |
-| [billing_account](variables.tf#L17) | Billing account id used as default for new projects. | string
| | null
|
-| [cidrs](variables.tf#L23) | CIDR ranges for subnets. | map(string)
| | {…}
|
-| [create_packer_vars](variables.tf#L31) | Create packer variables file using template file and terraform output. | bool
| | false
|
-| [packer_account_users](variables.tf#L37) | List of members that will be allowed to impersonate Packer image builder service account in IAM format, i.e. 'user:{emailid}'. | list(string)
| | []
|
-| [packer_source_cidrs](variables.tf#L43) | List of CIDR ranges allowed to connect to the temporary VM for provisioning. | list(string)
| | ["0.0.0.0/0"]
|
-| [project_create](variables.tf#L49) | Create project instead of using an existing one. | bool
| | true
|
-| [region](variables.tf#L60) | Default region for resources. | string
| | "europe-west1"
|
-| [root_node](variables.tf#L66) | The resource name of the parent folder or organization for project creation, in 'folders/folder_id' or 'organizations/org_id' format. | string
| | null
|
-| [use_iap](variables.tf#L72) | Use IAP tunnel to connect to Compute Engine instance for provisioning. | bool
| | true
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [builder_sa](outputs.tf#L17) | Packer's service account email. | |
-| [compute_sa](outputs.tf#L22) | Packer's temporary VM service account email. | |
-| [compute_subnetwork](outputs.tf#L27) | Name of a subnetwork for Packer's temporary VM. | |
-| [compute_zone](outputs.tf#L32) | Name of a compute engine zone for Packer's temporary VM. | |
-
-
diff --git a/examples/cloud-operations/packer-image-builder/main.tf b/examples/cloud-operations/packer-image-builder/main.tf
deleted file mode 100644
index 1e4fd0075e..0000000000
--- a/examples/cloud-operations/packer-image-builder/main.tf
+++ /dev/null
@@ -1,131 +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 {
- compute_subnet_name = "image-builder"
- compute_zone = "${var.region}-a"
- packer_variables_template = "packer/build.pkrvars.tpl"
- packer_variables_file = "packer/build.auto.pkrvars.hcl"
-}
-
-module "project" {
- source = "../../../modules/project"
- name = var.project_id
- parent = var.root_node
- billing_account = var.billing_account
- project_create = var.project_create
- services = [
- "compute.googleapis.com"
- ]
- service_config = {
- disable_on_destroy = false
- disable_dependent_services = false
- }
-}
-
-module "service-account-image-builder" {
- source = "../../../modules/iam-service-account"
- project_id = module.project.project_id
- name = "image-builder"
- iam_project_roles = {
- (var.project_id) = [
- "roles/compute.instanceAdmin.v1",
- "roles/iam.serviceAccountUser"
- ]
- }
-}
-
-module "service-account-image-builder-vm" {
- source = "../../../modules/iam-service-account"
- project_id = module.project.project_id
- name = "image-builder-vm"
-}
-
-module "vpc" {
- source = "../../../modules/net-vpc"
- project_id = module.project.project_id
- name = "image-builder"
- subnets = [
- {
- name = local.compute_subnet_name
- ip_cidr_range = var.cidrs.image-builder
- region = var.region
- secondary_ip_range = null
- }
- ]
-}
-
-module "firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.project.project_id
- network = module.vpc.name
- custom_rules = {
- image-builder-ingress-builder-vm = {
- description = "Allow image builder vm ingress traffic"
- direction = "INGRESS"
- action = "allow"
- sources = []
- ranges = var.packer_source_cidrs
- targets = [module.service-account-image-builder-vm.email]
- use_service_accounts = true
- rules = [{ protocol = "tcp", ports = [22, 5985, 5986] }]
- extra_attributes = {}
- }
- }
-}
-
-module "nat" {
- source = "../../../modules/net-cloudnat"
- project_id = module.project.project_id
- region = var.region
- name = "default"
- router_network = module.vpc.name
- config_source_subnets = "LIST_OF_SUBNETWORKS"
- subnetworks = [
- {
- self_link = module.vpc.subnet_self_links["${var.region}/${local.compute_subnet_name}"]
- config_source_ranges = ["ALL_IP_RANGES"]
- secondary_ranges = null
- }
- ]
-}
-
-resource "google_service_account_iam_binding" "sa-image-builder-token-creators" {
- count = length(var.packer_account_users) > 0 ? 1 : 0
- service_account_id = module.service-account-image-builder.service_account.name
- role = "roles/iam.serviceAccountTokenCreator"
- members = var.packer_account_users
-}
-
-resource "google_project_iam_member" "project-iap-sa-image-builder" {
- count = var.use_iap ? 1 : 0
- project = var.project_id
- member = module.service-account-image-builder.iam_email
- role = "roles/iap.tunnelResourceAccessor"
-}
-
-resource "local_file" "packer-vars" {
- count = var.create_packer_vars ? 1 : 0
- content = templatefile(local.packer_variables_template, {
- PROJECT_ID = "${var.project_id}"
- COMPUTE_ZONE = "${local.compute_zone}"
- BUILDER_SA = "${module.service-account-image-builder.email}"
- COMPUTE_SA = "${module.service-account-image-builder-vm.email}"
- COMPUTE_SUBNETWORK = "${local.compute_subnet_name}"
- USE_IAP = "${var.use_iap}"
- })
- filename = local.packer_variables_file
-}
diff --git a/examples/cloud-operations/packer-image-builder/versions.tf b/examples/cloud-operations/packer-image-builder/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/cloud-operations/packer-image-builder/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/cloud-operations/quota-monitoring/README.md b/examples/cloud-operations/quota-monitoring/README.md
deleted file mode 100644
index 7b4e442f96..0000000000
--- a/examples/cloud-operations/quota-monitoring/README.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# Compute Engine quota monitoring
-
-This example improves on the [GCE quota exporter tool](https://github.com/GoogleCloudPlatform/professional-services/tree/master/tools/gce-quota-sync) (by the same author of this example), and shows a practical way of collecting and monitoring [Compute Engine resource quotas](https://cloud.google.com/compute/quotas) via Cloud Monitoring metrics as an alternative to the recently released [built-in quota metrics](https://cloud.google.com/monitoring/alerts/using-quota-metrics).
-
-Compared to the built-in metrics, it offers a simpler representation of quotas and quota ratios which is especially useful in charts, it allows filtering or combining quotas between different projects regardless of their monitoring workspace, and it creates a default alerting policy without the need to interact directly with the monitoring API.
-
-Regardless of its specific purpose, this example is also useful in showing how to manipulate and write time series to cloud monitoring. The resources it creates are shown in the high level diagram below:
-
-
-
-The solution is designed so that the Cloud Function arguments that control function execution (eg to set which project quotas to monitor) are defined in the Cloud Scheduler payload set in the PubSub message, so that a single function can be used for different configurations by creating more schedules.
-
-Quota time series are stored using a [custom metric](https://cloud.google.com/monitoring/custom-metrics) with the `custom.googleapis.com/quota/gce` type and [gauge kind](https://cloud.google.com/monitoring/api/v3/kinds-and-types#metric-kinds), tracking the ratio between quota and limit as double to aid in visualization and alerting. Labels are set with the quota name, project id (which may differ from the monitoring workspace projects), value, and limit. This is how they look like in the metrics explorer.
-
-
-
-The solution also creates a basic monitoring alert policy, to demonstrate how to raise alerts when any of the tracked quota ratios go over a predefined threshold.
-
-## Running the example
-
-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=examples%2Fcloud-operations%2Fquota-monitoring), then go through the following steps to create resources:
-
-- `terraform init`
-- `terraform apply -var project_id=my-project-id`
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [project_id](variables.tf#L35) | Project id that references existing project. | string
| ✓ | |
-| [bundle_path](variables.tf#L17) | Path used to write the intermediate Cloud Function code bundle. | string
| | "./bundle.zip"
|
-| [name](variables.tf#L23) | Arbitrary string used to name created resources. | string
| | "quota-monitor"
|
-| [project_create](variables.tf#L29) | Create project instead ofusing an existing one. | bool
| | false
|
-| [quota_config](variables.tf#L40) | Cloud function configuration. | object({…})
| | {…}
|
-| [region](variables.tf#L54) | Compute region used in the example. | string
| | "europe-west1"
|
-| [schedule_config](variables.tf#L60) | Schedule timer configuration in crontab format. | string
| | "0 * * * *"
|
-
-
diff --git a/examples/cloud-operations/quota-monitoring/cf/main.py b/examples/cloud-operations/quota-monitoring/cf/main.py
deleted file mode 100755
index 622c28310b..0000000000
--- a/examples/cloud-operations/quota-monitoring/cf/main.py
+++ /dev/null
@@ -1,201 +0,0 @@
-#! /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.
-
-"""Sync GCE quota usage to Stackdriver for multiple projects.
-
-This tool fetches global and/or regional quotas from the GCE API for
-multiple projects, and sends them to Stackdriver as custom metrics, where they
-can be used to set alert policies or create charts.
-"""
-
-import base64
-import datetime
-import json
-import logging
-import os
-import warnings
-
-import click
-
-from google.api_core.exceptions import GoogleAPIError
-from google.cloud import monitoring_v3
-
-import googleapiclient.discovery
-import googleapiclient.errors
-
-
-_BATCH_SIZE = 5
-_METRIC_KIND = monitoring_v3.enums.MetricDescriptor.MetricKind.GAUGE
-_METRIC_TYPE = 'custom.googleapis.com/quota/gce'
-
-
-def _add_series(project_id, series, client=None):
- """Write metrics series to Stackdriver.
-
- Args:
- project_id: series will be written to this project id's account
- series: the time series to be written, as a list of
- monitoring_v3.types.TimeSeries instances
- client: optional monitoring_v3.MetricServiceClient will be used
- instead of obtaining a new one
- """
- client = client or monitoring_v3.MetricServiceClient()
- project_name = client.project_path(project_id)
- if isinstance(series, monitoring_v3.types.TimeSeries):
- series = [series]
- try:
- client.create_time_series(project_name, series)
- except GoogleAPIError as e:
- raise RuntimeError('Error from monitoring API: %s' % e)
-
-
-def _configure_logging(verbose=True):
- """Basic logging configuration.
-
- Args:
- verbose: enable verbose logging
- """
- level = logging.DEBUG if verbose else logging.INFO
- logging.basicConfig(level=level)
- warnings.filterwarnings('ignore', r'.*end user credentials.*', UserWarning)
-
-
-def _fetch_quotas(project, region='global', compute=None):
- """Fetch GCE per - project or per - region quotas from the API.
-
- Args:
- project: fetch global or regional quotas for this project id
- region: which quotas to fetch, 'global' or region name
- compute: optional instance of googleapiclient.discovery.build will be used
- instead of obtaining a new one
- """
- compute = compute or googleapiclient.discovery.build('compute', 'v1')
- try:
- if region != 'global':
- req = compute.regions().get(project=project, region=region)
- else:
- req = compute.projects().get(project=project)
- resp = req.execute()
- return resp['quotas']
- except (GoogleAPIError, googleapiclient.errors.HttpError) as e:
- logging.debug('API Error: %s', e, exc_info=True)
- raise RuntimeError('Error fetching quota (project: %s, region: %s)' %
- (project, region))
-
-
-def _get_series(metric_labels, value, metric_type=_METRIC_TYPE, dt=None):
- """Create a Stackdriver monitoring time series from value and labels.
-
- Args:
- metric_labels: dict with labels that will be used in the time series
- value: time series value
- metric_type: which metric is this series for
- dt: datetime.datetime instance used for the series end time
- """
- series = monitoring_v3.types.TimeSeries()
- series.metric.type = metric_type
- series.resource.type = 'global'
- for label in metric_labels:
- series.metric.labels[label] = metric_labels[label]
- point = series.points.add()
- point.value.double_value = value
- point.interval.end_time.FromDatetime(dt or datetime.datetime.utcnow())
- return series
-
-
-def _quota_to_series(project, region, quota):
- """Convert API quota objects to Stackdriver monitoring time series.
-
- Args:
- project: set in converted time series labels
- region: set in converted time series labels
- quota: quota object received from the GCE API
- """
- labels = dict((k, str(v)) for k, v in quota.items())
- labels['project'] = project
- labels['region'] = region
- try:
- value = quota['usage'] / float(quota['limit'])
- except ZeroDivisionError:
- value = 0
- return _get_series(labels, value)
-
-
-@click.command()
-@click.option('--monitoring-project', required=True,
- help='monitoring project id')
-@click.option('--gce-project', multiple=True,
- help='project ids (multiple), defaults to monitoring project')
-@click.option('--gce-region', multiple=True,
- help='regions (multiple), defaults to "global"')
-@click.option('--verbose', is_flag=True, help='Verbose output')
-@click.argument('keywords', nargs=-1)
-def main_cli(monitoring_project=None, gce_project=None, gce_region=None,
- verbose=False, keywords=None):
- """Fetch GCE quotas and writes them as custom metrics to Stackdriver.
-
- If KEYWORDS are specified as arguments, only quotas matching one of the
- keywords will be stored in Stackdriver.
- """
- try:
- _main(monitoring_project, gce_project, gce_region, verbose, keywords)
- except RuntimeError:
- logging.exception('exception raised')
-
-
-def main(event, context):
- """Cloud Function entry point."""
- try:
- data = json.loads(base64.b64decode(event['data']).decode('utf-8'))
- _main(os.environ.get('GCP_PROJECT'), **data)
- # uncomment once https://issuetracker.google.com/issues/155215191 is fixed
- # except RuntimeError:
- # raise
- except Exception:
- logging.exception('exception in cloud function entry point')
-
-
-def _main(monitoring_project, gce_project=None, gce_region=None, verbose=False,
- keywords=None):
- """Module entry point used by cli and cloud function wrappers."""
- _configure_logging(verbose=verbose)
- gce_projects = gce_project or [monitoring_project]
- gce_regions = gce_region or ['global']
- keywords = set(keywords or [])
- logging.debug('monitoring project %s', monitoring_project)
- logging.debug('projects %s regions %s', gce_projects, gce_regions)
- logging.debug('keywords %s', keywords)
- quotas = []
- compute = googleapiclient.discovery.build(
- 'compute', 'v1', cache_discovery=False)
- for project in gce_projects:
- logging.debug('project %s', project)
- for region in gce_regions:
- logging.debug('region %s', region)
- for quota in _fetch_quotas(project, region, compute=compute):
- if keywords and not any(k in quota['metric'] for k in keywords):
- # logging.debug('skipping %s', quota)
- continue
- logging.debug('quota %s', quota)
- quotas.append((project, region, quota))
- client, i = monitoring_v3.MetricServiceClient(), 0
- while i < len(quotas):
- series = [_quota_to_series(*q) for q in quotas[i:i + _BATCH_SIZE]]
- _add_series(monitoring_project, series, client)
- i += _BATCH_SIZE
-
-
-if __name__ == '__main__':
- main_cli()
diff --git a/examples/cloud-operations/quota-monitoring/explorer.png b/examples/cloud-operations/quota-monitoring/explorer.png
deleted file mode 100644
index 80f5025414..0000000000
Binary files a/examples/cloud-operations/quota-monitoring/explorer.png and /dev/null differ
diff --git a/examples/cloud-operations/quota-monitoring/main.tf b/examples/cloud-operations/quota-monitoring/main.tf
deleted file mode 100644
index 5612e5b9c8..0000000000
--- a/examples/cloud-operations/quota-monitoring/main.tf
+++ /dev/null
@@ -1,146 +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 {
- projects = (
- var.quota_config.projects == null
- ? [var.project_id]
- : var.quota_config.projects
- )
-}
-
-module "project" {
- source = "../../../modules/project"
- name = var.project_id
- project_create = var.project_create
- services = [
- "compute.googleapis.com",
- "cloudfunctions.googleapis.com"
- ]
- service_config = {
- disable_on_destroy = false,
- disable_dependent_services = false
- }
- iam = {
- "roles/monitoring.metricWriter" = [module.cf.service_account_iam_email]
- }
-}
-
-module "pubsub" {
- source = "../../../modules/pubsub"
- project_id = module.project.project_id
- name = var.name
- subscriptions = {
- "${var.name}-default" = null
- }
- # the Cloud Scheduler robot service account already has pubsub.topics.publish
- # at the project level via roles/cloudscheduler.serviceAgent
-}
-
-module "cf" {
- source = "../../../modules/cloud-function"
- project_id = module.project.project_id
- name = var.name
- bucket_name = "${var.name}-${random_pet.random.id}"
- bucket_config = {
- location = var.region
- lifecycle_delete_age = null
- }
- bundle_config = {
- source_dir = "cf"
- output_path = var.bundle_path
- excludes = null
- }
- # https://github.com/hashicorp/terraform-provider-archive/issues/40
- # https://issuetracker.google.com/issues/155215191
- environment_variables = {
- USE_WORKER_V2 = "true"
- PYTHON37_DRAIN_LOGS_ON_CRASH_WAIT_SEC = "5"
- }
- service_account_create = true
- trigger_config = {
- event = "google.pubsub.topic.publish"
- resource = module.pubsub.topic.id
- retry = null
- }
-}
-
-resource "google_cloud_scheduler_job" "job" {
- 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({
- gce_project = var.quota_config.projects
- gce_region = var.quota_config.regions
- keywords = var.quota_config.filters
- }))
- }
-}
-
-resource "google_project_iam_member" "network_viewer" {
- for_each = toset(local.projects)
- project = each.key
- role = "roles/compute.networkViewer"
- member = module.cf.service_account_iam_email
-}
-
-resource "google_project_iam_member" "quota_viewer" {
- for_each = toset(local.projects)
- project = each.key
- role = "roles/servicemanagement.quotaViewer"
- member = module.cf.service_account_iam_email
-}
-
-resource "google_monitoring_alert_policy" "alert_policy" {
- project = module.project.project_id
- display_name = "Quota monitor"
- combiner = "OR"
- conditions {
- display_name = "simple quota threshold"
- condition_threshold {
- filter = "metric.type=\"custom.googleapis.com/quota/gce\" resource.type=\"global\""
- threshold_value = 0.75
- comparison = "COMPARISON_GT"
- duration = "0s"
- aggregations {
- alignment_period = "60s"
- group_by_fields = []
- per_series_aligner = "ALIGN_MEAN"
- }
- trigger {
- count = 1
- percent = 0
- }
- }
- }
- enabled = false
- user_labels = {
- name = var.name
- }
- documentation {
- content = "GCE quota over threshold."
- }
-}
-
-resource "random_pet" "random" {
- length = 1
-}
diff --git a/examples/cloud-operations/quota-monitoring/variables.tf b/examples/cloud-operations/quota-monitoring/variables.tf
deleted file mode 100644
index 2b69aa1cb5..0000000000
--- a/examples/cloud-operations/quota-monitoring/variables.tf
+++ /dev/null
@@ -1,64 +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 "bundle_path" {
- description = "Path used to write the intermediate Cloud Function code bundle."
- type = string
- default = "./bundle.zip"
-}
-
-variable "name" {
- description = "Arbitrary string used to name created resources."
- type = string
- default = "quota-monitor"
-}
-
-variable "project_create" {
- description = "Create project instead ofusing an existing one."
- type = bool
- default = false
-}
-
-variable "project_id" {
- description = "Project id that references existing project."
- type = string
-}
-
-variable "quota_config" {
- description = "Cloud function configuration."
- type = object({
- filters = list(string)
- projects = list(string)
- regions = list(string)
- })
- default = {
- filters = null
- projects = null
- regions = null
- }
-}
-
-variable "region" {
- description = "Compute region used in the example."
- type = string
- default = "europe-west1"
-}
-
-variable "schedule_config" {
- description = "Schedule timer configuration in crontab format."
- type = string
- default = "0 * * * *"
-}
diff --git a/examples/cloud-operations/quota-monitoring/versions.tf b/examples/cloud-operations/quota-monitoring/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/cloud-operations/quota-monitoring/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/cloud-operations/scheduled-asset-inventory-export-bq/README.md b/examples/cloud-operations/scheduled-asset-inventory-export-bq/README.md
deleted file mode 100644
index a817c31a2d..0000000000
--- a/examples/cloud-operations/scheduled-asset-inventory-export-bq/README.md
+++ /dev/null
@@ -1,79 +0,0 @@
-# Scheduled Cloud Asset Inventory Export to Bigquery
-
-This example shows how to leverage [Cloud Asset Inventory Exporting to Bigquery](https://cloud.google.com/asset-inventory/docs/exporting-to-bigquery) feature to keep track of your project wide assets over time storing information in Bigquery.
-
-The data stored in Bigquery can then be used for different purposes:
-
-- dashboarding
-- analysis
-
-The example uses export resources at the project level for ease of testing, in actual use a few changes are needed to operate at the resource hierarchy level:
-
-- the export should be set at the folder or organization level
-- the `roles/cloudasset.viewer` on the service account should be set at the folder or organization level
-
-The resources created in this example are shown in the high level diagram below:
-
-
-
-## Prerequisites
-
-Ensure that you grant your account one of the following roles on your project, folder, or organization:
-
-- Cloud Asset Viewer role (`roles/cloudasset.viewer`)
-- Owner primitive role (`roles/owner`)
-
-## Running the example
-
-Clone this repository, specify your variables in a `terraform.tvars` and then go through the following steps to create resources:
-
-- `terraform init`
-- `terraform apply`
-
-Once done testing, you can clean up resources by running `terraform destroy`. To persist state, check out the `backend.tf.sample` file.
-
-## Testing the example
-
-Once resources are created, you can run queries on the data you exported on Bigquery. [Here](https://cloud.google.com/asset-inventory/docs/exporting-to-bigquery#querying_an_asset_snapshot) you can find some example of queries you can run.
-
-You can also create a dashboard connecting [Datalab](https://datastudio.google.com/) or any other BI tools of your choice to your Bigquery dataset.
-
-## File exporter for JSON, CSV (optional).
-
-This is an optional part.
-
-Regular file-based exports of data from Cloud Asset Inventory may be useful for e.g. scale-out network dependencies discovery tools like [Planet Exporter](https://github.com/williamchanrico/planet-exporter), or to update legacy workloads tracking or configuration management systems. Bigquery supports multiple [export formats](https://cloud.google.com/bigquery/docs/exporting-data#export_formats_and_compression_types) and one may upload objects to Storage Bucket using provided Cloud Function. Specify `job.DestinationFormat` as defined in [documentation](https://googleapis.dev/python/bigquery/latest/generated/google.cloud.bigquery.job.DestinationFormat.html), e.g. `NEWLINE_DELIMITED_JSON`.
-
-It helps to create custom [scheduled query](https://cloud.google.com/bigquery/docs/scheduling-queries#console) from CAI export tables, and to write out results in to dedicated table (with overwrites). Define such query's output columns to comply with downstream systems' fields requirements, and time query execution after CAI export into BQ for freshness. See [sample queries](https://cloud.google.com/asset-inventory/docs/exporting-to-bigquery-sample-queries).
-
-This is an optional part, created if `cai_gcs_export` is set to `true`. The high level diagram extends to the following:
-
-
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [cai_config](variables.tf#L36) | Cloud Asset Inventory export config. | object({…})
| ✓ | |
-| [project_id](variables.tf#L101) | Project id that references existing project. | string
| ✓ | |
-| [billing_account](variables.tf#L17) | Billing account id used as default for new projects. | string
| | null
|
-| [bundle_path](variables.tf#L23) | Path used to write the intermediate Cloud Function code bundle. | string
| | "./bundle.zip"
|
-| [bundle_path_cffile](variables.tf#L30) | Path used to write the intermediate Cloud Function code bundle. | string
| | "./bundle_cffile.zip"
|
-| [cai_gcs_export](variables.tf#L47) | Enable optional part to export tables to GCS. | bool
| | false
|
-| [file_config](variables.tf#L54) | Optional BQ table as a file export function config. | object({…})
| | {…}
|
-| [location](variables.tf#L73) | Appe Engine location used in the example. | string
| | "europe-west"
|
-| [name](variables.tf#L80) | Arbitrary string used to name created resources. | string
| | "asset-inventory"
|
-| [name_cffile](variables.tf#L88) | Arbitrary string used to name created resources. | string
| | "cffile-exporter"
|
-| [project_create](variables.tf#L95) | Create project instead ofusing an existing one. | bool
| | true
|
-| [region](variables.tf#L106) | Compute region used in the example. | string
| | "europe-west1"
|
-| [root_node](variables.tf#L112) | The resource name of the parent folder or organization for project creation, in 'folders/folder_id' or 'organizations/org_id' format. | string
| | null
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [bq-dataset](outputs.tf#L17) | Bigquery instance details. | |
-| [cloud-function](outputs.tf#L22) | Cloud Function instance details. | |
-
-
diff --git a/examples/cloud-operations/scheduled-asset-inventory-export-bq/main.tf b/examples/cloud-operations/scheduled-asset-inventory-export-bq/main.tf
deleted file mode 100644
index 15d479a3b0..0000000000
--- a/examples/cloud-operations/scheduled-asset-inventory-export-bq/main.tf
+++ /dev/null
@@ -1,211 +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.
- */
-
-
-
-###############################################################################
-# Projects #
-###############################################################################
-
-module "project" {
- source = "../../../modules/project"
- name = var.project_id
- parent = var.root_node
- billing_account = try(var.billing_account, null)
- project_create = var.project_create
- services = [
- "bigquery.googleapis.com",
- "cloudasset.googleapis.com",
- "compute.googleapis.com",
- "cloudfunctions.googleapis.com",
- "cloudbuild.googleapis.com",
- "cloudscheduler.googleapis.com",
- "pubsub.googleapis.com"
- ]
- iam = {
- "roles/resourcemanager.projectIamAdmin" = ["serviceAccount:${module.project.service_accounts.robots.cloudasset}"]
- "roles/bigquery.dataEditor" = ["serviceAccount:${module.project.service_accounts.robots.cloudasset}"]
- "roles/bigquery.user" = ["serviceAccount:${module.project.service_accounts.robots.cloudasset}"]
- }
-}
-
-module "service-account" {
- source = "../../../modules/iam-service-account"
- project_id = module.project.project_id
- name = "${var.name}-cf"
- iam_project_roles = {
- (var.project_id) = [
- "roles/cloudasset.owner",
- "roles/bigquery.jobUser"
- ]
- }
-}
-
-###############################################################################
-# Pub/Sub #
-###############################################################################
-
-module "pubsub" {
- source = "../../../modules/pubsub"
- project_id = module.project.project_id
- name = var.name
- subscriptions = {
- "${var.name}-default" = null
- }
- # the Cloud Scheduler robot service account already has pubsub.topics.publish
- # at the project level via roles/cloudscheduler.serviceAgent
-}
-
-module "pubsub_file" {
- source = "../../../modules/pubsub"
- project_id = module.project.project_id
- name = var.name_cffile
- subscriptions = {
- "${var.name_cffile}-default" = null
- }
- # the Cloud Scheduler robot service account already has pubsub.topics.publish
- # at the project level via roles/cloudscheduler.serviceAgent
-}
-
-###############################################################################
-# Cloud Function #
-###############################################################################
-
-module "cf" {
- source = "../../../modules/cloud-function"
- project_id = module.project.project_id
- region = var.region
- name = var.name
- bucket_name = "${var.name}-${random_pet.random.id}"
- bucket_config = {
- location = var.region
- lifecycle_delete_age = null
- }
- bundle_config = {
- source_dir = "cf"
- output_path = var.bundle_path
- excludes = null
- }
- service_account = module.service-account.email
- trigger_config = {
- event = "google.pubsub.topic.publish"
- resource = module.pubsub.topic.id
- retry = null
- }
-}
-
-module "cffile" {
- count = var.cai_gcs_export ? 1 : 0
- source = "../../../modules/cloud-function"
- project_id = module.project.project_id
- region = var.region
- name = var.name_cffile
- bucket_name = "${var.name_cffile}-${random_pet.random.id}"
- bucket_config = {
- location = var.region
- lifecycle_delete_age = null
- }
- bundle_config = {
- source_dir = "cffile"
- output_path = var.bundle_path_cffile
- excludes = null
- }
- service_account = module.service-account.email
- trigger_config = {
- event = "google.pubsub.topic.publish"
- resource = module.pubsub_file.topic.id
- retry = null
- }
-}
-
-resource "random_pet" "random" {
- length = 1
-}
-
-###############################################################################
-# Cloud Scheduler #
-###############################################################################
-
-resource "google_app_engine_application" "app" {
- project = module.project.project_id
- location_id = var.location
-}
-
-resource "google_cloud_scheduler_job" "job" {
- project = google_app_engine_application.app.project
- region = var.region
- name = "cai-export-job"
- description = "CAI Export Job."
- schedule = "* 9 * * 1"
- time_zone = "Etc/UTC"
-
- pubsub_target {
- attributes = {}
- topic_name = module.pubsub.topic.id
- data = base64encode(jsonencode({
- project = module.project.project_id
- bq_project = module.project.project_id
- bq_dataset = var.cai_config.bq_dataset
- bq_table = var.cai_config.bq_table
- bq_table_overwrite = var.cai_config.bq_table_overwrite
- target_node = var.cai_config.target_node
- }))
- }
-}
-
-resource "google_cloud_scheduler_job" "job_file" {
- count = var.cai_gcs_export ? 1 : 0
- project = google_app_engine_application.app.project
- region = var.region
- name = "file-export-job"
- description = "File export from BQ Job."
- schedule = "* 9 * * 1"
- time_zone = "Etc/UTC"
-
- pubsub_target {
- attributes = {}
- topic_name = module.pubsub_file.topic.id
- data = base64encode(jsonencode({
- bucket = var.file_config.bucket
- filename = var.file_config.filename
- format = var.file_config.format
- bq_dataset = var.file_config.bq_dataset
- bq_table = var.file_config.bq_table
- }))
- }
-}
-
-###############################################################################
-# Bigquery #
-###############################################################################
-
-module "bq" {
- source = "../../../modules/bigquery-dataset"
- project_id = module.project.project_id
- id = var.cai_config.bq_dataset
- location = var.region
- access = {
- owner = { role = "OWNER", type = "user" }
- }
- access_identities = {
- owner = module.service-account.email
- }
- options = {
- default_table_expiration_ms = null
- default_partition_expiration_ms = null
- delete_contents_on_destroy = true
- }
-}
diff --git a/examples/cloud-operations/scheduled-asset-inventory-export-bq/versions.tf b/examples/cloud-operations/scheduled-asset-inventory-export-bq/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/cloud-operations/scheduled-asset-inventory-export-bq/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/cloud-operations/unmanaged-instances-healthcheck/README.md b/examples/cloud-operations/unmanaged-instances-healthcheck/README.md
deleted file mode 100644
index 9fa8a98e35..0000000000
--- a/examples/cloud-operations/unmanaged-instances-healthcheck/README.md
+++ /dev/null
@@ -1,120 +0,0 @@
-# TCP healthcheck and restart for unmanaged GCE instances
-
-This example shows how to leverage [Serverless VPC Access](https://cloud.google.com/vpc/docs/configure-serverless-vpc-access) and Cloud Functions to organize a highly performant TCP healthcheck for unmanaged GCE instances. Healthchecker Cloud Function uses [goroutines](https://gobyexample.com/goroutines) to achieve parallel healthchecking for multiple instances and handles up to 1 thousand VMs checked in less than a second execution time.
-
-**_NOTE:_** [Managed Instance Groups](https://cloud.google.com/compute/docs/instance-groups/autohealing-instances-in-migs) has autohealing functionality out of the box, current example is more applicable for standalone VMs or VMs in an unmanaged instance group.
-
-The example contains the following components:
-
-- [Cloud Scheduler](https://cloud.google.com/scheduler) to initiate a healthcheck on a schedule.
-- [Serverless VPC Connector](https://cloud.google.com/vpc/docs/configure-serverless-vpc-access) to allow Cloud Functions TCP level access to private GCE instances.
-- **Healthchecker** Cloud Function to perform TCP checks against GCE instances.
-- **Restarter** PubSub topic to keep track of instances which are to be restarted.
-- **Restarter** Cloud Function to perform GCE instance reset for instances which are failing TCP healthcheck.
-
-
-The resources created in this example are shown in the high level diagram below:
-
-
-
-### Healthchecker configuration
-Healthchecker cloud function has the following configuration options:
-
-- `FILTER` to filter list of GCE instances the health check will be targeted to. For instance `(name = nginx-*) AND (labels.env = dev)`
-- `GRACE_PERIOD` time period to prevent instance check of newly created instanced allowing services to start on the instance.
-- `MAX_PARALLELISM` - max amount of healthchecks performed in parallel, be aware that every check requires an open TCP connection which is limited.
-- `PUBSUB_TOPIC` topic to publish the message with instance metadata.
-- `RECHECK_INTERVAL` time period for performing recheck, when a check is failed it will be rechecked before marking as unhealthy.
-- `TCP_PORT` port used for health checking
-- `TIMEOUT` the timeout time of a TCP probe.
-
-**_NOTE:_** In the current example `healthchecker` is used along with the `restarter` cloud function, but restarter can be replaced with another function like [Pubsub2Inbox](https://github.com/GoogleCloudPlatform/professional-services/tree/main/tools/pubsub2inbox) for email notifications.
-
-
-## Running the example
-
-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=examples%2Fcloud-operations%2Funmanaged-instances-healthcheck), then go through the following steps to create resources:
-
-- `terraform init`
-- `terraform apply -var project_id=my-project-id`
-
-Once done testing, you can clean up resources by running `terraform destroy`. To persist state, check out the `backend.tf.sample` file.
-
-## Testing the example
-Configure `gcloud` with the project used for the deployment
-```bash
-gcloud config set project string
| ✓ | |
-| [project_id](variables.tf#L33) | Project id to create a project when `project_create` is `true`, or to be used when `false`. | string
| ✓ | |
-| [grace_period](variables.tf#L56) | Grace period for an instance startup. | string
| | "180s"
|
-| [location](variables.tf#L21) | App Engine location used in the example (required for CloudFunctions). | string
| | "europe-west"
|
-| [project_create](variables.tf#L27) | Create project instead of using an existing one. | bool
| | false
|
-| [region](variables.tf#L38) | Compute region used in the example. | string
| | "europe-west1"
|
-| [root_node](variables.tf#L44) | The resource name of the parent folder or organization for project creation, in 'folders/folder_id' or 'organizations/org_id' format. | string
| | null
|
-| [schedule](variables.tf#L50) | Cron schedule for executing compute instances healthcheck. | string
| | "*/5 * * * *" # every five minutes"
|
-| [tcp_port](variables.tf#L62) | TCP port to run healthcheck against. | string
| | "80" #http"
|
-| [timeout](variables.tf#L68) | TCP probe timeout. | string
| | "1000ms"
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [cloud-function-healthchecker](outputs.tf#L16) | Cloud Function Healthchecker instance details. | |
-| [cloud-function-restarter](outputs.tf#L21) | Cloud Function Healthchecker instance details. | |
-| [pubsub-topic](outputs.tf#L26) | Restarter PubSub topic. | |
-
-
diff --git a/examples/cloud-operations/unmanaged-instances-healthcheck/main.tf b/examples/cloud-operations/unmanaged-instances-healthcheck/main.tf
deleted file mode 100644
index b7bb422664..0000000000
--- a/examples/cloud-operations/unmanaged-instances-healthcheck/main.tf
+++ /dev/null
@@ -1,255 +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
-#
-# 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.
-
-
-###############################################################################
-# Project #
-###############################################################################
-
-module "project" {
- source = "../../../modules/project"
- name = var.project_id
- parent = var.root_node
- billing_account = var.billing_account
- project_create = var.project_create
- services = [
- "vpcaccess.googleapis.com",
- "compute.googleapis.com",
- "cloudfunctions.googleapis.com",
- "cloudbuild.googleapis.com",
- "cloudscheduler.googleapis.com",
- "pubsub.googleapis.com"
- ]
-}
-
-###############################################################################
-# Network #
-###############################################################################
-
-module "vpc" {
- source = "../../../modules/net-vpc"
- project_id = module.project.project_id
- name = "vpc"
- subnets = [
- {
- name = "apps"
- ip_cidr_range = "10.8.32.0/24"
- region = var.region
- secondary_ip_range = null
- }
- ]
-}
-
-module "firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.project.project_id
- network = module.vpc.name
-}
-
-###############################################################################
-# Service Accounts #
-###############################################################################
-
-module "service-account-healthchecker" {
- source = "../../../modules/iam-service-account"
- project_id = module.project.project_id
- name = "healthckecker-cf"
- iam_project_roles = {
- (var.project_id) = [
- "roles/compute.viewer",
- "roles/logging.logWriter"
- ]
- }
-}
-
-module "service-account-restarter" {
- source = "../../../modules/iam-service-account"
- project_id = module.project.project_id
- name = "restarter-cf"
- iam_project_roles = {
- (var.project_id) = [
- "roles/compute.instanceAdmin",
- "roles/logging.logWriter"
- ]
- }
-}
-
-module "service-account-scheduler" {
- source = "../../../modules/iam-service-account"
- project_id = module.project.project_id
- name = "cloud-scheduler"
-}
-
-###############################################################################
-# Pub/Sub #
-###############################################################################
-
-module "pubsub" {
- source = "../../../modules/pubsub"
- project_id = module.project.project_id
- name = "restarter"
- iam = {
- "roles/pubsub.publisher" = [module.service-account-healthchecker.iam_email]
- }
-}
-
-###############################################################################
-# Cloud Function #
-###############################################################################
-
-module "cf-restarter" {
- source = "../../../modules/cloud-function"
- project_id = module.project.project_id
- name = "cf-restarter"
- region = var.region
- bucket_name = "cf-bundle-bucket-${random_pet.random.id}"
- bucket_config = {
- location = var.region
- lifecycle_delete_age = null
- }
- bundle_config = {
- source_dir = "${path.module}/function/restarter"
- output_path = "restarter.zip"
- excludes = []
- }
- service_account = module.service-account-restarter.email
-
- function_config = {
- entry_point = "RestartInstance"
- ingress_settings = null
- instances = 1
- memory = 256
- runtime = "go116"
- timeout = 300
- }
-
- trigger_config = {
- event = "google.pubsub.topic.publish"
- resource = module.pubsub.topic.id
- retry = null
- }
-
-}
-
-module "cf-healthchecker" {
- source = "../../../modules/cloud-function"
- project_id = module.project.project_id
- 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"
- excludes = []
- }
- service_account = module.service-account-healthchecker.email
-
- function_config = {
- entry_point = "HealthCheck"
- ingress_settings = null
- instances = 1
- memory = 256
- runtime = "go116"
- timeout = 300
- }
-
- environment_variables = {
- FILTER = "name = nginx-*"
- GRACE_PERIOD = var.grace_period
- PROJECT = module.project.project_id
- PUBSUB_TOPIC = module.pubsub.topic.name
- REGION = var.region
- TCP_PORT = var.tcp_port
- TIMEOUT = var.timeout
- }
-
- vpc_connector = {
- create = true
- name = "hc-connector"
- egress_settings = "PRIVATE_RANGES_ONLY"
-
- }
-
- vpc_connector_config = {
- ip_cidr_range = "10.132.0.0/28"
- network = "vpc"
- }
-
- iam = {
- "roles/cloudfunctions.invoker" = [module.service-account-scheduler.iam_email]
- }
-
- depends_on = [
- module.vpc
- ]
-}
-
-resource "random_pet" "random" {
- length = 1
-}
-
-###############################################################################
-# Cloud Scheduler #
-###############################################################################
-
-resource "google_app_engine_application" "app" {
- project = module.project.project_id
- location_id = var.location
-}
-
-resource "google_cloud_scheduler_job" "healthcheck-job" {
- project = google_app_engine_application.app.project
- region = var.region
- name = "healthchecker-schedule"
- description = "Execute Compute Instance Healthcheck CF"
- schedule = var.schedule
- time_zone = "Etc/UTC"
-
- http_target {
- http_method = "GET"
- uri = module.cf-healthchecker.function.https_trigger_url
-
- oidc_token {
- service_account_email = module.service-account-scheduler.email
- }
- }
-}
-
-###############################################################################
-# Test Nginx Instance #
-###############################################################################
-
-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"
- 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"]
- }
-}
diff --git a/examples/cloud-operations/unmanaged-instances-healthcheck/variables.tf b/examples/cloud-operations/unmanaged-instances-healthcheck/variables.tf
deleted file mode 100644
index d015757cf2..0000000000
--- a/examples/cloud-operations/unmanaged-instances-healthcheck/variables.tf
+++ /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
-#
-# 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.
-
-
-variable "billing_account" {
- description = "Billing account id used as default for new projects."
- type = string
-}
-
-variable "location" {
- description = "App Engine location used in the example (required for CloudFunctions)."
- type = string
- default = "europe-west"
-}
-
-variable "project_create" {
- description = "Create project instead of using an existing one."
- type = bool
- default = false
-}
-
-variable "project_id" {
- description = "Project id to create a project when `project_create` is `true`, or to be used when `false`."
- type = string
-}
-
-variable "region" {
- description = "Compute region used in the example."
- type = string
- default = "europe-west1"
-}
-
-variable "root_node" {
- description = "The resource name of the parent folder or organization for project creation, in 'folders/folder_id' or 'organizations/org_id' format."
- type = string
- default = null
-}
-
-variable "schedule" {
- description = "Cron schedule for executing compute instances healthcheck."
- type = string
- default = "*/5 * * * *" # every five minutes
-}
-
-variable "grace_period" {
- description = "Grace period for an instance startup."
- type = string
- default = "180s"
-}
-
-variable "tcp_port" {
- description = "TCP port to run healthcheck against."
- type = string
- default = "80" #http
-}
-
-variable "timeout" {
- description = "TCP probe timeout."
- type = string
- default = "1000ms"
-}
diff --git a/examples/cloud-operations/vm-migration/README.md b/examples/cloud-operations/vm-migration/README.md
deleted file mode 100644
index 0c75af0917..0000000000
--- a/examples/cloud-operations/vm-migration/README.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# Migrate for Compute Engine (v5) examples
-
-The examples in this folder implement **Migrate for Compute Engine (v5)** environments for the main migration scenarios like the ones with host and target project, or with shared VPC.
-
-They are meant to be used as minimal but complete starting points to create migration environment **on top of existing cloud foundations**, and as playgrounds to experiment with specific Google Cloud features.
-
-## Examples
-
-### M4CE on a single project
-
- This [example](./single-project/) implements a simple environment for Migrate for Compute Engine (v5) where both the API backend and the migration target environment are deployed on a single GCP project.
-
-This example represents the easiest sceario to implement a Migrate for Compute Engine (v5) enviroment suitable for small migrations on simple enviroments or for product showcases.
-string
| ✓ | |
-| [vcenter_password](variables.tf#L48) | VCenter user password. | string
| ✓ | |
-| [vsphere_environment](variables.tf#L53) | VMVware VSphere connection parameters | object({…})
| ✓ | |
-| [m4ce_appliance_properties](variables.tf#L15) | M4CE connector OVA image configuration parameters | object({…})
| | {…}
|
-| [m4ce_connector_ovf_url](variables.tf#L37) | http URL to the public M4CE connector OVA image | string
| | "https://storage.googleapis.com/vmmigration-public-artifacts/migrate-connector-2-0-1663.ova"
|
-
-
-## Manual Steps
-Once this example is deployed a VCenter user has to be created and binded to the M4CE role in order to allow the connector access the VMWare resources.
-The user can be created manually through the VCenter web interface or througt GOV commandline if it is available:
-```bash
-export GOVC_URL=list(string)
| ✓ | |
-| [migration_target_projects](variables.tf#L20) | List of target projects for m4ce workload migrations | list(string)
| ✓ | |
-| [migration_viewer_users](variables.tf#L25) | List of users authorized to retrive information about M4CE in the Google Cloud Console, in IAM format | list(string)
| | []
|
-| [project_create](variables.tf#L31) | Parameters for the creation of the new project to host the M4CE backend | object({…})
| | null
|
-| [project_name](variables.tf#L40) | Name of an existing project or of the new project assigned as M4CE host project | string
| | "m4ce-host-project-000"
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [m4ce_gmanaged_service_account](outputs.tf#L15) | Google managed service account created automatically during the migrate connector registration.. It is used by M4CE to perform activities on target projects | |
-
-
diff --git a/examples/cloud-operations/vm-migration/host-target-projects/outputs.tf b/examples/cloud-operations/vm-migration/host-target-projects/outputs.tf
deleted file mode 100644
index ef78d4c7ca..0000000000
--- a/examples/cloud-operations/vm-migration/host-target-projects/outputs.tf
+++ /dev/null
@@ -1,18 +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
-#
-# 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 "m4ce_gmanaged_service_account" {
- description = "Google managed service account created automatically during the migrate connector registration.. It is used by M4CE to perform activities on target projects"
- value = "serviceAccount:service-${module.host-project.number}@gcp-sa-vmmigration.iam.gserviceaccount.com"
-}
diff --git a/examples/cloud-operations/vm-migration/host-target-projects/variables.tf b/examples/cloud-operations/vm-migration/host-target-projects/variables.tf
deleted file mode 100644
index f6e3345f8c..0000000000
--- a/examples/cloud-operations/vm-migration/host-target-projects/variables.tf
+++ /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
-#
-# 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.
-
-variable "migration_admin_users" {
- description = "List of users authorized to create a new M4CE sources and perform all other migration operations, in IAM format"
- type = list(string)
-}
-
-variable "migration_target_projects" {
- description = "List of target projects for m4ce workload migrations"
- type = list(string)
-}
-
-variable "migration_viewer_users" {
- description = "List of users authorized to retrive information about M4CE in the Google Cloud Console, in IAM format"
- type = list(string)
- default = []
-}
-
-variable "project_create" {
- description = "Parameters for the creation of the new project to host the M4CE backend"
- type = object({
- billing_account_id = string
- parent = string
- })
- default = null
-}
-
-variable "project_name" {
- description = "Name of an existing project or of the new project assigned as M4CE host project"
- type = string
- default = "m4ce-host-project-000"
-}
diff --git a/examples/cloud-operations/vm-migration/host-target-sharedvpc/README.md b/examples/cloud-operations/vm-migration/host-target-sharedvpc/README.md
deleted file mode 100644
index bf82f23689..0000000000
--- a/examples/cloud-operations/vm-migration/host-target-sharedvpc/README.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# M4CE(v5) - Host and Target Projects with Shared VPC
-
-This example creates a Migrate for Compute Engine (v5) environment deployed on an host project with multiple [target projects](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#identifying_your_host_project) and shared VPCs.
-
-The example is designed to implement a M4CE (v5) environment on-top of complex migration landing environment where VMs have to be migrated to multiple target projects. In this example targets are alse service projects for a shared VPC. It also includes the IAM wiring needed to make such scenarios work.
-
-This is the high level diagram:
-
-![High-level diagram](diagram.png "High-level diagram")
-
-## Managed resources and services
-
-This sample creates\update several distinct groups of resources:
-
-- projects
- - M4CE host project with [required services](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#enabling_required_services_on_the_host_project) deployed on a new or existing project.
- - M4CE target project prerequisites deployed on existing projects.
-- IAM
- - Create a [service account](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/migrate-connector#step-3) used at runtime by the M4CE connector for data replication
- - Grant [migration admin roles](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#using_predefined_roles) to provided user accounts.
- - Grant [migration viewer role](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#using_predefined_roles) to provided user accounts.
- - Grant [roles on shared VPC](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/target-project#configure-permissions) to migration admins
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [migration_admin_users](variables.tf#L15) | List of users authorized to create a new M4CE sources and perform all other migration operations, in IAM format | list(string)
| ✓ | |
-| [migration_target_projects](variables.tf#L20) | List of target projects for m4ce workload migrations | list(string)
| ✓ | |
-| [sharedvpc_host_projects](variables.tf#L45) | List of host projects that share a VPC with the selected target projects | list(string)
| ✓ | |
-| [migration_viewer_users](variables.tf#L25) | List of users authorized to retrive information about M4CE in the Google Cloud Console, in IAM format | list(string)
| | []
|
-| [project_create](variables.tf#L30) | Parameters for the creation of the new project to host the M4CE backend | object({…})
| | null
|
-| [project_name](variables.tf#L39) | Name of an existing project or of the new project assigned as M4CE host project | string
| | "m4ce-host-project-000"
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [m4ce_gmanaged_service_account](outputs.tf#L15) | Google managed service account created automatically during the migrate connector registration. It is used by M4CE to perform activities on target projects | |
-
-
-## Manual Steps
-Once this example is deployed the M4CE [m4ce_gmanaged_service_account](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/target-sa-compute-engine#configuring_the_default_service_account) has to be configured to grant the access to the shared VPC and allow the deploy of Compute Engine instances as the result of the migration.
\ No newline at end of file
diff --git a/examples/cloud-operations/vm-migration/host-target-sharedvpc/outputs.tf b/examples/cloud-operations/vm-migration/host-target-sharedvpc/outputs.tf
deleted file mode 100644
index 3e6d553dc4..0000000000
--- a/examples/cloud-operations/vm-migration/host-target-sharedvpc/outputs.tf
+++ /dev/null
@@ -1,18 +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
-#
-# 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 "m4ce_gmanaged_service_account" {
- description = "Google managed service account created automatically during the migrate connector registration. It is used by M4CE to perform activities on target projects"
- value = "serviceAccount:service-${module.host-project.number}@gcp-sa-vmmigration.iam.gserviceaccount.com"
-}
diff --git a/examples/cloud-operations/vm-migration/host-target-sharedvpc/variables.tf b/examples/cloud-operations/vm-migration/host-target-sharedvpc/variables.tf
deleted file mode 100644
index 85f333ce05..0000000000
--- a/examples/cloud-operations/vm-migration/host-target-sharedvpc/variables.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
-#
-# 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.
-
-variable "migration_admin_users" {
- description = "List of users authorized to create a new M4CE sources and perform all other migration operations, in IAM format"
- type = list(string)
-}
-
-variable "migration_target_projects" {
- description = "List of target projects for m4ce workload migrations"
- type = list(string)
-}
-
-variable "migration_viewer_users" {
- description = "List of users authorized to retrive information about M4CE in the Google Cloud Console, in IAM format"
- type = list(string)
- default = []
-}
-variable "project_create" {
- description = "Parameters for the creation of the new project to host the M4CE backend"
- type = object({
- billing_account_id = string
- parent = string
- })
- default = null
-}
-
-variable "project_name" {
- description = "Name of an existing project or of the new project assigned as M4CE host project"
- type = string
- default = "m4ce-host-project-000"
-}
-
-variable "sharedvpc_host_projects" {
- description = "List of host projects that share a VPC with the selected target projects"
- type = list(string)
-}
diff --git a/examples/cloud-operations/vm-migration/single-project/README.md b/examples/cloud-operations/vm-migration/single-project/README.md
deleted file mode 100644
index d196ed8b1b..0000000000
--- a/examples/cloud-operations/vm-migration/single-project/README.md
+++ /dev/null
@@ -1,41 +0,0 @@
-# M4CE(v5) - Single Project
-
-This sample creates a simple M4CE (v5) environment deployed on a signle [host project](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#identifying_your_host_project).
-
-The example is designed for quick tests or product demos where it is required to setup a simple and minimal M4CE (v5) environment. It also includes the IAM wiring needed to make such scenarios work.
-
-This is the high level diagram:
-
-![High-level diagram](diagram.png "High-level diagram")
-
-## Managed resources and services
-
-This sample creates several distinct groups of resources:
-
-- projects
- - M4CE host project with [required services](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#enabling_required_services_on_the_host_project) deployed on a new or existing project.
-- networking
- - Default VPC network
-- IAM
- - One [service account](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/migrate-connector#step-3) used at runtime by the M4CE connector for data replication
- - Grant [migration admin roles](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#using_predefined_roles) to admin user accounts
- - Grant [migration viewer role](https://cloud.google.com/migrate/compute-engine/docs/5.0/how-to/enable-services#using_predefined_roles) to viewer user accounts
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [migration_admin_users](variables.tf#L15) | List of users authorized to create a new M4CE sources and perform all other migration operations, in IAM format | list(string)
| ✓ | |
-| [migration_viewer_users](variables.tf#L20) | List of users authorized to retrive information about M4CE in the Google Cloud Console, in IAM format | list(string)
| | []
|
-| [project_create](variables.tf#L26) | Parameters for the creation of the new project to host the M4CE backend | object({…})
| | null
|
-| [project_name](variables.tf#L35) | Name of an existing project or of the new project assigned as M4CE host an target project | string
| | "m4ce-host-project-000"
|
-| [vpc_config](variables.tf#L41) | Parameters to create a simple VPC on the M4CE project | object({…})
| | {…}
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [m4ce_gmanaged_service_account](outputs.tf#L15) | Google managed service account created automatically during the migrate connector registration. It is used by M4CE to perform activities on target projects | |
-
-
diff --git a/examples/cloud-operations/vm-migration/single-project/main.tf b/examples/cloud-operations/vm-migration/single-project/main.tf
deleted file mode 100644
index 6adf9fbd55..0000000000
--- a/examples/cloud-operations/vm-migration/single-project/main.tf
+++ /dev/null
@@ -1,90 +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
-#
-# 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.
-
-module "landing-project" {
- source = "../../../../modules/project"
- billing_account = (var.project_create != null
- ? var.project_create.billing_account_id
- : null
- )
- name = var.project_name
- parent = (var.project_create != null
- ? var.project_create.parent
- : null
- )
-
- services = [
- "cloudresourcemanager.googleapis.com",
- "compute.googleapis.com",
- "iam.googleapis.com",
- "logging.googleapis.com",
- "networkconnectivity.googleapis.com",
- "servicemanagement.googleapis.com",
- "servicecontrol.googleapis.com",
- "vmmigration.googleapis.com"
- ]
-
- project_create = var.project_create != null
-
- iam_additive = {
- "roles/iam.serviceAccountKeyAdmin" = var.migration_admin_users,
- "roles/iam.serviceAccountCreator" = var.migration_admin_users,
- "roles/vmmigration.admin" = var.migration_admin_users,
- "roles/vmmigration.viewer" = var.migration_viewer_users
- }
-}
-
-module "m4ce-service-account" {
- source = "../../../../modules/iam-service-account"
- project_id = module.landing-project.project_id
- name = "m4ce-sa"
- generate_key = true
-}
-
-module "landing-vpc" {
- source = "../../../../modules/net-vpc"
- project_id = module.landing-project.project_id
- name = "landing-vpc"
- subnets = [
- {
- ip_cidr_range = var.vpc_config.ip_cidr_range
- name = "landing-vpc-${var.vpc_config.region}"
- region = var.vpc_config.region
- secondary_ip_range = {}
- }
- ]
-}
-
-module "landing-vpc-firewall" {
- source = "../../../../modules/net-vpc-firewall"
- project_id = module.landing-project.project_id
- network = module.landing-vpc.name
- admin_ranges = []
- http_source_ranges = []
- https_source_ranges = []
- ssh_source_ranges = []
- custom_rules = {
- allow-ssh = {
- description = "Allow SSH from IAP"
- direction = "INGRESS"
- action = "allow"
- sources = []
- ranges = ["35.235.240.0/20"]
- targets = []
- use_service_accounts = false
- rules = [{ protocol = "tcp", ports = ["22"] }]
- extra_attributes = {}
- }
- }
-}
diff --git a/examples/cloud-operations/vm-migration/single-project/outputs.tf b/examples/cloud-operations/vm-migration/single-project/outputs.tf
deleted file mode 100644
index 347eb54fc5..0000000000
--- a/examples/cloud-operations/vm-migration/single-project/outputs.tf
+++ /dev/null
@@ -1,18 +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
-#
-# 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 "m4ce_gmanaged_service_account" {
- description = "Google managed service account created automatically during the migrate connector registration. It is used by M4CE to perform activities on target projects"
- value = "serviceAccount:service-${module.landing-project.number}@gcp-sa-vmmigration.iam.gserviceaccount.com"
-}
diff --git a/examples/cloud-operations/vm-migration/single-project/variables.tf b/examples/cloud-operations/vm-migration/single-project/variables.tf
deleted file mode 100644
index 2d7214f47d..0000000000
--- a/examples/cloud-operations/vm-migration/single-project/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
-#
-# 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.
-
-variable "migration_admin_users" {
- description = "List of users authorized to create a new M4CE sources and perform all other migration operations, in IAM format"
- type = list(string)
-}
-
-variable "migration_viewer_users" {
- description = "List of users authorized to retrive information about M4CE in the Google Cloud Console, in IAM format"
- type = list(string)
- default = []
-}
-
-variable "project_create" {
- description = "Parameters for the creation of the new project to host the M4CE backend"
- type = object({
- billing_account_id = string
- parent = string
- })
- default = null
-}
-
-variable "project_name" {
- description = "Name of an existing project or of the new project assigned as M4CE host an target project"
- type = string
- default = "m4ce-host-project-000"
-}
-
-variable "vpc_config" {
- description = "Parameters to create a simple VPC on the M4CE project"
- type = object({
- ip_cidr_range = string,
- region = string
- })
- default = {
- ip_cidr_range = "10.200.0.0/20",
- region = "us-west2"
- }
-}
diff --git a/examples/cloud-operations/workload-identity-federation/README.md b/examples/cloud-operations/workload-identity-federation/README.md
deleted file mode 100644
index 5ac94e8d32..0000000000
--- a/examples/cloud-operations/workload-identity-federation/README.md
+++ /dev/null
@@ -1,96 +0,0 @@
-# Configuring workload identity federation to access Google Cloud resources from apps running on Azure
-
-The most straightforward way for workloads running outside of Google Cloud to call Google Cloud APIs is by using a downloaded service account key. However, this approach has 2 major pain points:
-
-* A management hassle, keys need to be stored securely and rotated often.
-* A security risk, keys are long term credentials that could be compromised.
-
-Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account.
-
-This example shows how to set up everything, both in Azure and Google Cloud, so a workload in Azure can access Google Cloud resources without a service account key. This will be possible by configuring workload identity federation to trust access tokens generated for a specific application in an Azure Active Directory (AAD) tenant.
-
-The following diagram illustrates how the VM will get a short-lived access token and use it to access a resource:
-
- ![Sequence diagram](sequence_diagram.png)
-
-The provided terraform configuration will set up the following architecture:
-
- ![Architecture](architecture.png)
-
-* On Azure:
-
- * An Azure Active Directory application and a service principal. By default, the new application grants all users in the Azure AD tenant permission to obtain access tokens. So an app role assignment will be required to restrict which identities can obtain access tokens for the application.
-
- * Optionally, all the resources required to have a VM configured to run with a system-assigned managed identity and accessible via SSH on a public IP using public key authentication, so we can log in to the machine and run the `gcloud` command to verify that everything works as expected.
-
-* On Google Cloud:
-
- * A Google Cloud project with:
-
- * A workload identity pool and provider configured to trust the AAD application
-
- * A service account with the Viewer role granted on the project. The external identities in the workload identity pool would be assigned the Workload Identity User role on that service account.
-
-## Running the example
-
-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=examples%2Fcloud-operations%2Fworkload-identity-federation), then go through the following steps to create resources:
-
-* `terraform init`
-* `terraform apply -var project_id=my-project-id`
-
-## Testing the example
-
-Once the resources have been created, do the following to verify that everything works as expected:
-
-1. Log in to the VM.
-
- If you have created the VM using this terraform configuration proceed the following way:
-
- * Copy the public IP address of the Azure VM and the username required to log in to the VM via SSH from the output.
-
- * Save the private key to a file
-
- `terraform state pull | jq -r '.outputs.tls_private_key.value' > private_key.pem`
-
- * Change the permissions on the private key file to 600
-
- `chmod 600 private_key.pem`
-
- * Login to the Azure VM using the following command:
-
- `ssh -i private_key.pem azureuser@VM_PUBLIC_IP`
-
- If you already had an existing VM with the gcloud CLI installed that you want to use, you will have assign its managed identity an application role as explained [here](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-assign-app-role-managed-identity-powershell#assign-a-managed-identity-access-to-another-applications-app-role).
-
-2. Create a file called credential.json in the VM with the contents of the `credential` output.
-
-3. Authorize gcloud to access Google Cloud with the credentials file created in the step before.
-
- `gcloud auth login --cred-file credential.json
-
-4. Get the Google Cloud project details
-
- `gcloud projects describe PROJECT_ID`
-
-
-Once done testing, you can clean up resources by running `terraform destroy`.
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [project_id](variables.tf#L26) | Identifier of the project that will contain the Pub/Sub topic that will be created from Azure and the service account that will be impersonated. | string
| ✓ | |
-| [project_create](variables.tf#L17) | Parameters for the creation of the new project. | object({…})
| | null
|
-| [vm_test](variables.tf#L31) | Flag indicating whether the infrastructure required to test that everything works should be created in Azure. | bool
| | false
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [credential](outputs.tf#L17) | Credential configuration file contents. | |
-| [tls_private_key](outputs.tf#L28) | Private key required to log in to the Azure VM via SSH. | ✓ |
-| [username](outputs.tf#L34) | Username required to log in to the Azure VM via SSH. | |
-| [vm_public_ip_address](outputs.tf#L39) | Azure VM public IP address. | |
-
-
diff --git a/examples/data-solutions/README.md b/examples/data-solutions/README.md
deleted file mode 100644
index 813e7cd752..0000000000
--- a/examples/data-solutions/README.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# GCP Data Services examples
-
-The examples in this folder implement **typical data service topologies** and **end-to-end scenarios**, that allow testing specific features like Cloud KMS to encrypt your data, or VPC-SC to mitigate data exfiltration.
-
-They are meant to be used as minimal but complete starting points to create actual infrastructure, and as playgrounds to experiment with specific Google Cloud features.
-
-## Examples
-
-### GCE and GCS CMEK via centralized Cloud KMS
-
- This [example](./cmek-via-centralized-kms/) implements [CMEK](https://cloud.google.com/kms/docs/cmek) for GCS and GCE, via keys hosted in KMS running in a centralized project. The example shows the basic resources and permissions for the typical use case of application projects implementing encryption at rest via a centrally managed KMS service.
-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"
|
-| [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"
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [bucket](outputs.tf#L15) | GCS Bucket URL. | |
-| [bucket_keys](outputs.tf#L20) | GCS Bucket Cloud KMS crypto keys. | |
-| [projects](outputs.tf#L25) | Project ids. | |
-| [vm](outputs.tf#L33) | GCE VM. | |
-| [vm_keys](outputs.tf#L41) | GCE VM Cloud KMS crypto keys. | |
-
-
diff --git a/examples/data-solutions/cmek-via-centralized-kms/main.tf b/examples/data-solutions/cmek-via-centralized-kms/main.tf
deleted file mode 100644
index 260d2e38b4..0000000000
--- a/examples/data-solutions/cmek-via-centralized-kms/main.tf
+++ /dev/null
@@ -1,143 +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
-#
-# 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.
-
-###############################################################################
-# Projects #
-###############################################################################
-
-module "project-service" {
- source = "../../../modules/project"
- name = var.project_service_name
- parent = var.root_node
- billing_account = var.billing_account
- services = [
- "compute.googleapis.com",
- "servicenetworking.googleapis.com",
- "storage-component.googleapis.com"
- ]
- oslogin = true
-}
-
-module "project-kms" {
- source = "../../../modules/project"
- name = var.project_kms_name
- parent = var.root_node
- billing_account = var.billing_account
- services = [
- "cloudkms.googleapis.com",
- "servicenetworking.googleapis.com"
- ]
- oslogin = true
-}
-
-###############################################################################
-# Networking #
-###############################################################################
-
-module "vpc" {
- source = "../../../modules/net-vpc"
- project_id = module.project-service.project_id
- name = var.vpc_name
- subnets = [
- {
- ip_cidr_range = var.vpc_ip_cidr_range
- name = var.vpc_subnet_name
- region = var.region
- secondary_ip_range = {}
- }
- ]
-}
-
-module "vpc-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.project-service.project_id
- network = module.vpc.name
- admin_ranges = [var.vpc_ip_cidr_range]
-}
-
-###############################################################################
-# KMS #
-###############################################################################
-
-module "kms" {
- source = "../../../modules/kms"
- project_id = module.project-kms.project_id
- keyring = {
- name = "my-keyring",
- location = var.location
- }
- 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}",
- ]
- }
- }
-}
-
-###############################################################################
-# GCE #
-###############################################################################
-
-module "vm_example" {
- source = "../../../modules/compute-vm"
- project_id = module.project-service.project_id
- zone = "${var.region}-b"
- name = "kms-vm"
- network_interfaces = [{
- network = module.vpc.self_link,
- subnetwork = module.vpc.subnet_self_links["${var.region}/subnet"],
- nat = false,
- addresses = null
- }]
- attached_disks = [
- {
- name = "data"
- size = 10
- source = null
- source_type = null
- options = null
- }
- ]
- boot_disk = {
- image = "projects/debian-cloud/global/images/family/debian-10"
- type = "pd-ssd"
- size = 10
- encrypt_disk = true
- }
- tags = ["ssh"]
- encryption = {
- encrypt_boot = true
- disk_encryption_key_raw = null
- kms_key_self_link = module.kms.key_ids.key-gce
- }
-}
-
-###############################################################################
-# GCS #
-###############################################################################
-
-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
-}
diff --git a/examples/data-solutions/cmek-via-centralized-kms/versions.tf b/examples/data-solutions/cmek-via-centralized-kms/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/data-solutions/cmek-via-centralized-kms/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/data-solutions/data-platform-foundations/01-landing.tf b/examples/data-solutions/data-platform-foundations/01-landing.tf
deleted file mode 100644
index 49f32aff95..0000000000
--- a/examples/data-solutions/data-platform-foundations/01-landing.tf
+++ /dev/null
@@ -1,135 +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
-#
-# 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 land project and resources.
-
-locals {
- land_orch_service_accounts = [
- module.load-sa-df-0.iam_email, module.orch-sa-cmp-0.iam_email
- ]
-}
-
-module "land-project" {
- source = "../../../modules/project"
- parent = var.folder_id
- billing_account = var.billing_account_id
- prefix = var.prefix
- name = "lnd${local.project_suffix}"
- group_iam = {
- (local.groups.data-engineers) = [
- "roles/bigquery.dataEditor",
- "roles/pubsub.editor",
- "roles/storage.admin",
- ]
- }
- iam = {
- "roles/bigquery.dataEditor" = [module.land-sa-bq-0.iam_email]
- "roles/bigquery.user" = [module.load-sa-df-0.iam_email]
- "roles/pubsub.publisher" = [module.land-sa-ps-0.iam_email]
- "roles/pubsub.subscriber" = concat(
- local.land_orch_service_accounts, [module.load-sa-df-0.iam_email]
- )
- "roles/storage.objectAdmin" = [module.load-sa-df-0.iam_email]
- "roles/storage.objectCreator" = [module.land-sa-cs-0.iam_email]
- "roles/storage.objectViewer" = [module.orch-sa-cmp-0.iam_email]
- "roles/storage.admin" = [module.load-sa-df-0.iam_email]
- }
- services = concat(var.project_services, [
- "bigquery.googleapis.com",
- "bigqueryreservation.googleapis.com",
- "bigquerystorage.googleapis.com",
- "cloudkms.googleapis.com",
- "pubsub.googleapis.com",
- "storage.googleapis.com",
- "storage-component.googleapis.com",
- ])
- service_encryption_key_ids = {
- bq = [try(local.service_encryption_keys.bq, null)]
- pubsub = [try(local.service_encryption_keys.pubsub, null)]
- storage = [try(local.service_encryption_keys.storage, null)]
- }
-}
-
-# Cloud Storage
-
-module "land-sa-cs-0" {
- source = "../../../modules/iam-service-account"
- project_id = module.land-project.project_id
- prefix = var.prefix
- name = "lnd-cs-0"
- display_name = "Data platform GCS landing service account."
- iam = {
- "roles/iam.serviceAccountTokenCreator" = [
- local.groups_iam.data-engineers
- ]
- }
-}
-
-module "land-cs-0" {
- source = "../../../modules/gcs"
- project_id = module.land-project.project_id
- prefix = var.prefix
- name = "lnd-cs-0"
- location = var.location
- storage_class = "MULTI_REGIONAL"
- encryption_key = try(local.service_encryption_keys.storage, null)
- force_destroy = var.data_force_destroy
- # retention_policy = {
- # retention_period = 7776000 # 90 * 24 * 60 * 60
- # is_locked = false
- # }
-}
-
-# PubSub
-
-module "land-sa-ps-0" {
- source = "../../../modules/iam-service-account"
- project_id = module.land-project.project_id
- prefix = var.prefix
- name = "lnd-ps-0"
- display_name = "Data platform PubSub landing service account"
- iam = {
- "roles/iam.serviceAccountTokenCreator" = [
- local.groups_iam.data-engineers
- ]
- }
-}
-
-module "land-ps-0" {
- source = "../../../modules/pubsub"
- project_id = module.land-project.project_id
- name = "${var.prefix}-lnd-ps-0"
- kms_key = try(local.service_encryption_keys.pubsub, null)
-}
-
-# BigQuery
-
-module "land-sa-bq-0" {
- source = "../../../modules/iam-service-account"
- project_id = module.land-project.project_id
- prefix = var.prefix
- name = "lnd-bq-0"
- display_name = "Data platform BigQuery landing service account"
- iam = {
- "roles/iam.serviceAccountTokenCreator" = [local.groups_iam.data-engineers]
- }
-}
-
-module "land-bq-0" {
- source = "../../../modules/bigquery-dataset"
- project_id = module.land-project.project_id
- id = "${replace(var.prefix, "-", "_")}lnd_bq_0"
- location = var.location
- encryption_key = try(local.service_encryption_keys.bq, null)
-}
diff --git a/examples/data-solutions/data-platform-foundations/03-composer.tf b/examples/data-solutions/data-platform-foundations/03-composer.tf
deleted file mode 100644
index 231d0cc52e..0000000000
--- a/examples/data-solutions/data-platform-foundations/03-composer.tf
+++ /dev/null
@@ -1,136 +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
-#
-# 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 Orchestration Cloud Composer definition.
-
-module "orch-sa-cmp-0" {
- source = "../../../modules/iam-service-account"
- project_id = module.orch-project.project_id
- prefix = var.prefix
- name = "orc-cmp-0"
- display_name = "Data platform Composer service account"
- iam = {
- "roles/iam.serviceAccountTokenCreator" = [local.groups_iam.data-engineers]
- "roles/iam.serviceAccountUser" = [module.orch-sa-cmp-0.iam_email]
- }
-}
-
-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
- config {
- node_count = var.composer_config.node_count
- 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
- ip_allocation_policy {
- use_ip_aliases = "true"
- cluster_secondary_range_name = try(
- var.network_config.composer_secondary_ranges.pods, "pods"
- )
- services_secondary_range_name = try(
- var.network_config.composer_secondary_ranges.services, "services"
- )
- }
- }
- private_environment_config {
- enable_private_endpoint = "true"
- cloud_sql_ipv4_cidr_block = try(
- var.network_config.composer_ip_ranges.cloudsql, "10.20.10.0/24"
- )
- 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
- DF_KMS_KEY = try(var.service_encryption_keys.dataflow, "")
- DTL_L0_PRJ = module.lake-0-project.project_id
- DTL_L0_BQ_DATASET = module.lake-0-bq-0.dataset_id
- DTL_L0_GCS = module.lake-0-cs-0.url
- DTL_L1_PRJ = module.lake-1-project.project_id
- DTL_L1_BQ_DATASET = module.lake-1-bq-0.dataset_id
- DTL_L1_GCS = module.lake-1-cs-0.url
- DTL_L2_PRJ = module.lake-2-project.project_id
- DTL_L2_BQ_DATASET = module.lake-2-bq-0.dataset_id
- DTL_L2_GCS = module.lake-2-cs-0.url
- DTL_PLG_PRJ = module.lake-plg-project.project_id
- DTL_PLG_BQ_DATASET = module.lake-plg-bq-0.dataset_id
- DTL_PLG_GCS = module.lake-plg-cs-0.url
- GCP_REGION = var.region
- LND_PRJ = module.land-project.project_id
- LND_BQ = module.land-bq-0.dataset_id
- LND_GCS = module.land-cs-0.url
- LND_PS = module.land-ps-0.id
- 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)
- ? { 1 = 1 }
- : {}
- )
- content {
- kms_key_name = try(local.service_encryption_keys.composer, 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,
- ]
-}
diff --git a/examples/data-solutions/data-platform-foundations/05-datalake.tf b/examples/data-solutions/data-platform-foundations/05-datalake.tf
deleted file mode 100644
index 64ec1b2420..0000000000
--- a/examples/data-solutions/data-platform-foundations/05-datalake.tf
+++ /dev/null
@@ -1,228 +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
-#
-# 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 Datalake projects.
-
-locals {
- lake_group_iam = {
- (local.groups.data-engineers) = [
- "roles/bigquery.dataEditor",
- "roles/storage.admin",
- ],
- (local.groups.data-analysts) = [
- "roles/bigquery.dataViewer",
- "roles/bigquery.jobUser",
- "roles/bigquery.user",
- "roles/datacatalog.viewer",
- "roles/datacatalog.tagTemplateViewer",
- "roles/storage.objectViewer",
- ]
- }
- lake_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.user",
- "roles/datacatalog.viewer",
- "roles/datacatalog.tagTemplateViewer",
- "roles/storage.objectAdmin",
- ]
- }
- lake_0_iam = {
- "roles/bigquery.dataEditor" = [
- module.load-sa-df-0.iam_email,
- module.transf-sa-df-0.iam_email,
- module.transf-sa-bq-0.iam_email,
- ]
- "roles/bigquery.jobUser" = [
- module.load-sa-df-0.iam_email,
- ]
- "roles/storage.objectCreator" = [
- module.load-sa-df-0.iam_email,
- ]
- }
- lake_iam = {
- "roles/bigquery.dataEditor" = [
- module.transf-sa-df-0.iam_email,
- module.transf-sa-bq-0.iam_email,
- ]
- "roles/bigquery.jobUser" = [
- module.transf-sa-bq-0.iam_email,
- ]
- "roles/storage.objectCreator" = [
- module.transf-sa-df-0.iam_email,
- ]
- "roles/storage.objectViewer" = [
- module.transf-sa-df-0.iam_email,
- ]
- }
- lake_services = concat(var.project_services, [
- "bigquery.googleapis.com",
- "bigqueryreservation.googleapis.com",
- "bigquerystorage.googleapis.com",
- "cloudkms.googleapis.com",
- "compute.googleapis.com",
- "dataflow.googleapis.com",
- "pubsub.googleapis.com",
- "servicenetworking.googleapis.com",
- "storage.googleapis.com",
- "storage-component.googleapis.com"
- ])
-}
-
-# Project
-
-module "lake-0-project" {
- source = "../../../modules/project"
- parent = var.folder_id
- billing_account = var.billing_account_id
- prefix = var.prefix
- name = "dtl-0${local.project_suffix}"
- group_iam = local.lake_group_iam
- iam = local.lake_0_iam
- services = local.lake_services
- service_encryption_key_ids = {
- bq = [try(local.service_encryption_keys.bq, null)]
- storage = [try(local.service_encryption_keys.storage, null)]
- }
-}
-
-module "lake-1-project" {
- source = "../../../modules/project"
- parent = var.folder_id
- billing_account = var.billing_account_id
- prefix = var.prefix
- name = "dtl-1${local.project_suffix}"
- group_iam = local.lake_group_iam
- iam = local.lake_iam
- services = local.lake_services
- service_encryption_key_ids = {
- bq = [try(local.service_encryption_keys.bq, null)]
- storage = [try(local.service_encryption_keys.storage, null)]
- }
-}
-
-module "lake-2-project" {
- source = "../../../modules/project"
- parent = var.folder_id
- billing_account = var.billing_account_id
- prefix = var.prefix
- name = "dtl-2${local.project_suffix}"
- group_iam = local.lake_group_iam
- iam = local.lake_iam
- services = local.lake_services
- service_encryption_key_ids = {
- bq = [try(local.service_encryption_keys.bq, null)]
- storage = [try(local.service_encryption_keys.storage, null)]
- }
-}
-
-module "lake-plg-project" {
- source = "../../../modules/project"
- parent = var.folder_id
- billing_account = var.billing_account_id
- prefix = var.prefix
- name = "dtl-plg${local.project_suffix}"
- group_iam = local.lake_plg_group_iam
- iam = {}
- services = local.lake_services
- service_encryption_key_ids = {
- bq = [try(local.service_encryption_keys.bq, null)]
- storage = [try(local.service_encryption_keys.storage, null)]
- }
-}
-
-# Bigquery
-
-module "lake-0-bq-0" {
- source = "../../../modules/bigquery-dataset"
- project_id = module.lake-0-project.project_id
- id = "${replace(var.prefix, "-", "_")}_dtl_0_bq_0"
- location = var.location
- encryption_key = try(local.service_encryption_keys.bq, null)
-}
-
-module "lake-1-bq-0" {
- source = "../../../modules/bigquery-dataset"
- project_id = module.lake-1-project.project_id
- id = "${replace(var.prefix, "-", "_")}_dtl_1_bq_0"
- location = var.location
- encryption_key = try(local.service_encryption_keys.bq, null)
-}
-
-module "lake-2-bq-0" {
- source = "../../../modules/bigquery-dataset"
- project_id = module.lake-2-project.project_id
- id = "${replace(var.prefix, "-", "_")}_dtl_2_bq_0"
- location = var.location
- encryption_key = try(local.service_encryption_keys.bq, null)
-}
-
-module "lake-plg-bq-0" {
- source = "../../../modules/bigquery-dataset"
- project_id = module.lake-plg-project.project_id
- id = "${replace(var.prefix, "-", "_")}_dtl_plg_bq_0"
- location = var.location
- encryption_key = try(local.service_encryption_keys.bq, null)
-}
-
-# Cloud storage
-
-module "lake-0-cs-0" {
- source = "../../../modules/gcs"
- project_id = module.lake-0-project.project_id
- prefix = var.prefix
- name = "dtl-0-cs-0"
- location = var.location
- storage_class = "MULTI_REGIONAL"
- encryption_key = try(local.service_encryption_keys.storage, null)
- force_destroy = var.data_force_destroy
-}
-
-module "lake-1-cs-0" {
- source = "../../../modules/gcs"
- project_id = module.lake-1-project.project_id
- prefix = var.prefix
- name = "dtl-1-cs-0"
- location = var.location
- storage_class = "MULTI_REGIONAL"
- encryption_key = try(local.service_encryption_keys.storage, null)
- force_destroy = var.data_force_destroy
-}
-
-module "lake-2-cs-0" {
- source = "../../../modules/gcs"
- project_id = module.lake-2-project.project_id
- prefix = var.prefix
- name = "dtl-2-cs-0"
- location = var.location
- storage_class = "MULTI_REGIONAL"
- encryption_key = try(local.service_encryption_keys.storage, null)
- force_destroy = var.data_force_destroy
-}
-
-module "lake-plg-cs-0" {
- source = "../../../modules/gcs"
- project_id = module.lake-plg-project.project_id
- prefix = var.prefix
- name = "dtl-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/examples/data-solutions/data-platform-foundations/06-common.tf b/examples/data-solutions/data-platform-foundations/06-common.tf
deleted file mode 100644
index cc18a46f5c..0000000000
--- a/examples/data-solutions/data-platform-foundations/06-common.tf
+++ /dev/null
@@ -1,83 +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
-#
-# 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 common project.
-
-module "common-project" {
- source = "../../../modules/project"
- parent = var.folder_id
- billing_account = var.billing_account_id
- prefix = var.prefix
- name = "cmn${local.project_suffix}"
- group_iam = {
- (local.groups.data-engineers) = [
- "roles/dlp.reader",
- "roles/dlp.user",
- "roles/dlp.estimatesAdmin",
- ]
- (local.groups.data-security) = [
- "roles/dlp.admin",
- ]
- }
- iam = {
- "roles/dlp.user" = [
- module.load-sa-df-0.iam_email,
- module.transf-sa-df-0.iam_email
- ]
- }
- services = concat(var.project_services, [
- "datacatalog.googleapis.com",
- "dlp.googleapis.com",
- ])
-}
-
-# To create KMS keys in the common projet: uncomment this section and assigne key links accondingly in local.service_encryption_keys variable
-
-# module "cmn-kms-0" {
-# source = "../../../modules/kms"
-# project_id = module.common-project.project_id
-# keyring = {
-# name = "${var.prefix}-kr-global",
-# location = "global"
-# }
-# keys = {
-# pubsub = null
-# }
-# }
-
-# module "cmn-kms-1" {
-# source = "../../../modules/kms"
-# project_id = module.common-project.project_id
-# keyring = {
-# name = "${var.prefix}-kr-mregional",
-# location = var.location
-# }
-# keys = {
-# bq = null
-# storage = null
-# }
-# }
-
-# module "cmn-kms-2" {
-# source = "../../../modules/kms"
-# project_id = module.cmn-prj.project_id
-# keyring = {
-# name = "${var.prefix}-kr-regional",
-# location = var.region
-# }
-# keys = {
-# composer = null
-# dataflow = null
-# }
-# }
diff --git a/examples/data-solutions/data-platform-foundations/IAM.md b/examples/data-solutions/data-platform-foundations/IAM.md
deleted file mode 100644
index aed1c40553..0000000000
--- a/examples/data-solutions/data-platform-foundations/IAM.md
+++ /dev/null
@@ -1,84 +0,0 @@
-# IAM bindings reference
-
-Legend: +
additive, •
conditional.
-
-## Project cmn
-
-| members | roles |
-|---|---|
-|gcp-data-engineersstring
| ✓ | |
-| [folder_id](variables.tf#L42) | Folder to be used for the networking resources in folders/nnnn format. | string
| ✓ | |
-| [organization_domain](variables.tf#L87) | Organization domain. | string
| ✓ | |
-| [prefix](variables.tf#L92) | Unique prefix used for resource names. | string
| ✓ | |
-| [composer_config](variables.tf#L22) | Cloud Composer config. | object({…})
| | {…}
|
-| [data_force_destroy](variables.tf#L36) | Flag to set 'force_destroy' on data services like BiguQery or Cloud Storage. | bool
| | false
|
-| [groups](variables.tf#L53) | User groups. | map(string)
| | {…}
|
-| [location](variables.tf#L47) | 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
|
-| [project_services](variables.tf#L97) | List of core services enabled on all projects. | list(string)
| | […]
|
-| [project_suffix](variables.tf#L108) | Suffix used only for project ids. | string
| | null
|
-| [region](variables.tf#L114) | Region used for regional resources. | string
| | "europe-west1"
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [bigquery-datasets](outputs.tf#L17) | BigQuery datasets. | |
-| [demo_commands](outputs.tf#L93) | Demo commands. | |
-| [gcs-buckets](outputs.tf#L28) | GCS buckets. | |
-| [kms_keys](outputs.tf#L42) | Cloud MKS keys. | |
-| [projects](outputs.tf#L47) | GCP Projects informations. | |
-| [vpc_network](outputs.tf#L75) | VPC network. | |
-| [vpc_subnet](outputs.tf#L84) | VPC subnetworks. | |
-
-
-## TODOs
-
-Features to add in future releases:
-
-- Add support for Column level access on BigQuery
-- Add example templates for Data Catalog
-- Add example on how to use Cloud Data Loss Prevention
-- Add solution to handle Tables, Views, and Authorized Views lifecycle
-- Add solution to handle Metadata lifecycle
-
-## To Test/Fix
-
-- Composer require "Require OS Login" not enforced
-- External Shared-VPC
diff --git a/examples/data-solutions/data-platform-foundations/demo/README.md b/examples/data-solutions/data-platform-foundations/demo/README.md
deleted file mode 100644
index 78297f7a1a..0000000000
--- a/examples/data-solutions/data-platform-foundations/demo/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Data ingestion Demo
-
-In this folder you can find an example to ingest data on the `data platfoem` instantiated in [here](../). See details in the [README.m](../#demo-pipeline) to run the demo.
\ No newline at end of file
diff --git a/examples/data-solutions/data-platform-foundations/demo/datapipeline.py b/examples/data-solutions/data-platform-foundations/demo/datapipeline.py
deleted file mode 100644
index fd633ebd81..0000000000
--- a/examples/data-solutions/data-platform-foundations/demo/datapipeline.py
+++ /dev/null
@@ -1,204 +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
-#
-# 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 csv
-import datetime
-import io
-import logging
-import os
-
-from airflow import models
-from airflow.contrib.operators.dataflow_operator import DataflowTemplateOperator
-from airflow.operators import dummy
-from airflow.providers.google.cloud.operators.bigquery import BigQueryInsertJobOperator
-
-# --------------------------------------------------------------------------------
-# Set variables
-# ------------------------------------------------------------
-BQ_LOCATION = os.environ.get("BQ_LOCATION")
-DTL_L0_PRJ = os.environ.get("DTL_L0_PRJ")
-DTL_L0_BQ_DATASET = os.environ.get("DTL_L0_BQ_DATASET")
-DTL_L0_GCS = os.environ.get("DTL_L0_GCS")
-DTL_L1_PRJ = os.environ.get("DTL_L1_PRJ")
-DTL_L1_BQ_DATASET = os.environ.get("DTL_L1_BQ_DATASET")
-DTL_L1_GCS = os.environ.get("DTL_L1_GCS")
-DTL_L2_PRJ = os.environ.get("DTL_L2_PRJ")
-DTL_L2_BQ_DATASET = os.environ.get("DTL_L2_BQ_DATASET")
-DTL_L2_GCS = os.environ.get("DTL_L2_GCS")
-DTL_PLG_PRJ = os.environ.get("DTL_PLG_PRJ")
-DTL_PLG_BQ_DATASET = os.environ.get("DTL_PLG_BQ_DATASET")
-DTL_PLG_GCS = os.environ.get("DTL_PLG_GCS")
-GCP_REGION = os.environ.get("GCP_REGION")
-LND_PRJ = os.environ.get("LND_PRJ")
-LND_BQ = os.environ.get("LND_BQ")
-LND_GCS = os.environ.get("LND_GCS")
-LND_PS = os.environ.get("LND_PS")
-LOD_PRJ = os.environ.get("LOD_PRJ")
-LOD_GCS_STAGING = os.environ.get("LOD_GCS_STAGING")
-LOD_NET_VPC = os.environ.get("LOD_NET_VPC")
-LOD_NET_SUBNET = os.environ.get("LOD_NET_SUBNET")
-LOD_SA_DF = os.environ.get("LOD_SA_DF")
-ORC_PRJ = os.environ.get("ORC_PRJ")
-ORC_GCS = os.environ.get("ORC_GCS")
-TRF_PRJ = os.environ.get("TRF_PRJ")
-TRF_GCS_STAGING = os.environ.get("TRF_GCS_STAGING")
-TRF_NET_VPC = os.environ.get("TRF_NET_VPC")
-TRF_NET_SUBNET = os.environ.get("TRF_NET_SUBNET")
-TRF_SA_DF = os.environ.get("TRF_SA_DF")
-TRF_SA_BQ = os.environ.get("TRF_SA_BQ")
-DF_KMS_KEY = os.environ.get("DF_KMS_KEY", "")
-DF_REGION = os.environ.get("GCP_REGION")
-DF_ZONE = os.environ.get("GCP_REGION") + "-b"
-
-# --------------------------------------------------------------------------------
-# 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_default_options': {
- 'project': LOD_PRJ,
- 'location': DF_REGION,
- 'zone': DF_ZONE,
- 'stagingLocation': LOD_GCS_STAGING,
- 'tempLocation': LOD_GCS_STAGING + "/tmp",
- 'serviceAccountEmail': LOD_SA_DF,
- 'subnetwork': LOD_NET_SUBNET,
- 'ipConfiguration': "WORKER_IP_PRIVATE",
- 'kmsKeyName' : DF_KMS_KEY
- },
-}
-
-# --------------------------------------------------------------------------------
-# Main DAG
-# --------------------------------------------------------------------------------
-
-with models.DAG(
- 'data_pipeline_dag',
- 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'
- )
-
- customers_import = DataflowTemplateOperator(
- task_id="dataflow_customer_import",
- template="gs://dataflow-templates/latest/GCS_Text_to_BigQuery",
- parameters={
- "javascriptTextTransformFunctionName": "transform",
- "JSONPath": ORC_GCS + "/customers_schema.json",
- "javascriptTextTransformGcsPath": ORC_GCS + "/customers_udf.js",
- "inputFilePattern": LND_GCS + "/customers.csv",
- "outputTable": DTL_L0_PRJ + ":"+DTL_L0_BQ_DATASET+".customers",
- "bigQueryLoadingTemporaryDirectory": LOD_GCS_STAGING + "/tmp/bq/",
- },
- )
-
- purchases_import = DataflowTemplateOperator(
- task_id="dataflow_purchases_import",
- template="gs://dataflow-templates/latest/GCS_Text_to_BigQuery",
- parameters={
- "javascriptTextTransformFunctionName": "transform",
- "JSONPath": ORC_GCS + "/purchases_schema.json",
- "javascriptTextTransformGcsPath": ORC_GCS + "/purchases_udf.js",
- "inputFilePattern": LND_GCS + "/purchases.csv",
- "outputTable": DTL_L0_PRJ + ":"+DTL_L0_BQ_DATASET+".purchases",
- "bigQueryLoadingTemporaryDirectory": LOD_GCS_STAGING + "/tmp/bq/",
- },
- )
-
- 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 `{dtl_0_prj}.{dtl_0_dataset}.customers` c
- JOIN `{dtl_0_prj}.{dtl_0_dataset}.purchases` p ON c.id = p.customer_id
- """.format(dtl_0_prj=DTL_L0_PRJ, dtl_0_dataset=DTL_L0_BQ_DATASET, ),
- 'destinationTable':{
- 'projectId': DTL_L1_PRJ,
- 'datasetId': DTL_L1_BQ_DATASET,
- 'tableId': 'customer_purchase'
- },
- 'writeDisposition':'WRITE_TRUNCATE',
- "useLegacySql": False
- }
- },
- impersonation_chain=[TRF_SA_BQ]
- )
-
- l2_customer_purchase = BigQueryInsertJobOperator(
- task_id='bq_l2_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 `{dtl_1_prj}.{dtl_1_dataset}.customer_purchase`
- """.format(dtl_1_prj=DTL_L1_PRJ, dtl_1_dataset=DTL_L1_BQ_DATASET, ),
- 'destinationTable':{
- 'projectId': DTL_L2_PRJ,
- 'datasetId': DTL_L2_BQ_DATASET,
- 'tableId': 'customer_purchase'
- },
- 'writeDisposition':'WRITE_TRUNCATE',
- "useLegacySql": False
- }
- },
- impersonation_chain=[TRF_SA_BQ]
- )
-
- start >> [customers_import, purchases_import] >> join_customer_purchase >> l2_customer_purchase >> end
diff --git a/examples/data-solutions/data-platform-foundations/images/overview_diagram.png b/examples/data-solutions/data-platform-foundations/images/overview_diagram.png
deleted file mode 100644
index b1a0f7887c..0000000000
Binary files a/examples/data-solutions/data-platform-foundations/images/overview_diagram.png and /dev/null differ
diff --git a/examples/data-solutions/data-platform-foundations/outputs.tf b/examples/data-solutions/data-platform-foundations/outputs.tf
deleted file mode 100644
index e5a2de3ee0..0000000000
--- a/examples/data-solutions/data-platform-foundations/outputs.tf
+++ /dev/null
@@ -1,104 +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
-#
-# 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 Output variables.
-
-output "bigquery-datasets" {
- description = "BigQuery datasets."
- value = {
- land-bq-0 = module.land-bq-0.dataset_id,
- lake-0-bq-0 = module.lake-0-bq-0.dataset_id,
- lake-1-bq-0 = module.lake-1-bq-0.dataset_id,
- lake-2-bq-0 = module.lake-2-bq-0.dataset_id,
- lake-plg-bq-0 = module.lake-plg-bq-0.dataset_id,
- }
-}
-
-output "gcs-buckets" {
- description = "GCS buckets."
- value = {
- lake-0-cs-0 = module.lake-0-cs-0.name,
- lake-1-cs-0 = module.lake-1-cs-0.name,
- lake-2-cs-0 = module.lake-2-cs-0.name,
- lake-plg-cs-0 = module.lake-plg-cs-0.name,
- land-cs-0 = module.land-cs-0.name,
- lod-cs-df = module.load-cs-df-0.name,
- orch-cs-0 = module.orch-cs-0.name,
- transf-cs-df = module.transf-cs-df-0.name,
- }
-}
-
-output "kms_keys" {
- description = "Cloud MKS keys."
- value = local.service_encryption_keys
-}
-
-output "projects" {
- description = "GCP Projects informations."
- value = {
- project_number = {
- lake-0 = module.lake-0-project.number,
- lake-1 = module.lake-1-project.number,
- lake-2 = module.lake-2-project.number,
- lake-plg = module.lake-plg-project.number,
- exposure = module.exp-project.number,
- landing = module.land-project.number,
- load = module.load-project.number,
- orchestration = module.orch-project.number,
- transformation = module.transf-project.number,
- }
- project_id = {
- lake-0 = module.lake-0-project.project_id,
- lake-1 = module.lake-1-project.project_id,
- lake-2 = module.lake-2-project.project_id,
- lake-plg = module.lake-plg-project.project_id,
- exposure = module.exp-project.project_id,
- landing = module.land-project.project_id,
- load = module.load-project.project_id,
- orchestration = module.orch-project.project_id,
- transformation = module.transf-project.project_id,
- }
- }
-}
-
-output "vpc_network" {
- description = "VPC network."
- value = {
- load = local.load_vpc
- orchestration = local.orch_vpc
- transformation = local.transf_vpc
- }
-}
-
-output "vpc_subnet" {
- description = "VPC subnetworks."
- value = {
- load = local.load_subnet
- orchestration = local.orch_subnet
- transformation = local.transf_subnet
- }
-}
-
-output "demo_commands" {
- description = "Demo commands."
- value = {
- 01 = "gsutil -i ${module.land-sa-cs-0.email} cp demo/data/*.csv gs://${module.land-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
| ✓ | |
-| [cmek_encryption](variables.tf#L15) | Flag to enable CMEK on GCP resources created. | bool
| | false
|
-| [data_eng_principals](variables.tf#L21) | Groups with Service Account Token creator role on service accounts in IAM format, eg 'group:group@domain.com'. | list(string)
| | []
|
-| [project_create](variables.tf#L31) | Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format. | object({…})
| | null
|
-| [region](variables.tf#L45) | The region where resources will be deployed. | string
| | "europe-west1"
|
-| [vpc_subnet_range](variables.tf#L51) | Ip range used for the VPC subnet created for the example. | string
| | "10.0.0.0/20"
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [bq_tables](outputs.tf#L15) | Bigquery Tables. | |
-| [buckets](outputs.tf#L20) | GCS bucket Cloud KMS crypto keys. | |
-| [command_01_gcs](outputs.tf#L43) | gcloud command to copy data into the created bucket impersonating the service account. | |
-| [command_02_dataflow](outputs.tf#L48) | Command to run Dataflow template impersonating the service account. | |
-| [command_03_bq](outputs.tf#L69) | BigQuery command to query imported data. | |
-| [project_id](outputs.tf#L28) | Project id. | |
-| [service_accounts](outputs.tf#L33) | Service account. | |
-
-
diff --git a/examples/data-solutions/gcs-to-bq-with-least-privileges/main.tf b/examples/data-solutions/gcs-to-bq-with-least-privileges/main.tf
deleted file mode 100644
index 8c8486440e..0000000000
--- a/examples/data-solutions/gcs-to-bq-with-least-privileges/main.tf
+++ /dev/null
@@ -1,106 +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
-#
-# 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.
-
-locals {
- iam = {
- # GCS roles
- "roles/storage.objectAdmin" = [
- module.service-account-df.iam_email,
- module.service-account-landing.iam_email
- ],
- "roles/storage.objectViewer" = [
- module.service-account-orch.iam_email,
- ],
- # BigQuery roles
- "roles/bigquery.admin" = concat([
- module.service-account-orch.iam_email,
- ], var.data_eng_principals
- )
- "roles/bigquery.dataEditor" = [
- module.service-account-df.iam_email,
- module.service-account-bq.iam_email
- ]
- "roles/bigquery.dataViewer" = [
- module.service-account-bq.iam_email,
- module.service-account-orch.iam_email
- ]
- "roles/bigquery.jobUser" = [
- module.service-account-df.iam_email,
- module.service-account-bq.iam_email
- ]
- "roles/bigquery.user" = [
- module.service-account-bq.iam_email,
- module.service-account-df.iam_email
- ]
- # common roles
- "roles/logging.logWriter" = [
- module.service-account-bq.iam_email,
- module.service-account-landing.iam_email,
- module.service-account-orch.iam_email,
- ]
- "roles/monitoring.metricWriter" = [
- module.service-account-bq.iam_email,
- module.service-account-landing.iam_email,
- module.service-account-orch.iam_email,
- ]
- "roles/iam.serviceAccountUser" = [
- module.service-account-orch.iam_email,
- ]
- "roles/iam.serviceAccountTokenCreator" = concat(
- var.data_eng_principals
- )
- # Dataflow roles
- "roles/dataflow.admin" = concat(
- [module.service-account-orch.iam_email],
- var.data_eng_principals
- )
- "roles/dataflow.worker" = [
- module.service-account-df.iam_email,
- ]
- "roles/dataflow.developer" = var.data_eng_principals
- "roles/compute.viewer" = var.data_eng_principals
- # network roles
- "roles/compute.networkUser" = [
- module.service-account-df.iam_email,
- "serviceAccount:${module.project.service_accounts.robots.dataflow}"
- ]
- }
-}
-
-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.project_create == null ? null : var.prefix
- services = [
- "bigquery.googleapis.com",
- "bigquerystorage.googleapis.com",
- "bigqueryreservation.googleapis.com",
- "cloudkms.googleapis.com",
- "compute.googleapis.com",
- "dataflow.googleapis.com",
- "servicenetworking.googleapis.com",
- "storage.googleapis.com",
- "storage-component.googleapis.com",
- ]
-
- # additive IAM bindings avoid disrupting bindings in existing project
- iam = var.project_create != null ? local.iam : {}
- iam_additive = var.project_create == null ? local.iam : {}
- service_config = {
- disable_on_destroy = false, disable_dependent_services = false
- }
-}
diff --git a/examples/data-solutions/gcs-to-bq-with-least-privileges/outputs.tf b/examples/data-solutions/gcs-to-bq-with-least-privileges/outputs.tf
deleted file mode 100644
index 7ffd063405..0000000000
--- a/examples/data-solutions/gcs-to-bq-with-least-privileges/outputs.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
-#
-# 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 "bq_tables" {
- description = "Bigquery Tables."
- value = module.bigquery-dataset.table_ids
-}
-
-output "buckets" {
- description = "GCS bucket Cloud KMS crypto keys."
- value = {
- data = module.gcs-data.name
- df-tmp = module.gcs-df-tmp.name
- }
-}
-
-output "project_id" {
- description = "Project id."
- value = module.project.project_id
-}
-
-output "service_accounts" {
- description = "Service account."
- value = {
- bq = module.service-account-bq.email
- df = module.service-account-df.email
- orch = module.service-account-orch.email
- landing = module.service-account-landing.email
- }
-}
-
-output "command_01_gcs" {
- description = "gcloud command to copy data into the created bucket impersonating the service account."
- value = "gsutil -i ${module.service-account-landing.email} cp data-demo/* ${module.gcs-data.url}"
-}
-
-output "command_02_dataflow" {
- description = "Command to run Dataflow template impersonating the service account."
- value = templatefile("${path.module}/dataflow.tftpl", {
- sa_orch_email = module.service-account-orch.email
- project_id = module.project.project_id
- region = var.region
- subnet = module.vpc.subnets["${var.region}/subnet"].self_link
- gcs_df_stg = format("%s/%s", module.gcs-df-tmp.url, "stg")
- sa_df_email = module.service-account-df.email
- cmek_encryption = var.cmek_encryption
- kms_key_df = var.cmek_encryption ? module.kms[0].key_ids.key-df : null
- gcs_data = module.gcs-data.url
- data_schema_file = format("%s/%s", module.gcs-data.url, "person_schema.json")
- data_udf_file = format("%s/%s", module.gcs-data.url, "person_udf.js")
- data_file = format("%s/%s", module.gcs-data.url, "person.csv")
- bigquery_dataset = module.bigquery-dataset.dataset_id
- bigquery_table = module.bigquery-dataset.tables["person"].table_id
- gcs_df_tmp = format("%s/%s", module.gcs-df-tmp.url, "tmp")
- })
-}
-
-output "command_03_bq" {
- description = "BigQuery command to query imported data."
- value = templatefile("${path.module}/bigquery.tftpl", {
- project_id = module.project.project_id
- bigquery_dataset = module.bigquery-dataset.dataset_id
- bigquery_table = module.bigquery-dataset.tables["person"].table_id
- sql_limit = 1000
- })
-}
diff --git a/examples/data-solutions/gcs-to-bq-with-least-privileges/variables.tf b/examples/data-solutions/gcs-to-bq-with-least-privileges/variables.tf
deleted file mode 100644
index 6863398720..0000000000
--- a/examples/data-solutions/gcs-to-bq-with-least-privileges/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
-#
-# 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.
-
-variable "cmek_encryption" {
- description = "Flag to enable CMEK on GCP resources created."
- type = bool
- default = false
-}
-
-variable "data_eng_principals" {
- description = "Groups with Service Account Token creator role on service accounts in IAM format, eg 'group:group@domain.com'."
- type = list(string)
- default = []
-}
-variable "prefix" {
- description = "Unique prefix used for resource names. Not used for project if 'project_create' is null."
- type = string
-}
-
-variable "project_create" {
- description = "Provide values if project creation is needed, uses existing project if null. Parent is in 'folders/nnn' or 'organizations/nnn' format."
- type = object({
- billing_account_id = string
- parent = string
- })
- default = null
-}
-
-variable "project_id" {
- description = "Project id, references existing project if `project_create` is null."
- type = string
-}
-
-variable "region" {
- description = "The region where resources will be deployed."
- type = string
- default = "europe-west1"
-}
-
-variable "vpc_subnet_range" {
- description = "Ip range used for the VPC subnet created for the example."
- type = string
- default = "10.0.0.0/20"
-}
diff --git a/examples/data-solutions/gcs-to-bq-with-least-privileges/versions.tf b/examples/data-solutions/gcs-to-bq-with-least-privileges/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/data-solutions/gcs-to-bq-with-least-privileges/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/data-solutions/gcs-to-bq-with-least-privileges/vpc.tf b/examples/data-solutions/gcs-to-bq-with-least-privileges/vpc.tf
deleted file mode 100644
index d8cc33244f..0000000000
--- a/examples/data-solutions/gcs-to-bq-with-least-privileges/vpc.tf
+++ /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
-#
-# 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.
-
-module "vpc" {
- source = "../../../modules/net-vpc"
- project_id = module.project.project_id
- name = "${var.prefix}-vpc"
- subnets = [
- {
- ip_cidr_range = var.vpc_subnet_range
- name = "subnet"
- region = var.region
- secondary_ip_range = {}
- }
- ]
-}
-
-module "vpc-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.project.project_id
- network = module.vpc.name
- admin_ranges = [var.vpc_subnet_range]
-}
-
-module "nat" {
- source = "../../../modules/net-cloudnat"
- project_id = module.project.project_id
- region = var.region
- name = "${var.prefix}-default"
- router_network = module.vpc.name
-}
diff --git a/examples/factories/README.md b/examples/factories/README.md
deleted file mode 100644
index b620f29ece..0000000000
--- a/examples/factories/README.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# The why and the how of Resource Factories
-
-Terraform modules can be designed - where it makes sense - to implement a resource factory, which is a configuration-driven approach to resource creation meant to:
-
-- accelerate and rationalize the repetitive creation of common resources, such as firewall rules and subnets
-- enable teams without Terraform specific knowledge to leverage IaC via human-friendly and machine-parseable YAML files
-- make it simple to implement specific requirements and best practices (e.g. "always enable PGA for GCP subnets", or "only allow using regions `europe-west1` and `europe-west3`")
-- codify and centralise business logics and policies (e.g. labels and naming conventions)
-- allow to easily parse and understand sets of specific resources, for documentation purposes
-
-Generally speaking, the configurations for a resource factory consists in one or more YaML files, optionally grouped in folders, that describe resources following a well defined, validable schema, such as in the example below for the subnet factory of the [`net-vpc`](../../modules/net-vpc) module, which allows for the massive creation of subnets for a given VPC.
-
-```yaml
-region: europe-west3
-ip_cidr_range: 10.0.0.0/24
-description: Sample Subnet in project project-prod-a, vpc-alpha
-secondary_ip_ranges:
- secondary-range-a: 192.168.0.0/24
- secondary-range-b: 192.168.1.0/24
-```
-
-Terraform natively supports YaML, JSON and CSV parsing - however Fabric has decided to embrace YaML for the following reasons:
-
-- YaML is easier to parse for a human, and allows for comments and nested, complex structures
-- JSON and CSV can't include comments, which can be used to document configurations, but are often useful to bridge from other systems in automated pipelines
-- JSON is more verbose (reads: longer) and harder to parse visually for humans
-- CSV isn't often expressive enough (e.g. dit oesn't allow for nested structures)
-
-If needed, converting factories to consume JSON is a matter of switching from `yamldecode()` to `jsondecode()` in the right place on each module.
-
-## Resource factories in Fabric
-
-### Fabric Modules
-
-- [folder](../../modules/folder/README.md#firewall-policy-factory) and [organization](../../modules/organization/README.md#firewall-policy-factory) implement factories for [hierarchical firewall policies](https://cloud.google.com/vpc/docs/firewall-policies)
-- [net-vpc](../../modules/net-vpc/README.md#subnet-factory) for subnets creation
-- [net-vpc-firewall](../../modules/net-vpc-firewall/README.md#rules-factory) for massive rules creation
-
-### Dedicated Factories
-
-- [net-vpc-firewall-yaml](net-vpc-firewall-yaml/README.md) for VPC firewall rules across different projects/VPCs
-- [project-factory](project-factory/README.md) for projects
-
diff --git a/examples/factories/net-vpc-firewall-yaml/README.md b/examples/factories/net-vpc-firewall-yaml/README.md
deleted file mode 100644
index 0cf7af5c82..0000000000
--- a/examples/factories/net-vpc-firewall-yaml/README.md
+++ /dev/null
@@ -1,157 +0,0 @@
-# Google Cloud VPC Firewall Factory
-
-This module allows creation and management of different types of firewall rules by defining them in well formatted `yaml` files.
-
-Yaml abstraction for FW rules can simplify users onboarding and also makes rules definition simpler and clearer comparing to HCL.
-
-Nested folder structure for yaml configurations is optionally supported, which allows better and structured code management for multiple teams and environments.
-
-## Example
-
-### Terraform code
-
-```hcl
-module "prod-firewall" {
- source = "./modules/net-vpc-firewall-yaml"
-
- project_id = "my-prod-project"
- network = "my-prod-network"
- config_directories = [
- "./prod",
- "./common"
- ]
-
- log_config = {
- metadata = "INCLUDE_ALL_METADATA"
- }
-}
-
-module "dev-firewall" {
- source = "./modules/net-vpc-firewall-yaml"
-
- project_id = "my-dev-project"
- network = "my-dev-network"
- config_directories = [
- "./dev",
- "./common"
- ]
-}
-# tftest skip
-```
-
-### Configuration Structure
-
-```bash
-├── common
-│ ├── default-egress.yaml
-│ ├── lb-rules.yaml
-│ └── iap-ingress.yaml
-├── dev
-│ ├── team-a
-│ │ ├── databases.yaml
-│ │ └── webb-app-a.yaml
-│ └── team-b
-│ ├── backend.yaml
-│ └── frontend.yaml
-└── prod
- ├── team-a
- │ ├── databases.yaml
- │ └── webb-app-a.yaml
- └── team-b
- ├── backend.yaml
- └── frontend.yaml
-```
-
-### Rule definition format and structure
-
-Firewall rules configuration should be placed in a set of yaml files in a folder/s. Firewall rule entry structure is following:
-
-```yaml
-rule-name: # descriptive name, naming convention is adjusted by the module
- allow: # `allow` or `deny`
- - ports: ['443', '80'] # ports for a specific protocol, keep empty list `[]` for all ports
- protocol: tcp # protocol, put `all` for any protocol
- direction: EGRESS # EGRESS or INGRESS
- disabled: false # `false` or `true`, FW rule is disabled when `true`, default value is `false`
- priority: 1000 # rule priority value, default value is 1000
- source_ranges: # list of source ranges, should be specified only for `INGRESS` rule
- - 0.0.0.0/0
- destination_ranges: # list of destination ranges, should be specified only for `EGRESS` rule
- - 0.0.0.0/0
- source_tags: ['some-tag'] # list of source tags, should be specified only for `INGRESS` rule
- source_service_accounts: # list of source service accounts, should be specified only for `INGRESS` rule, can not be specified together with `source_tags` or `target_tags`
- - myapp@myproject-id.iam.gserviceaccount.com
- target_tags: ['some-tag'] # list of target tags
- target_service_accounts: # list of target service accounts, , can not be specified together with `source_tags` or `target_tags`
- - myapp@myproject-id.iam.gserviceaccount.com
-```
-
-
-Firewall rules example yaml configuration
-
-```bash
-cat ./prod/core-network/common-rules.yaml
-# allow ingress from GCLB to all instances in the network
-lb-health-checks:
- allow:
- - ports: []
- protocol: tcp
- direction: INGRESS
- priority: 1001
- source_ranges:
- - 35.191.0.0/16
- - 130.211.0.0/22
-
-# deny all egress
-deny-all:
- deny:
- - ports: []
- protocol: all
- direction: EGRESS
- priority: 65535
- destination_ranges:
- - 0.0.0.0/0
-
-cat ./dev/team-a/web-app-a.yaml
-# Myapp egress
-web-app-a-egress:
- allow:
- - ports: [443]
- protocol: tcp
- direction: EGRESS
- destination_ranges:
- - 192.168.0.0/24
- target_service_accounts:
- - myapp@myproject-id.iam.gserviceaccount.com
-# Myapp ingress
-web-app-a-ingress:
- allow:
- - ports: [1234]
- protocol: tcp
- direction: INGRESS
- source_service_accounts:
- - frontend-sa@myproject-id.iam.gserviceaccount.com
- target_service_accounts:
- - web-app-a@myproject-id.iam.gserviceaccount.com
-```
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [config_directories](variables.tf#L17) | List of paths to folders where firewall configs are stored in yaml format. Folder may include subfolders with configuration files. Files suffix must be `.yaml`. | list(string)
| ✓ | |
-| [network](variables.tf#L30) | Name of the network this set of firewall rules applies to. | string
| ✓ | |
-| [project_id](variables.tf#L35) | Project Id. | string
| ✓ | |
-| [log_config](variables.tf#L22) | Log configuration. Possible values for `metadata` are `EXCLUDE_ALL_METADATA` and `INCLUDE_ALL_METADATA`. Set to `null` for disabling firewall logging. | object({…})
| | null
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [egress_allow_rules](outputs.tf#L17) | Egress rules with allow blocks. | |
-| [egress_deny_rules](outputs.tf#L25) | Egress rules with allow blocks. | |
-| [ingress_allow_rules](outputs.tf#L33) | Ingress rules with allow blocks. | |
-| [ingress_deny_rules](outputs.tf#L41) | Ingress rules with deny blocks. | |
-
-
diff --git a/examples/factories/net-vpc-firewall-yaml/versions.tf b/examples/factories/net-vpc-firewall-yaml/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/factories/net-vpc-firewall-yaml/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/factories/project-factory/README.md b/examples/factories/project-factory/README.md
deleted file mode 100644
index 0b8b5183dd..0000000000
--- a/examples/factories/project-factory/README.md
+++ /dev/null
@@ -1,245 +0,0 @@
-# Minimal Project Factory
-
-This module implements a minimal, opinionated project factory (see [Factories](../README.md) for rationale) that allows for the creation of projects.
-
-While the module can be invoked by manually populating the required variables, its interface is meant for the massive creation of resources leveraging a set of well-defined YaML documents, as shown in the examples below.
-
-The Project Factory is meant to be executed by a Service Account (or a regular user) having this minimal set of permissions over your resources:
-
-* **Org level** - a custom role for networking operations including the following permissions
- * `"compute.organizations.enableXpnResource"`,
- * `"compute.organizations.disableXpnResource"`,
- * `"compute.subnetworks.setIamPolicy"`,
- * `"dns.networks.bindPrivateDNSZone"`
- * and role `"roles/orgpolicy.policyAdmin"`
-* **on each folder** where projects will be created
- * `"roles/logging.admin"`
- * `"roles/owner"`
- * `"roles/resourcemanager.folderAdmin"`
- * `"roles/resourcemanager.projectCreator"`
-* **on the host project** for the Shared VPC/s
- * `"roles/browser"`
- * `"roles/compute.viewer"`
- * `"roles/dns.admin"`
-
-## Example
-
-### Directory structure
-
-```
-.
-├── data
-│ ├── defaults.yaml
-│ └── projects
-│ ├── project-example-one.yaml
-│ ├── project-example-two.yaml
-│ └── project-example-three.yaml
-├── main.tf
-└── terraform.tfvars
-
-```
-
-### Terraform code
-
-```tfvars
-# ./terraform.tfvars
-data_dir = "data/projects/"
-defaults_file = "data/defaults.yaml"
-```
-
-```hcl
-# ./main.tf
-
-locals {
- defaults = yamldecode(file(var.defaults_file))
- projects = {
- for f in fileset("${var.data_dir}", "**/*.yaml") :
- trimsuffix(f, ".yaml") => yamldecode(file("${var.data_dir}/${f}"))
- }
-}
-
-module "projects" {
- source = "./factories/project-factory"
- for_each = local.projects
- defaults = local.defaults
- project_id = each.key
- 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, [])
- essential_contacts = try(each.value.essential_contacts, [])
- folder_id = each.value.folder_id
- group_iam = try(each.value.group_iam, {})
- iam = try(each.value.iam, {})
- kms_service_agents = try(each.value.kms, {})
- labels = try(each.value.labels, {})
- org_policies = try(each.value.org_policies, null)
- secrets = try(each.value.secrets, {})
- service_accounts = try(each.value.service_accounts, {})
- services = try(each.value.services, [])
- services_iam = try(each.value.services_iam, {})
- vpc = try(each.value.vpc, null)
-}
-```
-
-### Projects configuration
-
-```yaml
-# ./data/defaults.yaml
-# The following applies as overrideable defaults for all projects
-# All attributes are required
-
-billing_account_id: 012345-67890A-BCDEF0
-billing_alert:
- amount: 1000
- thresholds:
- current: [0.5, 0.8]
- forecasted: [0.5, 0.8]
- credit_treatment: INCLUDE_ALL_CREDITS
-environment_dns_zone: prod.gcp.example.com
-essential_contacts: []
-labels:
- environment: production
- department: legal
- application: my-legal-bot
-notification_channels: []
-shared_vpc_self_link: https://www.googleapis.com/compute/v1/projects/project-example-host-project/global/networks/vpc-one
-vpc_host_project: project-example-host-project
-
-```
-
-```yaml
-# ./data/projects/project-example-one.yaml
-# One file per project - projects will be named after the filename
-
-# [opt] Billing account id - overrides default if set
-billing_account_id: 012345-67890A-BCDEF0
-
-# [opt] Billing alerts config - overrides default if set
-billing_alert:
- amount: 10
- thresholds:
- current:
- - 0.5
- - 0.8
- forecasted: []
-
-# [opt] DNS zones to be created as children of the environment_dns_zone defined in defaults
-dns_zones:
- - lorem
- - ipsum
-
-# [opt] Contacts for billing alerts and important notifications
-essential_contacts:
- - team-a-contacts@example.com
-
-# Folder the project will be created as children of
-folder_id: folders/012345678901
-
-# [opt] Authoritative IAM bindings in group => [roles] format
-group_iam:
- test-team-foobar@fast-lab-0.gcp-pso-italy.net:
- - roles/compute.admin
-
-# [opt] Authoritative IAM bindings in role => [principals] format
-# Generally used to grant roles to service accounts external to the project
-iam:
- roles/compute.admin:
- - serviceAccount:service-account
-
-# [opt] Service robots and keys they will be assigned as cryptoKeyEncrypterDecrypter
-# in service => [keys] format
-kms_service_agents:
- compute: [key1, key2]
- storage: [key1, key2]
-
-# [opt] Labels for the project - merged with the ones defined in defaults
-labels:
- environment: prod
-
-# [opt] Org policy overrides defined at project level
-org_policies:
- policy_boolean:
- constraints/compute.disableGuestAttributesAccess: true
- policy_list:
- constraints/compute.trustedImageProjects:
- inherit_from_parent: null
- status: true
- suggested_value: null
- values:
- - projects/fast-prod-iac-core-0
-
-# [opt] Service account to create for the project and their roles on the project
-# in name => [roles] format
-service_accounts:
- another-service-account:
- - roles/compute.admin
- my-service-account:
- - roles/compute.admin
-
-# [opt] APIs to enable on the project.
-services:
- - storage.googleapis.com
- - stackdriver.googleapis.com
- - compute.googleapis.com
-
-# [opt] Roles to assign to the robots service accounts in robot => [roles] format
-services_iam:
- compute:
- - roles/storage.objectViewer
-
- # [opt] VPC setup.
- # If set enables the `compute.googleapis.com` service and configures
- # service project attachment
-vpc:
-
- # [opt] If set, enables the container API
- gke_setup:
-
- # Grants "roles/container.hostServiceAgentUser" to the container robot if set
- enable_host_service_agent: false
-
- # Grants "roles/compute.securityAdmin" to the container robot if set
- enable_security_admin: true
-
- # Host project the project will be service project of
- host_project: fast-prod-net-spoke-0
-
- # [opt] Subnets in the host project where principals will be granted networkUser
- # in region/subnet-name => [principals]
- subnets_iam:
- europe-west1/prod-default-ew1: []
- - user:foobar@example.com
- - serviceAccount:service-account1
-```
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [billing_account_id](variables.tf#L17) | Billing account id. | string
| ✓ | |
-| [folder_id](variables.tf#L69) | Folder ID for the folder where the project will be created. | string
| ✓ | |
-| [project_id](variables.tf#L118) | 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)
| | []
|
-| [group_iam](variables.tf#L74) | Custom IAM settings in group => [role] format. | map(list(string))
| | {}
|
-| [iam](variables.tf#L80) | Custom IAM settings in role => [principal] format. | map(list(string))
| | {}
|
-| [kms_service_agents](variables.tf#L86) | KMS IAM configuration in as service => [key]. | map(list(string))
| | {}
|
-| [labels](variables.tf#L92) | Labels to be assigned at project level. | map(string)
| | {}
|
-| [org_policies](variables.tf#L98) | Org-policy overrides at project level. | object({…})
| | null
|
-| [prefix](variables.tf#L112) | Prefix used for the project id. | string
| | null
|
-| [service_accounts](variables.tf#L123) | Service accounts to be created, and roles to assign them. | map(list(string))
| | {}
|
-| [service_identities_iam](variables.tf#L136) | Custom IAM settings for service identities in service => [role] format. | map(list(string))
| | {}
|
-| [services](variables.tf#L129) | Services to be enabled for the project. | list(string)
| | []
|
-| [vpc](variables.tf#L143) | VPC configuration for the project. | object({…})
| | null
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [project](outputs.tf#L19) | The project resource as return by the `project` module | |
-| [project_id](outputs.tf#L29) | Project ID. | |
-
-
diff --git a/examples/factories/project-factory/main.tf b/examples/factories/project-factory/main.tf
deleted file mode 100644
index 4efdaeac04..0000000000
--- a/examples/factories/project-factory/main.tf
+++ /dev/null
@@ -1,194 +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 {
- # internal structures for group IAM bindings
- _group_iam = {
- for r in local._group_iam_bindings : r => [
- for k, v in var.group_iam :
- "group:${k}" if try(index(v, r), null) != null
- ]
- }
- _group_iam_bindings = distinct(flatten(values(var.group_iam)))
- # internal structures for project service accounts IAM bindings
- _service_accounts_iam = {
- for r in local._service_accounts_iam_bindings : r => [
- for k, v in var.service_accounts :
- "serviceAccount:${k}@${var.project_id}.iam.gserviceaccount.com"
- if try(index(v, r), null) != null
- ]
- }
- _service_accounts_iam_bindings = distinct(flatten(
- values(var.service_accounts)
- ))
- # internal structures for project services
- _services = concat([
- "billingbudgets.googleapis.com",
- "essentialcontacts.googleapis.com"
- ],
- length(var.dns_zones) > 0 ? ["dns.googleapis.com"] : [],
- try(var.vpc.gke_setup, null) != null ? ["container.googleapis.com"] : [],
- var.vpc != null ? ["compute.googleapis.com"] : [],
- )
- # internal structures for service identity IAM bindings
- _service_identities_roles = distinct(flatten(values(var.service_identities_iam)))
- _service_identities_iam = {
- for role in local._service_identities_roles : role => [
- for service, roles in var.service_identities_iam :
- "serviceAccount:${module.project.service_accounts.robots[service]}"
- if contains(roles, role)
- ]
- }
- # internal structure for Shared VPC service project IAM bindings
- _vpc_subnet_bindings = (
- local.vpc.subnets_iam == null || local.vpc.host_project == null
- ? []
- : flatten([
- for subnet, members in local.vpc.subnets_iam : [
- for member in members : {
- region = split("/", subnet)[0]
- subnet = split("/", subnet)[1]
- member = member
- }
- ]
- ])
- )
- # structures for billing id
- billing_account_id = coalesce(
- var.billing_account_id, try(var.defaults.billing_account_id, "")
- )
- billing_alert = (
- var.billing_alert == null
- ? try(var.defaults.billing_alert, null)
- : var.billing_alert
- )
- # structure for essential contacts
- essential_contacts = concat(
- try(var.defaults.essential_contacts, []), var.essential_contacts
- )
- # structure that combines all authoritative IAM bindings
- iam = {
- for role in distinct(concat(
- keys(var.iam),
- keys(local._group_iam),
- keys(local._service_accounts_iam),
- keys(local._service_identities_iam),
- )) :
- role => concat(
- try(var.iam[role], []),
- try(local._group_iam[role], []),
- try(local._service_accounts_iam[role], []),
- try(local._service_identities_iam[role], []),
- )
- }
- # merge labels with defaults
- labels = merge(
- coalesce(var.labels, {}), coalesce(try(var.defaults.labels, {}), {})
- )
- # deduplicate services
- services = distinct(concat(var.services, local._services))
- # structures for Shared VPC resources in host project
- vpc = coalesce(var.vpc, {
- host_project = null, gke_setup = null, subnets_iam = null
- })
- vpc_cloudservices = (
- local.vpc_gke_service_agent ||
- contains(var.services, "compute.googleapis.com")
- )
- vpc_gke_security_admin = coalesce(
- try(local.vpc.gke_setup.enable_security_admin, null), false
- )
- vpc_gke_service_agent = coalesce(
- try(local.vpc.gke_setup.enable_host_service_agent, null), false
- )
- vpc_subnet_bindings = {
- for binding in local._vpc_subnet_bindings :
- "${binding.subnet}:${binding.member}" => binding
- }
-}
-
-module "billing-alert" {
- for_each = local.billing_alert == null ? {} : { 1 = 1 }
- source = "../../../modules/billing-budget"
- billing_account = local.billing_account_id
- name = "${module.project.project_id} budget"
- amount = local.billing_alert.amount
- thresholds = local.billing_alert.thresholds
- credit_treatment = local.billing_alert.credit_treatment
- notification_channels = var.defaults.notification_channels
- projects = ["projects/${module.project.number}"]
- email_recipients = {
- project_id = module.project.project_id
- emails = local.essential_contacts
- }
-}
-
-module "dns" {
- source = "../../../modules/dns"
- for_each = toset(var.dns_zones)
- project_id = coalesce(local.vpc.host_project, module.project.project_id)
- type = "private"
- name = each.value
- domain = "${each.value}.${var.defaults.environment_dns_zone}"
- client_networks = [var.defaults.shared_vpc_self_link]
-}
-
-module "project" {
- source = "../../../modules/project"
- billing_account = local.billing_account_id
- name = var.project_id
- prefix = var.prefix
- contacts = { for c in local.essential_contacts : c => ["ALL"] }
- iam = local.iam
- labels = local.labels
- parent = var.folder_id
- policy_boolean = try(var.org_policies.policy_boolean, {})
- policy_list = try(var.org_policies.policy_list, {})
- service_encryption_key_ids = var.kms_service_agents
- services = local.services
- shared_vpc_service_config = var.vpc == null ? null : {
- host_project = local.vpc.host_project
- # these are non-authoritative
- service_identity_iam = {
- "roles/compute.networkUser" = compact([
- local.vpc_gke_service_agent ? "container-engine" : null,
- local.vpc_cloudservices ? "cloudservices" : null
- ])
- "roles/compute.securityAdmin" = compact([
- local.vpc_gke_security_admin ? "container-engine" : null,
- ])
- "roles/container.hostServiceAgentUser" = compact([
- local.vpc_gke_service_agent ? "container-engine" : null
- ])
- }
- }
-}
-
-module "service-accounts" {
- source = "../../../modules/iam-service-account"
- for_each = var.service_accounts
- name = each.key
- project_id = module.project.project_id
-}
-
-resource "google_compute_subnetwork_iam_member" "default" {
- for_each = local.vpc_subnet_bindings
- project = local.vpc.host_project
- subnetwork = "projects/${local.vpc.host_project}/regions/${each.value.region}/subnetworks/${each.value.subnet}"
- region = each.value.region
- role = "roles/compute.networkUser"
- member = each.value.member
-}
diff --git a/examples/factories/project-factory/outputs.tf b/examples/factories/project-factory/outputs.tf
deleted file mode 100644
index a60ad457fd..0000000000
--- a/examples/factories/project-factory/outputs.tf
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-# TODO(): proper outputs
-
-output "project" {
- description = "The project resource as return by the `project` module"
- value = module.project
-
- depends_on = [
- google_compute_subnetwork_iam_member.default,
- module.dns
- ]
-}
-
-output "project_id" {
- description = "Project ID."
- value = module.project.project_id
- depends_on = [
- google_compute_subnetwork_iam_member.default,
- module.dns
- ]
-}
diff --git a/examples/factories/project-factory/variables.tf b/examples/factories/project-factory/variables.tf
deleted file mode 100644
index 7f4a20f7d0..0000000000
--- a/examples/factories/project-factory/variables.tf
+++ /dev/null
@@ -1,157 +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_id" {
- description = "Billing account id."
- type = string
-}
-
-variable "billing_alert" {
- description = "Billing alert configuration."
- type = object({
- amount = number
- thresholds = object({
- current = list(number)
- forecasted = list(number)
- })
- credit_treatment = string
- })
- default = null
-}
-
-variable "defaults" {
- description = "Project factory default values."
- type = object({
- billing_account_id = string
- billing_alert = object({
- amount = number
- thresholds = object({
- current = list(number)
- forecasted = list(number)
- })
- credit_treatment = string
- })
- environment_dns_zone = string
- essential_contacts = list(string)
- labels = map(string)
- notification_channels = list(string)
- shared_vpc_self_link = string
- vpc_host_project = string
- })
- default = null
-}
-
-variable "dns_zones" {
- description = "DNS private zones to create as child of var.defaults.environment_dns_zone."
- type = list(string)
- default = []
-}
-
-variable "essential_contacts" {
- description = "Email contacts to be used for billing and GCP notifications."
- type = list(string)
- default = []
-}
-
-variable "folder_id" {
- description = "Folder ID for the folder where the project will be created."
- type = string
-}
-
-variable "group_iam" {
- description = "Custom IAM settings in group => [role] format."
- type = map(list(string))
- default = {}
-}
-
-variable "iam" {
- description = "Custom IAM settings in role => [principal] format."
- type = map(list(string))
- default = {}
-}
-
-variable "kms_service_agents" {
- description = "KMS IAM configuration in as service => [key]."
- type = map(list(string))
- default = {}
-}
-
-variable "labels" {
- description = "Labels to be assigned at project level."
- type = map(string)
- default = {}
-}
-
-variable "org_policies" {
- description = "Org-policy overrides at project level."
- type = object({
- policy_boolean = map(bool)
- policy_list = map(object({
- inherit_from_parent = bool
- suggested_value = string
- status = bool
- values = list(string)
- }))
- })
- default = null
-}
-
-variable "prefix" {
- description = "Prefix used for the project id."
- type = string
- default = null
-}
-
-variable "project_id" {
- description = "Project id."
- type = string
-}
-
-variable "service_accounts" {
- description = "Service accounts to be created, and roles to assign them."
- type = map(list(string))
- default = {}
-}
-
-variable "services" {
- description = "Services to be enabled for the project."
- type = list(string)
- default = []
- nullable = false
-}
-
-variable "service_identities_iam" {
- description = "Custom IAM settings for service identities in service => [role] format."
- type = map(list(string))
- default = {}
- nullable = false
-}
-
-variable "vpc" {
- description = "VPC configuration for the project."
- type = object({
- host_project = string
- gke_setup = object({
- enable_security_admin = bool
- enable_host_service_agent = bool
- })
- subnets_iam = map(list(string))
- })
- default = null
-}
-
-
-
diff --git a/examples/foundations/README.md b/examples/foundations/README.md
deleted file mode 100644
index 097cd486aa..0000000000
--- a/examples/foundations/README.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# Cloud foundation examples
-
-The examples in this folder deal with cloud foundations: the set of resources used to **create the organizational hierarchy** (folders and specific IAM roles), **implement top-level initial best practices** (audit log exports, policies) and **bootstrap infrastructure automation** (GCS buckets, service accounts and IAM roles).
-
-The examples are derived from actual production use cases, and are meant to be used as-is, or extended to create more complex hierarchies. The guiding principles they implement are:
-
-- divide the hierarchy in separate partitions along environment/organization boundaries, to enforce separation of duties and decouple organization admin permissions from the day-to-day running of infrastructure
-- keep top-level Terraform code minimal and encapsulate complexity in modules, to ensure readability and allow using code as high level documentation
-
-## Examples
-
-### Environment Hierarchy
-
- This [example](./environments/) implements a simple one-level oganizational layout, which is commonly used to bootstrap small infrastructures, or in situations where lower level folders are managed with separate, more granular Terraform setups.
-
-One authoritative service account, one bucket and one folder are created for each environment, together with top-level shared resources. This example's simplicity makes it a good starting point to understand and prototype foundational design.
-
-string
| ✓ | |
-| [organization_id](variables.tf#L69) | Organization id in organizations/nnnnnnn format. | string
| ✓ | |
-| [prefix](variables.tf#L74) | Prefix used for resources that need unique names. | string
| ✓ | |
-| [root_node](variables.tf#L88) | Root node for the new hierarchy, either 'organizations/org_id' or 'folders/folder_id'. | string
| ✓ | |
-| [audit_filter](variables.tf#L17) | Audit log filter used for the log sink. | string
| | …
|
-| [environments](variables.tf#L32) | Environment short names. | map(string)
| | {…}
|
-| [gcs_defaults](variables.tf#L42) | Defaults use for the state GCS buckets. | map(string)
| | {…}
|
-| [iam_audit_viewers](variables.tf#L51) | Audit project viewers, in IAM format. | list(string)
| | []
|
-| [iam_shared_owners](variables.tf#L57) | Shared services project owners, in IAM format. | list(string)
| | []
|
-| [iam_terraform_owners](variables.tf#L63) | Terraform project owners, in IAM format. | list(string)
| | []
|
-| [project_services](variables.tf#L79) | Service APIs enabled by default in new projects. | list(string)
| | […]
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [audit_logs_project](outputs.tf#L17) | Project that holds the audit logs export resources. | |
-| [bootstrap_tf_gcs_bucket](outputs.tf#L22) | GCS bucket used for the bootstrap Terraform state. | |
-| [bu_business_intelligence](outputs.tf#L27) | Business Intelligence attributes. | |
-| [bu_business_intelligence_keys](outputs.tf#L37) | Business Intelligence service account keys. | ✓ |
-| [bu_machine_learning](outputs.tf#L43) | Machine Learning attributes. | |
-| [bu_machine_learning_keys](outputs.tf#L53) | Machine Learning service account keys. | ✓ |
-| [shared_folder_id](outputs.tf#L59) | Shared folder id. | |
-| [shared_resources_project](outputs.tf#L64) | Project that holdes resources shared across business units. | |
-| [terraform_project](outputs.tf#L69) | Project that holds the base Terraform resources. | |
-
-
diff --git a/examples/foundations/business-units/backend.tf.sample b/examples/foundations/business-units/backend.tf.sample
deleted file mode 100644
index 065cd071e0..0000000000
--- a/examples/foundations/business-units/backend.tf.sample
+++ /dev/null
@@ -1,22 +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
-#
-# 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 {
- backend "gcs" {
- # once initial apply has completed, copy this file to `backend.tf` then
- # set the `bucket` value to the `bootstrap_tf_gcs_bucket` output, then
- # run apply again to transfer state
- bucket = ""
- }
-}
diff --git a/examples/foundations/business-units/diagram.png b/examples/foundations/business-units/diagram.png
deleted file mode 100644
index 5692b1d4ac..0000000000
Binary files a/examples/foundations/business-units/diagram.png and /dev/null differ
diff --git a/examples/foundations/business-units/main.tf b/examples/foundations/business-units/main.tf
deleted file mode 100644
index 0e1535bb30..0000000000
--- a/examples/foundations/business-units/main.tf
+++ /dev/null
@@ -1,175 +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 {
- logging_sinks = {
- audit-logs = {
- type = "bigquery"
- destination = module.audit-dataset.id
- filter = var.audit_filter
- iam = true
- include_children = true
- }
- }
- root_node_type = split("/", var.root_node)[0]
-}
-
-###############################################################################
-# Terraform top-level resources #
-###############################################################################
-
-# Shared folder
-
-module "shared-folder" {
- source = "../../../modules/folder"
- parent = var.root_node
- name = "shared"
-}
-
-# Terraform project
-
-module "tf-project" {
- source = "../../../modules/project"
- name = "terraform"
- parent = module.shared-folder.id
- prefix = var.prefix
- billing_account = var.billing_account_id
- iam_additive = {
- for name in var.iam_terraform_owners : (name) => ["roles/owner"]
- }
- services = var.project_services
-}
-
-# Bootstrap Terraform state GCS bucket
-
-module "tf-gcs-bootstrap" {
- source = "../../../modules/gcs"
- project_id = module.tf-project.project_id
- name = "tf-bootstrap"
- prefix = "${var.prefix}-tf"
- location = var.gcs_defaults.location
-}
-
-###############################################################################
-# Business units #
-###############################################################################
-
-module "bu-business-intelligence" {
- source = "../../../modules/folders-unit"
- name = "Business Intelligence"
- short_name = "bi"
- automation_project_id = module.tf-project.project_id
- billing_account_id = var.billing_account_id
- environments = var.environments
- gcs_defaults = var.gcs_defaults
- organization_id = var.organization_id
- root_node = var.root_node
- prefix = var.prefix
- # extra variables from the folders-unit module can be used here to grant
- # IAM roles to the bu users, configure the automation service accounts, etc.
- # iam_roles = ["viewer"]
- # iam_members = { viewer = ["user:user@example.com"] }
-}
-
-module "bu-machine-learning" {
- source = "../../../modules/folders-unit"
- name = "Machine Learning"
- short_name = "ml"
- automation_project_id = module.tf-project.project_id
- billing_account_id = var.billing_account_id
- environments = var.environments
- gcs_defaults = var.gcs_defaults
- organization_id = var.organization_id
- root_node = var.root_node
- prefix = var.prefix
- # extra variables from the folders-unit module can be used here to grant
- # IAM roles to the bu users, configure the automation service accounts, etc.
-}
-
-###############################################################################
-# Audit log exports #
-###############################################################################
-
-# Audit logs project
-
-module "audit-project" {
- source = "../../../modules/project"
- name = "audit"
- parent = module.shared-folder.id
- prefix = var.prefix
- billing_account = var.billing_account_id
- iam = {
- "roles/viewer" = var.iam_audit_viewers
- }
- services = concat(var.project_services, [
- "bigquery.googleapis.com",
- ])
-}
-
-# audit logs dataset and sink
-
-module "audit-dataset" {
- source = "../../../modules/bigquery-dataset"
- project_id = module.audit-project.project_id
- id = "audit_export"
- friendly_name = "Audit logs export."
- # disable delete on destroy for actual use
- options = {
- default_table_expiration_ms = null
- default_partition_expiration_ms = null
- delete_contents_on_destroy = true
- }
-}
-
-# uncomment the next two modules to create the logging sinks
-
-# module "root_org" {
-# count = local.root_node_type == "organizations" ? 1 : 0
-# source = "../../../modules/organization"
-# organization_id = var.root_node
-# logging_sinks = local.logging_sinks
-# exclusions = {}
-# }
-
-# module "root_folder" {
-# count = local.root_node_type == "folders" ? 1 : 0
-# source = "../../../modules/folder"
-# id = var.root_node
-# folder_create = false
-# logging_sinks = local.logging_sinks
-# exclusions = {}
-# }
-
-###############################################################################
-# Shared resources (GCR, GCS, KMS, etc.) #
-###############################################################################
-
-# Shared resources project
-
-module "shared-project" {
- source = "../../../modules/project"
- name = "shared"
- parent = module.shared-folder.id
- prefix = var.prefix
- billing_account = var.billing_account_id
- iam_additive = {
- for name in var.iam_shared_owners : (name) => ["roles/owner"]
- }
- services = var.project_services
-}
-
-# Add further modules here for resources that are common to all business units
-# like GCS buckets (used to hold shared assets), Container Registry, KMS, etc.
diff --git a/examples/foundations/business-units/outputs.tf b/examples/foundations/business-units/outputs.tf
deleted file mode 100644
index 4456123a07..0000000000
--- a/examples/foundations/business-units/outputs.tf
+++ /dev/null
@@ -1,75 +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 "audit_logs_project" {
- description = "Project that holds the audit logs export resources."
- value = module.audit-project.project_id
-}
-
-output "bootstrap_tf_gcs_bucket" {
- description = "GCS bucket used for the bootstrap Terraform state."
- value = module.tf-gcs-bootstrap.name
-}
-
-output "bu_business_intelligence" {
- description = "Business Intelligence attributes."
- value = {
- unit_folder = module.bu-business-intelligence.unit_folder,
- env_gcs_buckets = module.bu-business-intelligence.env_gcs_buckets
- env_folders = module.bu-business-intelligence.env_folders
- env_service_accounts = module.bu-business-intelligence.env_service_accounts
- }
-}
-
-output "bu_business_intelligence_keys" {
- description = "Business Intelligence service account keys."
- sensitive = true
- value = module.bu-business-intelligence.env_sa_keys
-}
-
-output "bu_machine_learning" {
- description = "Machine Learning attributes."
- value = {
- unit_folder = module.bu-machine-learning.unit_folder,
- env_gcs_buckets = module.bu-machine-learning.env_gcs_buckets
- env_folders = module.bu-machine-learning.env_folders
- env_service_accounts = module.bu-machine-learning.env_service_accounts
- }
-}
-
-output "bu_machine_learning_keys" {
- description = "Machine Learning service account keys."
- sensitive = true
- value = module.bu-machine-learning.env_sa_keys
-}
-
-output "shared_folder_id" {
- description = "Shared folder id."
- value = module.shared-folder.id
-}
-
-output "shared_resources_project" {
- description = "Project that holdes resources shared across business units."
- value = module.shared-project.project_id
-}
-
-output "terraform_project" {
- description = "Project that holds the base Terraform resources."
- value = module.tf-project.project_id
-}
-
-# Add further outputs here for the additional modules that manage shared
-# resources, like GCR, GCS buckets, KMS, etc.
diff --git a/examples/foundations/business-units/terraform.tfvars.sample b/examples/foundations/business-units/terraform.tfvars.sample
deleted file mode 100644
index 7036b9697d..0000000000
--- a/examples/foundations/business-units/terraform.tfvars.sample
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright 2022 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# 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.
-
-billing_account_id = "014617-19UCBC-AF02D9"
-organization_id= "500001140800"
-prefix = "xyz"
-root_node = "folders/9572793983696"
-generate_keys = true
diff --git a/examples/foundations/business-units/variables.tf b/examples/foundations/business-units/variables.tf
deleted file mode 100644
index 69f5ac3b22..0000000000
--- a/examples/foundations/business-units/variables.tf
+++ /dev/null
@@ -1,91 +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 "audit_filter" {
- description = "Audit log filter used for the log sink."
- type = string
- default = <string
| ✓ | |
-| [environments](variables.tf#L30) | Environment short names. | set(string)
| ✓ | |
-| [organization_id](variables.tf#L94) | Organization id in organizations/nnnnnnnn format. | string
| ✓ | |
-| [prefix](variables.tf#L99) | Prefix used for resources that need unique names. | string
| ✓ | |
-| [root_node](variables.tf#L113) | Root node for the new hierarchy, either 'organizations/org_id' or 'folders/folder_id'. | string
| ✓ | |
-| [audit_filter](variables.tf#L15) | Audit log filter used for the log sink. | string
| | …
|
-| [gcs_location](variables.tf#L35) | GCS bucket location. | string
| | "EU"
|
-| [iam_audit_viewers](variables.tf#L41) | Audit project viewers, in IAM format. | list(string)
| | []
|
-| [iam_billing_config](variables.tf#L47) | Control granting billing user role to service accounts. Target the billing account by default. | object({…})
| | {…}
|
-| [iam_folder_roles](variables.tf#L59) | List of roles granted to each service account on its respective folder (excluding XPN roles). | list(string)
| | […]
|
-| [iam_shared_owners](variables.tf#L70) | Shared services project owners, in IAM format. | list(string)
| | []
|
-| [iam_terraform_owners](variables.tf#L76) | Terraform project owners, in IAM format. | list(string)
| | []
|
-| [iam_xpn_config](variables.tf#L82) | Control granting Shared VPC creation roles to service accounts. Target the root node by default. | object({…})
| | {…}
|
-| [project_services](variables.tf#L104) | Service APIs enabled by default in new projects. | list(string)
| | […]
|
-| [service_account_keys](variables.tf#L118) | Generate and store service account keys in the state file. | bool
| | true
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [audit_logs_bq_dataset](outputs.tf#L15) | Bigquery dataset for the audit logs export. | |
-| [audit_logs_project](outputs.tf#L20) | Project that holds the audit logs export resources. | |
-| [bootstrap_tf_gcs_bucket](outputs.tf#L25) | GCS bucket used for the bootstrap Terraform state. | |
-| [environment_folders](outputs.tf#L30) | Top-level environment folders. | |
-| [environment_service_account_keys](outputs.tf#L35) | Service account keys used to run each environment Terraform modules. | ✓ |
-| [environment_service_accounts](outputs.tf#L40) | Service accounts used to run each environment Terraform modules. | |
-| [environment_tf_gcs_buckets](outputs.tf#L45) | GCS buckets used for each environment Terraform state. | |
-| [shared_services_project](outputs.tf#L50) | Project that holdes resources shared across environments. | |
-| [terraform_project](outputs.tf#L55) | Project that holds the base Terraform resources. | |
-
-
diff --git a/examples/foundations/environments/backend.tf.sample b/examples/foundations/environments/backend.tf.sample
deleted file mode 100644
index 4232a96eb9..0000000000
--- a/examples/foundations/environments/backend.tf.sample
+++ /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
-#
-# 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 {
- backend "gcs" {
- # once initial apply has completed, copy this file to `backend.tf` then
- # set the `bucket` value to the `bootstrap_tf_gcs_bucket` output, then
- # run apply again to transfer state
- bucket = ""
- }
-}
diff --git a/examples/foundations/environments/diagram.png b/examples/foundations/environments/diagram.png
deleted file mode 100644
index ac6d79364a..0000000000
Binary files a/examples/foundations/environments/diagram.png and /dev/null differ
diff --git a/examples/foundations/environments/locals.tf b/examples/foundations/environments/locals.tf
deleted file mode 100644
index f6c9ae0bac..0000000000
--- a/examples/foundations/environments/locals.tf
+++ /dev/null
@@ -1,50 +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 {
- folder_roles = concat(var.iam_folder_roles, local.sa_xpn_folder_role)
- organization_id = element(split("/", var.organization_id), 1)
- sa_billing_account_role = (
- var.iam_billing_config.target_org ? [] : ["roles/billing.user"]
- )
- sa_billing_org_role = (
- !var.iam_billing_config.target_org ? [] : ["roles/billing.user"]
- )
- sa_xpn_folder_role = (
- local.sa_xpn_target_org ? [] : ["roles/compute.xpnAdmin"]
- )
- sa_xpn_org_roles = (
- local.sa_xpn_target_org
- ? ["roles/compute.xpnAdmin", "roles/resourcemanager.organizationViewer"]
- : ["roles/resourcemanager.organizationViewer"]
- )
- sa_xpn_target_org = (
- var.iam_xpn_config.target_org
- ||
- substr(var.root_node, 0, 13) == "organizations"
- )
- logging_sinks = {
- audit-logs = {
- type = "bigquery"
- destination = module.audit-dataset.id
- filter = var.audit_filter
- iam = true
- include_children = true
- exclusions = {}
- }
- }
- root_node_type = split("/", var.root_node)[0]
-}
diff --git a/examples/foundations/environments/main.tf b/examples/foundations/environments/main.tf
deleted file mode 100644
index 00b623259b..0000000000
--- a/examples/foundations/environments/main.tf
+++ /dev/null
@@ -1,168 +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
-#
-# 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 top-level resources #
-###############################################################################
-
-# Terraform project
-
-module "tf-project" {
- source = "../../../modules/project"
- name = "terraform"
- parent = var.root_node
- prefix = var.prefix
- billing_account = var.billing_account_id
- iam_additive = {
- "roles/owner" = var.iam_terraform_owners
- }
- services = var.project_services
-}
-
-# per-environment service accounts
-
-module "tf-service-accounts" {
- source = "../../../modules/iam-service-account"
- for_each = var.environments
- project_id = module.tf-project.project_id
- name = each.value
- prefix = var.prefix
- iam_billing_roles = {
- (var.billing_account_id) = (
- var.iam_billing_config.grant ? local.sa_billing_account_role : []
- )
- }
- # folder roles are set in the folders module using authoritative bindings
- iam_organization_roles = {
- (local.organization_id) = concat(
- var.iam_billing_config.grant ? local.sa_billing_org_role : [],
- var.iam_xpn_config.grant ? local.sa_xpn_org_roles : []
- )
- }
- generate_key = var.service_account_keys
-}
-
-# bootstrap Terraform state GCS bucket
-
-module "tf-gcs-bootstrap" {
- source = "../../../modules/gcs"
- project_id = module.tf-project.project_id
- name = "tf-bootstrap"
- prefix = "${var.prefix}-tf"
- location = var.gcs_location
-}
-
-# per-environment Terraform state GCS buckets
-
-module "tf-gcs-environments" {
- source = "../../../modules/gcs"
- for_each = var.environments
- project_id = module.tf-project.project_id
- name = each.value
- prefix = "${var.prefix}-tf"
- location = var.gcs_location
- iam = {
- "roles/storage.objectAdmin" = [module.tf-service-accounts[each.value].iam_email]
- }
-}
-
-###############################################################################
-# Top-level folders #
-###############################################################################
-
-module "environment-folders" {
- source = "../../../modules/folder"
- for_each = var.environments
- parent = var.root_node
- name = each.value
- iam = {
- for role in local.folder_roles :
- (role) => [module.tf-service-accounts[each.value].iam_email]
- }
-}
-
-###############################################################################
-# Audit log exports #
-###############################################################################
-
-# audit logs project
-
-module "audit-project" {
- source = "../../../modules/project"
- name = "audit"
- parent = var.root_node
- prefix = var.prefix
- billing_account = var.billing_account_id
- iam = {
- "roles/viewer" = var.iam_audit_viewers
- }
- services = concat(var.project_services, [
- "bigquery.googleapis.com",
- ])
-}
-
-# audit logs dataset and sink
-
-module "audit-dataset" {
- source = "../../../modules/bigquery-dataset"
- project_id = module.audit-project.project_id
- id = "audit_export"
- friendly_name = "Audit logs export."
- # disable delete on destroy for actual use
- options = {
- default_table_expiration_ms = null
- default_partition_expiration_ms = null
- delete_contents_on_destroy = true
- }
-}
-
-# uncomment the next two modules to create the logging sinks
-
-# module "root_org" {
-# count = local.root_node_type == "organizations" ? 1 : 0
-# source = "../../../modules/organization"
-# organization_id = var.root_node
-# logging_sinks = local.logging_sinks
-# }
-
-# module "root_folder" {
-# count = local.root_node_type == "folders" ? 1 : 0
-# source = "../../../modules/folder"
-# id = var.root_node
-# folder_create = false
-# logging_sinks = local.logging_sinks
-# }
-
-
-###############################################################################
-# Shared resources (GCR, GCS, KMS, etc.) #
-###############################################################################
-
-# shared resources project
-# see the README file for additional options on managing shared services
-
-module "sharedsvc-project" {
- source = "../../../modules/project"
- name = "sharedsvc"
- parent = var.root_node
- prefix = var.prefix
- billing_account = var.billing_account_id
- iam_additive = {
- "roles/owner" = var.iam_shared_owners
- }
- services = var.project_services
-}
-
-# Add further modules here for resources that are common to all environments
-# like GCS buckets (used to hold shared assets), Container Registry, KMS, etc.
diff --git a/examples/foundations/environments/outputs.tf b/examples/foundations/environments/outputs.tf
deleted file mode 100644
index 4d5f9d4c25..0000000000
--- a/examples/foundations/environments/outputs.tf
+++ /dev/null
@@ -1,61 +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
-#
-# 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 "audit_logs_bq_dataset" {
- description = "Bigquery dataset for the audit logs export."
- value = module.audit-dataset.id
-}
-
-output "audit_logs_project" {
- description = "Project that holds the audit logs export resources."
- value = module.audit-project.project_id
-}
-
-output "bootstrap_tf_gcs_bucket" {
- description = "GCS bucket used for the bootstrap Terraform state."
- value = module.tf-gcs-bootstrap.name
-}
-
-output "environment_folders" {
- description = "Top-level environment folders."
- value = { for folder in module.environment-folders : folder.name => folder.id }
-}
-
-output "environment_service_account_keys" {
- description = "Service account keys used to run each environment Terraform modules."
- sensitive = true
- value = { for env, sa in module.tf-service-accounts : env => sa.key }
-}
-output "environment_service_accounts" {
- description = "Service accounts used to run each environment Terraform modules."
- value = { for env, sa in module.tf-service-accounts : env => sa.email }
-}
-
-output "environment_tf_gcs_buckets" {
- description = "GCS buckets used for each environment Terraform state."
- value = { for env, bucket in module.tf-gcs-environments : env => bucket.name }
-}
-
-output "shared_services_project" {
- description = "Project that holdes resources shared across environments."
- value = module.sharedsvc-project.project_id
-}
-
-output "terraform_project" {
- description = "Project that holds the base Terraform resources."
- value = module.tf-project.project_id
-}
-
-# Add further outputs here for the additional modules that manage shared
-# resources, like GCR, GCS buckets, KMS, etc.
diff --git a/examples/foundations/environments/variables.tf b/examples/foundations/environments/variables.tf
deleted file mode 100644
index 3b38a8ca2a..0000000000
--- a/examples/foundations/environments/variables.tf
+++ /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
-#
-# 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.
-
-variable "audit_filter" {
- description = "Audit log filter used for the log sink."
- type = string
- default = <string
| ✓ | |
-| [prefix](variables.tf#L29) | Prefix used for resources that need unique names. | string
| ✓ | |
-| [root_node](variables.tf#L50) | Hierarchy node where projects will be created, 'organizations/org_id' or 'folders/folder_id'. | string
| ✓ | |
-| [ip_ranges](variables.tf#L20) | Subnet IP CIDR ranges. | map(string)
| | {…}
|
-| [project_services](variables.tf#L34) | Service APIs enabled by default in new projects. | list(string)
| | […]
|
-| [region](variables.tf#L44) | Region used. | string
| | "europe-west1"
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [fw_rules](outputs.tf#L15) | Firewall rules. | |
-| [projects](outputs.tf#L33) | Project ids. | |
-| [vpc](outputs.tf#L41) | Shared VPCs. | |
-
-
diff --git a/examples/networking/decentralized-firewall/main.tf b/examples/networking/decentralized-firewall/main.tf
deleted file mode 100644
index fbc5d9736a..0000000000
--- a/examples/networking/decentralized-firewall/main.tf
+++ /dev/null
@@ -1,136 +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
-#
-# 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.
-
-###############################################################################
-# Shared VPC Host projects #
-###############################################################################
-
-module "project-host-prod" {
- source = "../../../modules/project"
- parent = var.root_node
- billing_account = var.billing_account_id
- prefix = var.prefix
- name = "prod-host"
- services = var.project_services
-
- shared_vpc_host_config = {
- enabled = true
- service_projects = []
- }
-}
-
-module "project-host-dev" {
- source = "../../../modules/project"
- parent = var.root_node
- billing_account = var.billing_account_id
- prefix = var.prefix
- name = "dev-host"
- services = var.project_services
-
- shared_vpc_host_config = {
- enabled = true
- service_projects = []
- }
-}
-
-################################################################################
-# Networking #
-################################################################################
-
-module "vpc-prod" {
- source = "../../../modules/net-vpc"
- project_id = module.project-host-prod.project_id
- name = "prod-vpc"
- subnets = [
- {
- ip_cidr_range = var.ip_ranges.prod
- name = "prod"
- region = var.region
- secondary_ip_range = {}
- }
- ]
-}
-
-module "vpc-dev" {
- source = "../../../modules/net-vpc"
- project_id = module.project-host-dev.project_id
- name = "dev-vpc"
- subnets = [
- {
- ip_cidr_range = var.ip_ranges.dev
- name = "dev"
- region = var.region
- secondary_ip_range = {}
- }
- ]
-}
-
-###############################################################################
-# Private Google Access DNS #
-###############################################################################
-
-module "dns-api-prod" {
- source = "../../../modules/dns"
- project_id = module.project-host-prod.project_id
- type = "private"
- name = "googleapis"
- domain = "googleapis.com."
- client_networks = [module.vpc-prod.self_link]
- recordsets = {
- "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
- }
-}
-
-module "dns-api-dev" {
- source = "../../../modules/dns"
- project_id = module.project-host-dev.project_id
- type = "private"
- name = "googleapis"
- domain = "googleapis.com."
- client_networks = [module.vpc-dev.self_link]
- recordsets = {
- "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
- }
-}
-
-###############################################################################
-# Distributed Firewall #
-###############################################################################
-
-module "vpc-firewall-prod" {
- source = "../../factories/net-vpc-firewall-yaml"
-
- project_id = module.project-host-prod.project_id
- network = module.vpc-prod.name
- config_directories = [
- "${path.module}/firewall/common",
- "${path.module}/firewall/prod"
- ]
-
- # Enable Firewall Logging for the production fwl rules
- log_config = {
- metadata = "INCLUDE_ALL_METADATA"
- }
-}
-
-module "vpc-firewall-dev" {
- source = "../../factories/net-vpc-firewall-yaml"
-
- project_id = module.project-host-dev.project_id
- network = module.vpc-dev.name
- config_directories = [
- "${path.module}/firewall/common",
- "${path.module}/firewall/dev"
- ]
-}
diff --git a/examples/networking/decentralized-firewall/variables.tf b/examples/networking/decentralized-firewall/variables.tf
deleted file mode 100644
index 76a3e1cd4f..0000000000
--- a/examples/networking/decentralized-firewall/variables.tf
+++ /dev/null
@@ -1,53 +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
-#
-# 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.
-
-variable "billing_account_id" {
- description = "Billing account id used as default for new projects."
- type = string
-}
-
-variable "ip_ranges" {
- description = "Subnet IP CIDR ranges."
- type = map(string)
- default = {
- prod = "10.0.16.0/24"
- dev = "10.0.32.0/24"
- }
-}
-
-variable "prefix" {
- description = "Prefix used for resources that need unique names."
- type = string
-}
-
-variable "project_services" {
- description = "Service APIs enabled by default in new projects."
- type = list(string)
- default = [
- "container.googleapis.com",
- "dns.googleapis.com",
- "stackdriver.googleapis.com",
- ]
-}
-
-variable "region" {
- description = "Region used."
- type = string
- default = "europe-west1"
-}
-
-variable "root_node" {
- description = "Hierarchy node where projects will be created, 'organizations/org_id' or 'folders/folder_id'."
- type = string
-}
diff --git a/examples/networking/decentralized-firewall/versions.tf b/examples/networking/decentralized-firewall/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/networking/decentralized-firewall/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/networking/filtering-proxy/README.md b/examples/networking/filtering-proxy/README.md
deleted file mode 100644
index f08293ffbf..0000000000
--- a/examples/networking/filtering-proxy/README.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Network filtering with Squid
-
-This example shows how to deploy a filtering HTTP proxy to restrict Internet access. Here we show one way to do this using a VPC with two subnets:
-
-- The `apps` subnet hosts the VMs that will have their Internet access tightly controlled by a non-caching filtering forward proxy.
-- The `proxy` subnet hosts a Cloud NAT instance and a [Squid](http://www.squid-cache.org/) server.
-
-The VPC is a Shared VPC and all the service projects will be located under a folder enforcing the `compute.vmExternalIpAccess` [organization policy](https://cloud.google.com/resource-manager/docs/organization-policy/org-policy-constraints). This prevents the service projects from having external IPs, thus forcing all outbound Internet connections through the proxy.
-
-To allow Internet connectivity to the proxy subnet, a Cloud NAT instance is configured to allow usage from [that subnet only](https://cloud.google.com/nat/docs/using-nat#specify_subnet_ranges_for_nat). All other subnets are not allowed to use the Cloud NAT instance.
-
-To simplify the usage of the proxy, a Cloud DNS private zone is created and the IP address of the proxy is exposed with the FQDN `proxy.internal`.
-
-You can optionally deploy the Squid server as [Managed Instance Group](https://cloud.google.com/compute/docs/instance-groups) by setting the `mig` option to `true`. This option defaults to `false` which results in a standalone VM.
-
-![High-level diagram](squid.png "High-level diagram")
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [billing_account](variables.tf#L26) | Billing account id used as default for new projects. | string
| ✓ | |
-| [prefix](variables.tf#L52) | Prefix used for resources that need unique names. | string
| ✓ | |
-| [root_node](variables.tf#L63) | Root node for the new hierarchy, either 'organizations/org_id' or 'folders/folder_id'. | string
| ✓ | |
-| [allowed_domains](variables.tf#L17) | List of domains allowed by the squid proxy. | list(string)
| | […]
|
-| [cidrs](variables.tf#L31) | CIDR ranges for subnets. | map(string)
| | {…}
|
-| [mig](variables.tf#L40) | Enables the creation of an autoscaling managed instance group of squid instances. | bool
| | false
|
-| [nat_logging](variables.tf#L46) | Enables Cloud NAT logging if not null, value is one of 'ERRORS_ONLY', 'TRANSLATIONS_ONLY', 'ALL'. | string
| | "ERRORS_ONLY"
|
-| [region](variables.tf#L57) | Default region for resources. | string
| | "europe-west1"
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [squid-address](outputs.tf#L17) | IP address of the Squid proxy. | |
-
-
diff --git a/examples/networking/filtering-proxy/main.tf b/examples/networking/filtering-proxy/main.tf
deleted file mode 100644
index 440a4b5997..0000000000
--- a/examples/networking/filtering-proxy/main.tf
+++ /dev/null
@@ -1,281 +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 {
- squid_address = (
- var.mig
- ? module.squid-ilb.0.forwarding_rule_address
- : module.squid-vm.internal_ip
- )
-}
-
-###############################################################################
-# Folder with network-related resources #
-###############################################################################
-
-module "folder-netops" {
- source = "../../../modules/folder"
- parent = var.root_node
- name = "netops"
-}
-
-###############################################################################
-# Host project and shared VPC resources #
-###############################################################################
-
-module "project-host" {
- source = "../../../modules/project"
- billing_account = var.billing_account
- name = "host"
- parent = module.folder-netops.id
- prefix = var.prefix
- services = [
- "compute.googleapis.com",
- "dns.googleapis.com",
- "logging.googleapis.com"
- ]
- shared_vpc_host_config = {
- enabled = true
- service_projects = []
- }
-}
-
-module "vpc" {
- source = "../../../modules/net-vpc"
- project_id = module.project-host.project_id
- name = "vpc"
- subnets = [
- {
- name = "apps"
- ip_cidr_range = var.cidrs.apps
- region = var.region
- secondary_ip_range = null
- },
- {
- name = "proxy"
- ip_cidr_range = var.cidrs.proxy
- region = var.region
- secondary_ip_range = null
- }
- ]
-}
-
-module "firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.project-host.project_id
- network = module.vpc.name
- custom_rules = {
- allow-ingress-squid = {
- description = "Allow squid ingress traffic"
- direction = "INGRESS"
- action = "allow"
- sources = []
- ranges = [var.cidrs.apps, "35.191.0.0/16", "130.211.0.0/22"]
- targets = [module.service-account-squid.email]
- use_service_accounts = true
- rules = [{ protocol = "tcp", ports = [3128] }]
- extra_attributes = {}
- }
- }
-}
-
-module "nat" {
- source = "../../../modules/net-cloudnat"
- project_id = module.project-host.project_id
- region = var.region
- name = "default"
- router_network = module.vpc.name
- config_source_subnets = "LIST_OF_SUBNETWORKS"
- # 64512/11 = 5864 . 11 is the number of usable IPs in the proxy subnet
- config_min_ports_per_vm = 5864
- subnetworks = [
- {
- self_link = module.vpc.subnet_self_links["${var.region}/proxy"]
- config_source_ranges = ["ALL_IP_RANGES"]
- secondary_ranges = null
- }
- ]
- logging_filter = var.nat_logging
-}
-
-module "private-dns" {
- source = "../../../modules/dns"
- project_id = module.project-host.project_id
- type = "private"
- name = "internal"
- domain = "internal."
- client_networks = [module.vpc.self_link]
- recordsets = {
- "A squid" = { ttl = 60, records = [local.squid_address] }
- "CNAME proxy" = { ttl = 3600, records = ["squid.internal."] }
- }
-}
-
-###############################################################################
-# Squid resources #
-###############################################################################
-
-module "service-account-squid" {
- source = "../../../modules/iam-service-account"
- project_id = module.project-host.project_id
- name = "svc-squid"
- iam_project_roles = {
- (module.project-host.project_id) = [
- "roles/logging.logWriter",
- "roles/monitoring.metricWriter",
- ]
- }
-}
-
-module "cos-squid" {
- source = "../../../modules/cloud-config-container/squid"
- allow = var.allowed_domains
- clients = [var.cidrs.apps]
-}
-
-module "squid-vm" {
- source = "../../../modules/compute-vm"
- project_id = module.project-host.project_id
- zone = "${var.region}-b"
- name = "squid-vm"
- instance_type = "e2-medium"
- create_template = var.mig
- network_interfaces = [{
- network = module.vpc.self_link
- subnetwork = module.vpc.subnet_self_links["${var.region}/proxy"]
- nat = false
- addresses = null
- }]
- boot_disk = {
- image = "cos-cloud/cos-stable"
- type = "pd-standard"
- size = 10
- }
- service_account = module.service-account-squid.email
- service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
- metadata = {
- user-data = module.cos-squid.cloud_config
- }
-}
-
-module "squid-mig" {
- count = var.mig ? 1 : 0
- source = "../../../modules/compute-mig"
- project_id = module.project-host.project_id
- location = "${var.region}-b"
- name = "squid-mig"
- target_size = 1
- autoscaler_config = {
- max_replicas = 10
- min_replicas = 1
- cooldown_period = 30
- cpu_utilization_target = 0.65
- load_balancing_utilization_target = null
- metric = null
- }
- default_version = {
- instance_template = module.squid-vm.template.self_link
- name = "default"
- }
- health_check_config = {
- type = "tcp"
- check = { port = 3128 }
- config = {}
- logging = true
- }
- auto_healing_policies = {
- health_check = module.squid-mig.0.health_check.self_link
- initial_delay_sec = 60
- }
-}
-
-module "squid-ilb" {
- count = var.mig ? 1 : 0
- source = "../../../modules/net-ilb"
- project_id = module.project-host.project_id
- region = var.region
- name = "squid-ilb"
- service_label = "squid-ilb"
- network = module.vpc.self_link
- subnetwork = module.vpc.subnet_self_links["${var.region}/proxy"]
- ports = [3128]
- backends = [{
- failover = false
- group = module.squid-mig.0.group_manager.instance_group
- balancing_mode = "CONNECTION"
- }]
- health_check_config = {
- type = "tcp"
- check = { port = 3128 }
- config = {}
- logging = true
- }
-}
-
-###############################################################################
-# Service project #
-###############################################################################
-
-module "folder-apps" {
- source = "../../../modules/folder"
- parent = var.root_node
- name = "apps"
- policy_list = {
- # prevent VMs with public IPs in the apps folder
- "constraints/compute.vmExternalIpAccess" = {
- inherit_from_parent = false
- suggested_value = null
- status = false
- values = []
- }
- }
-}
-
-module "project-app" {
- source = "../../../modules/project"
- billing_account = var.billing_account
- name = "app1"
- parent = module.folder-apps.id
- prefix = var.prefix
- services = ["compute.googleapis.com"]
- shared_vpc_service_config = {
- host_project = module.project-host.project_id
- service_identity_iam = {
- "roles/compute.networkUser" = ["cloudservices"]
- }
- }
-}
-
-module "test-vm" {
- source = "../../../modules/compute-vm"
- project_id = module.project-app.project_id
- zone = "${var.region}-b"
- name = "test-vm"
- instance_type = "e2-micro"
- tags = ["ssh"]
- network_interfaces = [{
- network = module.vpc.self_link
- subnetwork = module.vpc.subnet_self_links["${var.region}/apps"]
- nat = false
- addresses = null
- }]
- boot_disk = {
- image = "debian-cloud/debian-10"
- type = "pd-standard"
- size = 10
- }
- service_account_create = true
-}
diff --git a/examples/networking/filtering-proxy/variables.tf b/examples/networking/filtering-proxy/variables.tf
deleted file mode 100644
index 35245a409e..0000000000
--- a/examples/networking/filtering-proxy/variables.tf
+++ /dev/null
@@ -1,66 +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 "allowed_domains" {
- description = "List of domains allowed by the squid proxy."
- type = list(string)
- default = [
- ".google.com",
- ".github.com"
- ]
-}
-
-variable "billing_account" {
- description = "Billing account id used as default for new projects."
- type = string
-}
-
-variable "cidrs" {
- description = "CIDR ranges for subnets."
- type = map(string)
- default = {
- apps = "10.0.0.0/24"
- proxy = "10.0.1.0/28"
- }
-}
-
-variable "mig" {
- description = "Enables the creation of an autoscaling managed instance group of squid instances."
- type = bool
- default = false
-}
-
-variable "nat_logging" {
- description = "Enables Cloud NAT logging if not null, value is one of 'ERRORS_ONLY', 'TRANSLATIONS_ONLY', 'ALL'."
- type = string
- default = "ERRORS_ONLY"
-}
-
-variable "prefix" {
- description = "Prefix used for resources that need unique names."
- type = string
-}
-
-variable "region" {
- description = "Default region for resources."
- type = string
- default = "europe-west1"
-}
-
-variable "root_node" {
- description = "Root node for the new hierarchy, either 'organizations/org_id' or 'folders/folder_id'."
- type = string
-}
diff --git a/examples/networking/filtering-proxy/versions.tf b/examples/networking/filtering-proxy/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/networking/filtering-proxy/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/networking/hub-and-spoke-peering/README.md b/examples/networking/hub-and-spoke-peering/README.md
deleted file mode 100644
index 4cf68108c3..0000000000
--- a/examples/networking/hub-and-spoke-peering/README.md
+++ /dev/null
@@ -1,102 +0,0 @@
-# Hub and Spoke via VPC Peering
-
-This example creates a simple **Hub and Spoke** setup, where the VPC network connects satellite locations (spokes) through a single intermediary location (hub) via [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering).
-
-The example shows some of the limitations that need to be taken into account when using VPC Peering, mostly due to the lack of transivity between peerings:
-
-- no mesh networking between the spokes
-- complex support for managed services hosted in tenant VPCs connected via peering (Cloud SQL, GKE, etc.)
-
-One possible solution to the managed service limitation above is presented here, using a static VPN to establish connectivity to the GKE masters in the tenant project ([courtesy of @drebes](https://github.com/drebes/tf-samples/blob/master/gke-master-from-hub/main.tf#L10)). Other solutions typically involve the use of proxies, as [described in this GKE article](https://cloud.google.com/solutions/creating-kubernetes-engine-private-clusters-with-net-proxies).
-
-One other topic that needs to be considered when using peering is the limit of 25 peerings in each peering group, which constrains the scalability of design like the one presented here.
-
-The example has been purposefully kept simple to show how to use and wire the VPC modules together, and so that it can be used as a basis for more complex scenarios. This is the high level diagram:
-
-![High-level diagram](diagram.png "High-level diagram")
-
-## Managed resources and services
-
-This sample creates several distinct groups of resources:
-
-- one VPC each for hub and each spoke
-- one set of firewall rules for each VPC
-- one Cloud NAT configuration for each spoke
-- one test instance for each spoke
-- one GKE cluster with a single nodepool in spoke 2
-- one service account for the GCE instances
-- one service account for the GKE nodes
-- one static VPN gateway in hub and spoke 2 with a single tunnel each
-
-## Testing GKE access from spoke 1
-
-As mentioned above, a VPN tunnel is used as a workaround to avoid the peering transitivity issue that would prevent any VPC other than spoke 2 to connect to the GKE master. This diagram illustrates the solution
-
-![Network-level diagram](diagram-network.png "Network-level diagram")
-
-To test cluster access, first log on to the spoke 2 instance and confirm cluster and IAM roles are set up correctly:
-
-```bash
-gcloud container clusters get-credentials cluster-1 --zone europe-west1-b
-kubectl get all
-```
-
-The example configures the peering with the GKE master VPC to export routes for you, so that VPN routes are passed through the peering. You can disable by hand in the console or by editing the `peering_config` variable in the `gke-cluster` module, to test non-working configurations or switch to using the [GKE proxy](https://cloud.google.com/solutions/creating-kubernetes-engine-private-clusters-with-net-proxies).
-
-### Export routes via Terraform (recommended)
-
-Change the GKE cluster module and add a new variable after `private_cluster_config`:
-
-```hcl
- peering_config = {
- export_routes = true
- import_routes = false
- }
-```
-
-If you added the variable after applying, simply apply Terraform again.
-
-### Export routes via gcloud (alternative)
-
-If you prefer to use `gcloud` to export routes on the peering, first identify the peering (it has a name like `gke-xxxxxxxxxxxxxxxxxxxx-xxxx-xxxx-peer`) in the Cloud Console from the *VPC network peering* page, or using `gcloud`, then configure it to export routes:
-
-```
-gcloud compute networks peerings list
-# find the gke-xxxxxxxxxxxxxxxxxxxx-xxxx-xxxx-peer in the spoke-2 network
-gcloud compute networks peerings update [peering name from above] \
- --network spoke-2 --export-custom-routes
-```
-
-### Test routes
-
-Then connect via SSH to the spoke 1 instance and run the same commands you ran on the spoke 2 instance above, you should be able to run `kubectl` commands against the cluster. To test the default situation with no supporting VPN, just comment out the two VPN modules in `main.tf` and run `terraform apply` to bring down the VPN gateways and tunnels. GKE should only become accessible from spoke 2.
-
-## Operational considerations
-
-A single pre-existing project is used in this example to keep variables and complexity to a minimum, in a real world scenario each spoke would use a separate project (and Shared VPC).
-
-A few APIs need to be enabled in the project, if `apply` fails due to a service not being enabled just click on the link in the error message to enable it for the project, then resume `apply`.
-
-The VPN used to connect the GKE masters VPC does not account for HA, upgrading to use HA VPN is reasonably simple by using the relevant [module](../../../modules/net-vpn-ha).
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [project_id](variables.tf#L66) | Project id used for all resources. | string
| ✓ | |
-| [ip_ranges](variables.tf#L15) | IP CIDR ranges. | map(string)
| | {…}
|
-| [ip_secondary_ranges](variables.tf#L25) | Secondary IP CIDR ranges. | map(string)
| | {…}
|
-| [prefix](variables.tf#L34) | Arbitrary string used to prefix resource names. | string
| | null
|
-| [private_service_ranges](variables.tf#L40) | Private service IP CIDR ranges. | map(string)
| | {…}
|
-| [project_create](variables.tf#L48) | Set to non null if project needs to be created. | object({…})
| | null
|
-| [region](variables.tf#L71) | VPC region. | string
| | "europe-west1"
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [project](outputs.tf#L15) | Project id. | |
-| [vms](outputs.tf#L20) | GCE VMs. | |
-
-
diff --git a/examples/networking/hub-and-spoke-peering/main.tf b/examples/networking/hub-and-spoke-peering/main.tf
deleted file mode 100644
index f2109b83fa..0000000000
--- a/examples/networking/hub-and-spoke-peering/main.tf
+++ /dev/null
@@ -1,333 +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
-#
-# 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.
-
-locals {
- prefix = var.prefix != null && var.prefix != "" ? "${var.prefix}-" : ""
- vm-instances = [
- module.vm-hub.instance,
- module.vm-spoke-1.instance,
- module.vm-spoke-2.instance
- ]
- vm-startup-script = join("\n", [
- "#! /bin/bash",
- "apt-get update && apt-get install -y bash-completion dnsutils kubectl"
- ])
-}
-
-###############################################################################
-# project #
-###############################################################################
-
-module "project" {
- source = "../../../modules/project"
- project_create = var.project_create != null
- billing_account = try(var.project_create.billing_account, null)
- oslogin = try(var.project_create.oslogin, false)
- parent = try(var.project_create.parent, null)
- name = var.project_id
- services = [
- "compute.googleapis.com",
- "container.googleapis.com"
- ]
- service_config = {
- disable_on_destroy = false,
- disable_dependent_services = false
- }
-}
-
-################################################################################
-# Hub networking #
-################################################################################
-
-module "vpc-hub" {
- source = "../../../modules/net-vpc"
- project_id = module.project.project_id
- name = "${local.prefix}hub"
- subnets = [
- {
- ip_cidr_range = var.ip_ranges.hub
- name = "${local.prefix}hub-1"
- region = var.region
- secondary_ip_range = {}
- }
- ]
-}
-
-module "nat-hub" {
- source = "../../../modules/net-cloudnat"
- project_id = module.project.project_id
- region = var.region
- name = "${local.prefix}hub"
- router_name = "${local.prefix}hub"
- router_network = module.vpc-hub.self_link
-}
-
-module "vpc-hub-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = var.project_id
- network = module.vpc-hub.name
- admin_ranges = values(var.ip_ranges)
-}
-
-################################################################################
-# Spoke 1 networking #
-################################################################################
-
-module "vpc-spoke-1" {
- source = "../../../modules/net-vpc"
- project_id = module.project.project_id
- name = "${local.prefix}spoke-1"
- subnets = [
- {
- ip_cidr_range = var.ip_ranges.spoke-1
- name = "${local.prefix}spoke-1-1"
- region = var.region
- secondary_ip_range = {}
- }
- ]
-}
-
-module "vpc-spoke-1-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.project.project_id
- network = module.vpc-spoke-1.name
- admin_ranges = values(var.ip_ranges)
-}
-
-module "nat-spoke-1" {
- source = "../../../modules/net-cloudnat"
- project_id = module.project.project_id
- region = var.region
- name = "${local.prefix}spoke-1"
- router_name = "${local.prefix}spoke-1"
- router_network = module.vpc-spoke-1.self_link
-}
-
-module "hub-to-spoke-1-peering" {
- source = "../../../modules/net-vpc-peering"
- local_network = module.vpc-hub.self_link
- peer_network = module.vpc-spoke-1.self_link
- export_local_custom_routes = true
- export_peer_custom_routes = false
-}
-
-################################################################################
-# Spoke 2 networking #
-################################################################################
-
-module "vpc-spoke-2" {
- source = "../../../modules/net-vpc"
- project_id = module.project.project_id
- name = "${local.prefix}spoke-2"
- subnets = [
- {
- ip_cidr_range = var.ip_ranges.spoke-2
- name = "${local.prefix}spoke-2-1"
- region = var.region
- secondary_ip_range = {
- pods = var.ip_secondary_ranges.spoke-2-pods
- services = var.ip_secondary_ranges.spoke-2-services
- }
- }
- ]
-}
-
-module "vpc-spoke-2-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.project.project_id
- network = module.vpc-spoke-2.name
- admin_ranges = values(var.ip_ranges)
-}
-
-module "nat-spoke-2" {
- source = "../../../modules/net-cloudnat"
- project_id = module.project.project_id
- region = var.region
- name = "${local.prefix}spoke-2"
- router_name = "${local.prefix}spoke-2"
- router_network = module.vpc-spoke-2.self_link
-}
-
-module "hub-to-spoke-2-peering" {
- source = "../../../modules/net-vpc-peering"
- local_network = module.vpc-hub.self_link
- peer_network = module.vpc-spoke-2.self_link
- export_local_custom_routes = true
- export_peer_custom_routes = false
- depends_on = [module.hub-to-spoke-1-peering]
-}
-
-################################################################################
-# Test VMs #
-################################################################################
-
-module "vm-hub" {
- source = "../../../modules/compute-vm"
- project_id = module.project.project_id
- zone = "${var.region}-b"
- name = "${local.prefix}hub"
- network_interfaces = [{
- network = module.vpc-hub.self_link
- subnetwork = module.vpc-hub.subnet_self_links["${var.region}/${local.prefix}hub-1"]
- nat = false
- addresses = null
- }]
- metadata = { startup-script = local.vm-startup-script }
- service_account = module.service-account-gce.email
- service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
- tags = ["ssh"]
-}
-
-module "vm-spoke-1" {
- source = "../../../modules/compute-vm"
- project_id = module.project.project_id
- zone = "${var.region}-b"
- name = "${local.prefix}spoke-1"
- network_interfaces = [{
- network = module.vpc-spoke-1.self_link
- subnetwork = module.vpc-spoke-1.subnet_self_links["${var.region}/${local.prefix}spoke-1-1"]
- nat = false
- addresses = null
- }]
- metadata = { startup-script = local.vm-startup-script }
- service_account = module.service-account-gce.email
- service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
- tags = ["ssh"]
-}
-
-module "vm-spoke-2" {
- source = "../../../modules/compute-vm"
- project_id = module.project.project_id
- zone = "${var.region}-b"
- name = "${local.prefix}spoke-2"
- network_interfaces = [{
- network = module.vpc-spoke-2.self_link
- subnetwork = module.vpc-spoke-2.subnet_self_links["${var.region}/${local.prefix}spoke-2-1"]
- nat = false
- addresses = null
- }]
- metadata = { startup-script = local.vm-startup-script }
- service_account = module.service-account-gce.email
- service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
- tags = ["ssh"]
-}
-
-module "service-account-gce" {
- source = "../../../modules/iam-service-account"
- project_id = module.project.project_id
- name = "${local.prefix}gce-test"
- iam_project_roles = {
- (var.project_id) = [
- "roles/container.developer",
- "roles/logging.logWriter",
- "roles/monitoring.metricWriter",
- ]
- }
-}
-
-################################################################################
-# GKE #
-################################################################################
-
-module "cluster-1" {
- source = "../../../modules/gke-cluster"
- name = "${local.prefix}cluster-1"
- project_id = module.project.project_id
- location = "${var.region}-b"
- network = module.vpc-spoke-2.self_link
- subnetwork = module.vpc-spoke-2.subnet_self_links["${var.region}/${local.prefix}spoke-2-1"]
- secondary_range_pods = "pods"
- secondary_range_services = "services"
- default_max_pods_per_node = 32
- labels = {
- environment = "test"
- }
- master_authorized_ranges = {
- for name, range in var.ip_ranges : name => range
- }
- private_cluster_config = {
- enable_private_nodes = true
- enable_private_endpoint = true
- master_ipv4_cidr_block = var.private_service_ranges.spoke-2-cluster-1
- master_global_access = true
- }
- peering_config = {
- export_routes = true
- import_routes = false
- project_id = null
- }
-}
-
-module "cluster-1-nodepool-1" {
- source = "../../../modules/gke-nodepool"
- name = "${local.prefix}nodepool-1"
- project_id = module.project.project_id
- location = module.cluster-1.location
- cluster_name = module.cluster-1.name
- node_service_account = module.service-account-gke-node.email
-}
-
-# roles assigned via this module use non-authoritative IAM bindings at the
-# project level, with no risk of conflicts with pre-existing roles
-
-module "service-account-gke-node" {
- source = "../../../modules/iam-service-account"
- project_id = module.project.project_id
- name = "${local.prefix}gke-node"
- iam_project_roles = {
- (var.project_id) = [
- "roles/logging.logWriter", "roles/monitoring.metricWriter",
- ]
- }
-}
-
-################################################################################
-# GKE peering VPN #
-################################################################################
-
-module "vpn-hub" {
- source = "../../../modules/net-vpn-static"
- project_id = module.project.project_id
- region = var.region
- network = module.vpc-hub.name
- name = "${local.prefix}hub"
- remote_ranges = values(var.private_service_ranges)
- tunnels = {
- spoke-2 = {
- ike_version = 2
- peer_ip = module.vpn-spoke-2.address
- shared_secret = ""
- traffic_selectors = { local = ["0.0.0.0/0"], remote = null }
- }
- }
-}
-
-module "vpn-spoke-2" {
- source = "../../../modules/net-vpn-static"
- project_id = module.project.project_id
- region = var.region
- network = module.vpc-spoke-2.name
- name = "${local.prefix}spoke-2"
- # use an aggregate of the remote ranges, so as to be less specific than the
- # routes exchanged via peering
- remote_ranges = ["10.0.0.0/8"]
- tunnels = {
- hub = {
- ike_version = 2
- peer_ip = module.vpn-hub.address
- shared_secret = module.vpn-hub.random_secret
- traffic_selectors = { local = ["0.0.0.0/0"], remote = null }
- }
- }
-}
diff --git a/examples/networking/hub-and-spoke-peering/variables.tf b/examples/networking/hub-and-spoke-peering/variables.tf
deleted file mode 100644
index fdaf4e834a..0000000000
--- a/examples/networking/hub-and-spoke-peering/variables.tf
+++ /dev/null
@@ -1,75 +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
-#
-# 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.
-
-variable "ip_ranges" {
- description = "IP CIDR ranges."
- type = map(string)
- default = {
- hub = "10.0.0.0/24"
- spoke-1 = "10.0.16.0/24"
- spoke-2 = "10.0.32.0/24"
- }
-}
-
-variable "ip_secondary_ranges" {
- description = "Secondary IP CIDR ranges."
- type = map(string)
- default = {
- spoke-2-pods = "10.128.0.0/18"
- spoke-2-services = "172.16.0.0/24"
- }
-}
-
-variable "prefix" {
- description = "Arbitrary string used to prefix resource names."
- type = string
- default = null
-}
-
-variable "private_service_ranges" {
- description = "Private service IP CIDR ranges."
- type = map(string)
- default = {
- spoke-2-cluster-1 = "192.168.0.0/28"
- }
-}
-
-variable "project_create" {
- description = "Set to non null if project needs to be created."
- type = object({
- billing_account = string
- oslogin = bool
- parent = string
- })
- default = null
- validation {
- condition = (
- var.project_create == null
- ? true
- : can(regex("(organizations|folders)/[0-9]+", var.project_create.parent))
- )
- error_message = "Project parent must be of the form folders/folder_id or organizations/organization_id."
- }
-}
-
-variable "project_id" {
- description = "Project id used for all resources."
- type = string
-}
-
-variable "region" {
- description = "VPC region."
- type = string
- default = "europe-west1"
-}
diff --git a/examples/networking/hub-and-spoke-peering/versions.tf b/examples/networking/hub-and-spoke-peering/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/networking/hub-and-spoke-peering/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/networking/hub-and-spoke-vpn/README.md b/examples/networking/hub-and-spoke-vpn/README.md
deleted file mode 100644
index f745b2a445..0000000000
--- a/examples/networking/hub-and-spoke-vpn/README.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# Hub and Spoke via VPN
-
-This example creates a simple **Hub and Spoke VPN** setup, where the VPC network connects satellite locations (spokes) through a single intermediary location (hub) via [IPsec VPN](https://cloud.google.com/vpn/docs/concepts/overview), optionally providing full-mesh networking via [custom route advertisements](https://cloud.google.com/router/docs/how-to/advertising-overview).
-
-The example has been purposefully kept simple to show how to use and wire the VPC and VPN modules together, and so that it can be used as a basis for more complex scenarios. This is the high level diagram:
-
-![High-level diagram](diagram.png "High-level diagram")
-
-## Managed resources and services
-
-This sample creates several distinct groups of resources:
-
-- one VPC for each hub and each spoke
-- one set of firewall rules for each VPC
-- one VPN gateway, one tunnel and one Cloud Router for each spoke
-- two VPN gateways, two tunnels and two Cloud Routers for the hub (one for each spoke)
-- one DNS private zone in the hub
-- one DNS peering zone in each spoke
-- one Cloud NAT configuration for each spoke
-- one test instance for each spoke
-
-## Operational considerations
-
-A single pre-existing project is used in this example to keep variables and complexity to a minimum, in a real world scenarios each spoke would probably use a separate project. The provided project needs a valid billing account and the Compute and DNS APIs enabled. You can easily create such a project with the [project module](../../../modules/project) or with the following commands:
-
-``` shell
-MY_PROJECT_ID="string
| ✓ | |
-| [bgp_asn](variables.tf#L15) | BGP ASNs. | map(number)
| | {…}
|
-| [bgp_custom_advertisements](variables.tf#L25) | BGP custom advertisement IP CIDR ranges. | map(string)
| | {…}
|
-| [bgp_interface_ranges](variables.tf#L34) | BGP interface IP CIDR ranges. | map(string)
| | {…}
|
-| [ip_ranges](variables.tf#L43) | IP CIDR ranges. | map(string)
| | {…}
|
-| [regions](variables.tf#L61) | VPC regions. | map(string)
| | {…}
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [vms](outputs.tf#L15) | GCE VMs. | |
-
-
diff --git a/examples/networking/hub-and-spoke-vpn/diagram.png b/examples/networking/hub-and-spoke-vpn/diagram.png
deleted file mode 100644
index 6d5dc5ff25..0000000000
Binary files a/examples/networking/hub-and-spoke-vpn/diagram.png and /dev/null differ
diff --git a/examples/networking/hub-and-spoke-vpn/main.tf b/examples/networking/hub-and-spoke-vpn/main.tf
deleted file mode 100644
index 3eec0ae6ec..0000000000
--- a/examples/networking/hub-and-spoke-vpn/main.tf
+++ /dev/null
@@ -1,309 +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
-#
-# 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.
-
-locals {
- vm-startup-script = join("\n", [
- "#! /bin/bash",
- "apt-get update && apt-get install -y dnsutils"
- ])
-}
-
-################################################################################
-# Hub networking #
-################################################################################
-
-module "vpc-hub" {
- source = "../../../modules/net-vpc"
- project_id = var.project_id
- name = "hub"
- subnets = [
- {
- ip_cidr_range = var.ip_ranges.hub-a
- name = "hub-a"
- region = var.regions.a
- secondary_ip_range = {}
- },
- {
- ip_cidr_range = var.ip_ranges.hub-b
- name = "hub-b"
- region = var.regions.b
- secondary_ip_range = {}
- }
- ]
-}
-
-module "vpc-hub-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = var.project_id
- network = module.vpc-hub.name
- admin_ranges = values(var.ip_ranges)
-}
-
-module "vpn-hub-a" {
- source = "../../../modules/net-vpn-dynamic"
- project_id = var.project_id
- region = var.regions.a
- network = module.vpc-hub.name
- name = "hub-a"
- router_asn = var.bgp_asn.hub
- tunnels = {
- spoke-1 = {
- bgp_peer = {
- address = cidrhost(var.bgp_interface_ranges.spoke-1, 2)
- asn = var.bgp_asn.spoke-1
- }
- bgp_peer_options = {
- advertise_groups = ["ALL_SUBNETS"]
- advertise_ip_ranges = {
- (var.bgp_custom_advertisements.hub-to-spoke-1) = "spoke-2"
- }
- advertise_mode = "CUSTOM"
- route_priority = 1000
- }
- bgp_session_range = "${cidrhost(var.bgp_interface_ranges.spoke-1, 1)}/30"
- ike_version = 2
- peer_ip = module.vpn-spoke-1.address
- router = null
- shared_secret = ""
- }
- }
-}
-
-module "vpn-hub-b" {
- source = "../../../modules/net-vpn-dynamic"
- project_id = var.project_id
- region = var.regions.b
- network = module.vpc-hub.name
- name = "hub-b"
- router_asn = var.bgp_asn.hub
- tunnels = {
- spoke-2 = {
- bgp_peer = {
- address = cidrhost(var.bgp_interface_ranges.spoke-2, 2)
- asn = var.bgp_asn.spoke-2
- }
- bgp_peer_options = {
- advertise_groups = ["ALL_SUBNETS"]
- advertise_ip_ranges = {
- (var.bgp_custom_advertisements.hub-to-spoke-2) = "spoke-1"
- }
- advertise_mode = "CUSTOM"
- route_priority = 1000
- }
- bgp_session_range = "${cidrhost(var.bgp_interface_ranges.spoke-2, 1)}/30"
- ike_version = 2
- peer_ip = module.vpn-spoke-2.address
- router = null
- shared_secret = ""
- }
- }
-}
-
-################################################################################
-# Spoke 1 networking #
-################################################################################
-
-module "vpc-spoke-1" {
- source = "../../../modules/net-vpc"
- project_id = var.project_id
- name = "spoke-1"
- subnets = [
- {
- ip_cidr_range = var.ip_ranges.spoke-1-a
- name = "spoke-1-a"
- region = var.regions.a
- secondary_ip_range = {}
- },
- {
- ip_cidr_range = var.ip_ranges.spoke-1-b
- name = "spoke-1-b"
- region = var.regions.b
- secondary_ip_range = {}
- }
- ]
-}
-
-module "vpc-spoke-1-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = var.project_id
- network = module.vpc-spoke-1.name
- admin_ranges = values(var.ip_ranges)
-}
-
-module "vpn-spoke-1" {
- source = "../../../modules/net-vpn-dynamic"
- project_id = var.project_id
- region = var.regions.a
- network = module.vpc-spoke-1.name
- name = "spoke-1"
- router_asn = var.bgp_asn.spoke-1
- tunnels = {
- hub = {
- bgp_peer = {
- address = cidrhost(var.bgp_interface_ranges.spoke-1, 1)
- asn = var.bgp_asn.hub
- }
- bgp_peer_options = null
- bgp_session_range = "${cidrhost(var.bgp_interface_ranges.spoke-1, 2)}/30"
- ike_version = 2
- peer_ip = module.vpn-hub-a.address
- router = null
- shared_secret = module.vpn-hub-a.random_secret
- }
- }
-}
-
-module "nat-spoke-1" {
- source = "../../../modules/net-cloudnat"
- project_id = var.project_id
- region = var.regions.a
- name = "spoke-1"
- router_create = false
- router_name = module.vpn-spoke-1.router_name
-}
-
-################################################################################
-# Spoke 2 networking #
-################################################################################
-
-module "vpc-spoke-2" {
- source = "../../../modules/net-vpc"
- project_id = var.project_id
- name = "spoke-2"
- subnets = [
- {
- ip_cidr_range = var.ip_ranges.spoke-2-a
- name = "spoke-2-a"
- region = var.regions.a
- secondary_ip_range = {}
- },
- {
- ip_cidr_range = var.ip_ranges.spoke-2-b
- name = "spoke-2-b"
- region = var.regions.b
- secondary_ip_range = {}
- }
- ]
-}
-
-module "vpc-spoke-2-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = var.project_id
- network = module.vpc-spoke-2.name
- admin_ranges = values(var.ip_ranges)
-}
-
-module "vpn-spoke-2" {
- source = "../../../modules/net-vpn-dynamic"
- project_id = var.project_id
- region = var.regions.a
- network = module.vpc-spoke-2.name
- name = "spoke-2"
- router_asn = var.bgp_asn.spoke-2
- tunnels = {
- hub = {
- bgp_peer = {
- address = cidrhost(var.bgp_interface_ranges.spoke-2, 1)
- asn = var.bgp_asn.hub
- }
- bgp_peer_options = null
- bgp_session_range = "${cidrhost(var.bgp_interface_ranges.spoke-2, 2)}/30"
- ike_version = 2
- peer_ip = module.vpn-hub-b.address
- router = null
- shared_secret = module.vpn-hub-b.random_secret
- }
- }
-}
-
-module "nat-spoke-2" {
- source = "../../../modules/net-cloudnat"
- project_id = var.project_id
- region = var.regions.a
- name = "spoke-2"
- router_create = false
- router_name = module.vpn-spoke-2.router_name
-}
-
-################################################################################
-# Test VMs #
-################################################################################
-
-module "vm-spoke-1" {
- source = "../../../modules/compute-vm"
- project_id = var.project_id
- zone = "${var.regions.b}-b"
- name = "spoke-1-test"
- network_interfaces = [{
- network = module.vpc-spoke-1.self_link
- subnetwork = module.vpc-spoke-1.subnet_self_links["${var.regions.b}/spoke-1-b"]
- nat = false
- addresses = null
- }]
- tags = ["ssh"]
- metadata = { startup-script = local.vm-startup-script }
-}
-
-module "vm-spoke-2" {
- source = "../../../modules/compute-vm"
- project_id = var.project_id
- zone = "${var.regions.b}-b"
- name = "spoke-2-test"
- network_interfaces = [{
- network = module.vpc-spoke-2.self_link
- subnetwork = module.vpc-spoke-2.subnet_self_links["${var.regions.b}/spoke-2-b"]
- nat = false
- addresses = null
- }]
- tags = ["ssh"]
- metadata = { startup-script = local.vm-startup-script }
-}
-
-################################################################################
-# DNS zones #
-################################################################################
-
-module "dns-host" {
- source = "../../../modules/dns"
- project_id = var.project_id
- type = "private"
- name = "example"
- domain = "example.com."
- client_networks = [module.vpc-hub.self_link]
- recordsets = {
- "A localhost" = { ttl = 300, records = ["127.0.0.1"] }
- "A spoke-1-test" = { ttl = 300, records = [module.vm-spoke-1.internal_ip] }
- "A spoke-2-test" = { ttl = 300, records = [module.vm-spoke-2.internal_ip] }
- }
-}
-
-module "dns-spoke-1" {
- source = "../../../modules/dns"
- project_id = var.project_id
- type = "peering"
- name = "spoke-1"
- domain = "example.com."
- client_networks = [module.vpc-spoke-1.self_link]
- peer_network = module.vpc-hub.self_link
-}
-
-module "dns-spoke-2" {
- source = "../../../modules/dns"
- project_id = var.project_id
- type = "peering"
- name = "spoke-2"
- domain = "example.com."
- client_networks = [module.vpc-spoke-2.self_link]
- peer_network = module.vpc-hub.self_link
-}
diff --git a/examples/networking/hub-and-spoke-vpn/outputs.tf b/examples/networking/hub-and-spoke-vpn/outputs.tf
deleted file mode 100644
index f69ca1096f..0000000000
--- a/examples/networking/hub-and-spoke-vpn/outputs.tf
+++ /dev/null
@@ -1,21 +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
-#
-# 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 "vms" {
- description = "GCE VMs."
- value = {
- for instance in [module.vm-spoke-1.instance, module.vm-spoke-2.instance] :
- instance.name => instance.network_interface.0.network_ip
- }
-}
diff --git a/examples/networking/hub-and-spoke-vpn/variables.tf b/examples/networking/hub-and-spoke-vpn/variables.tf
deleted file mode 100644
index f30bebc5af..0000000000
--- a/examples/networking/hub-and-spoke-vpn/variables.tf
+++ /dev/null
@@ -1,68 +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
-#
-# 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.
-
-variable "bgp_asn" {
- description = "BGP ASNs."
- type = map(number)
- default = {
- hub = 64513
- spoke-1 = 64514
- spoke-2 = 64515
- }
-}
-
-variable "bgp_custom_advertisements" {
- description = "BGP custom advertisement IP CIDR ranges."
- type = map(string)
- default = {
- hub-to-spoke-1 = "10.0.32.0/20"
- hub-to-spoke-2 = "10.0.16.0/20"
- }
-}
-
-variable "bgp_interface_ranges" {
- description = "BGP interface IP CIDR ranges."
- type = map(string)
- default = {
- spoke-1 = "169.254.1.0/30"
- spoke-2 = "169.254.1.4/30"
- }
-}
-
-variable "ip_ranges" {
- description = "IP CIDR ranges."
- type = map(string)
- default = {
- hub-a = "10.0.0.0/24"
- hub-b = "10.0.8.0/24"
- spoke-1-a = "10.0.16.0/24"
- spoke-1-b = "10.0.24.0/24"
- spoke-2-a = "10.0.32.0/24"
- spoke-2-b = "10.0.40.0/24"
- }
-}
-
-variable "project_id" {
- description = "Project id for all resources."
- type = string
-}
-
-variable "regions" {
- description = "VPC regions."
- type = map(string)
- default = {
- a = "europe-west1"
- b = "europe-west2"
- }
-}
diff --git a/examples/networking/hub-and-spoke-vpn/versions.tf b/examples/networking/hub-and-spoke-vpn/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/networking/hub-and-spoke-vpn/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/networking/ilb-next-hop/README.md b/examples/networking/ilb-next-hop/README.md
deleted file mode 100644
index ad0f80d34c..0000000000
--- a/examples/networking/ilb-next-hop/README.md
+++ /dev/null
@@ -1,88 +0,0 @@
-# Internal Load Balancer as Next Hop
-
-This example bootstraps a minimal infrastructure for testing [ILB as next hop](https://cloud.google.com/load-balancing/docs/internal/ilb-next-hop-overview), using simple Linux gateway VMS between two VPCs to emulate virtual appliances.
-
-The following diagram shows the resources created by this example
-
-![High-level diagram](diagram.png "High-level diagram")
-
-Two ILBs are configured on the primary and secondary interfaces of gateway VMs with active health checks, but only a single one is used as next hop by default to simplify testing. The second (right-side) VPC has default routes that point to the gateway VMs, to also use the right-side ILB as next hop set the `ilb_right_enable` variable to `true`.
-
-## Testing
-
-This setup can be used to test and verify new ILB features like [forwards all protocols on ILB as next hops](https://cloud.google.com/load-balancing/docs/internal/ilb-next-hop-overview#all-traffic) and [symmetric hashing](https://cloud.google.com/load-balancing/docs/internal/ilb-next-hop-overview#symmetric-hashing), using simple `curl` and `ping` tests on clients. To make this practical, test VMs on both VPCs have `nginx` pre-installed and active on port 80.
-
-On the gateways, `iftop` and `tcpdump` are installed by default to quickly monitor traffic passing forwarded across VPCs.
-
-Session affinity on the ILB backend services can be changed using `gcloud compute backend-services update` on each of the ILBs, or by setting the `ilb_session_affinity` variable to update both ILBs.
-
-Simple `/root/start.sh` and `/root/stop.sh` scripts are pre-installed on both gateways to configure `iptables` so that health check requests are rejected and re-enabled, to quickly simulate removing instances from the ILB backends.
-
-Some scenarios to test:
-
-- short-lived connections with session affinity set to the default of `NONE`, then to `CLIENT_IP`
-- long-lived connections, failing health checks on the active gateway while the connection is active
-
-### Useful commands
-
-Basic commands to SSH to VMs and monitor backend health are provided in the Terraform outputs, and they already match input variables so that names, zones, etc. are correct. Other testing commands are provided below, adjust names to match your setup.
-
-Create a large file on a destination VM (eg `ilb-test-vm-right-1`) to test long-running connections.
-
-```bash
-dd if=/dev/zero of=/var/www/html/test.txt bs=10M count=100 status=progress
-```
-
-Run curl from a source VM (eg `ilb-test-vm-left-1`) to send requests to a destination VM artifically slowing traffic.
-
-```bash
-curl -0 --output /dev/null --limit-rate 10k 10.0.1.3/test.txt
-```
-
-Monitor traffic from a source VM (eg `ilb-test-vm-left-1`) on the gateways.
-
-```bash
-iftop -n -F 10.0.0.3/32
-```
-
-Poll summary health status for a backend.
-
-```bash
-watch '\
- gcloud compute backend-services get-health ilb-test-ilb-right \
- --region europe-west1 \
- --flatten status.healthStatus \
- --format "value(status.healthStatus.ipAddress, status.healthStatus.healthState)" \
-'
-```
-
-A sample testing session using `tmux`:
-
-
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [project_id](variables.tf#L50) | Existing project id. | string
| ✓ | |
-| [ilb_right_enable](variables.tf#L17) | Route right to left traffic through ILB. | bool
| | false
|
-| [ilb_session_affinity](variables.tf#L23) | Session affinity configuration for ILBs. | string
| | "CLIENT_IP"
|
-| [ip_ranges](variables.tf#L29) | IP CIDR ranges used for VPC subnets. | map(string)
| | {…}
|
-| [prefix](variables.tf#L38) | Prefix used for resource names. | string
| | "ilb-test"
|
-| [project_create](variables.tf#L44) | Create project instead of using an existing one. | bool
| | false
|
-| [region](variables.tf#L55) | Region used for resources. | string
| | "europe-west1"
|
-| [zones](variables.tf#L61) | Zone suffixes used for instances. | list(string)
| | ["b", "c"]
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [addresses](outputs.tf#L17) | IP addresses. | |
-| [backend_health_left](outputs.tf#L28) | Command-line health status for left ILB backends. | |
-| [backend_health_right](outputs.tf#L38) | Command-line health status for right ILB backends. | |
-| [ssh_gw](outputs.tf#L48) | Command-line login to gateway VMs. | |
-| [ssh_vm_left](outputs.tf#L56) | Command-line login to left VMs. | |
-| [ssh_vm_right](outputs.tf#L64) | Command-line login to right VMs. | |
-
-
diff --git a/examples/networking/ilb-next-hop/gateways.tf b/examples/networking/ilb-next-hop/gateways.tf
deleted file mode 100644
index e5a7fa253e..0000000000
--- a/examples/networking/ilb-next-hop/gateways.tf
+++ /dev/null
@@ -1,106 +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 "gw" {
- source = "../../../modules/compute-vm"
- for_each = local.zones
- project_id = module.project.project_id
- zone = each.value
- name = "${local.prefix}gw-${each.key}"
- instance_type = "f1-micro"
-
- boot_disk = {
- image = "projects/ubuntu-os-cloud/global/images/family/ubuntu-2004-lts",
- type = "pd-ssd",
- size = 10
- }
-
- network_interfaces = [
- {
- network = module.vpc-left.self_link
- subnetwork = values(module.vpc-left.subnet_self_links)[0],
- nat = false,
- addresses = null
- },
- {
- network = module.vpc-right.self_link
- subnetwork = values(module.vpc-right.subnet_self_links)[0],
- nat = false,
- addresses = null
- }
- ]
- tags = ["ssh"]
- can_ip_forward = true
- metadata = {
- user-data = templatefile("${path.module}/assets/gw.yaml", {
- gw_right = cidrhost(var.ip_ranges.right, 1)
- ip_cidr_right = var.ip_ranges.right
- })
- }
- service_account = try(
- module.service-accounts.emails["${local.prefix}gce-vm"], null
- )
- service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
- group = { named_ports = null }
-}
-
-module "ilb-left" {
- source = "../../../modules/net-ilb"
- project_id = module.project.project_id
- region = var.region
- name = "${local.prefix}ilb-left"
- network = module.vpc-left.self_link
- subnetwork = values(module.vpc-left.subnet_self_links)[0]
- address = local.addresses.ilb-left
- ports = null
- backend_config = {
- session_affinity = var.ilb_session_affinity
- timeout_sec = null
- connection_draining_timeout_sec = null
- }
- backends = [for z, mod in module.gw : {
- failover = false
- group = mod.group.self_link
- balancing_mode = "CONNECTION"
- }]
- health_check_config = {
- type = "tcp", check = { port = 22 }, config = {}, logging = true
- }
-}
-
-module "ilb-right" {
- source = "../../../modules/net-ilb"
- project_id = module.project.project_id
- region = var.region
- name = "${local.prefix}ilb-right"
- network = module.vpc-right.self_link
- subnetwork = values(module.vpc-right.subnet_self_links)[0]
- address = local.addresses.ilb-right
- ports = null
- backend_config = {
- session_affinity = var.ilb_session_affinity
- timeout_sec = null
- connection_draining_timeout_sec = null
- }
- backends = [for z, mod in module.gw : {
- failover = false
- group = mod.group.self_link
- balancing_mode = "CONNECTION"
- }]
- health_check_config = {
- type = "tcp", check = { port = 22 }, config = {}, logging = true
- }
-}
diff --git a/examples/networking/ilb-next-hop/main.tf b/examples/networking/ilb-next-hop/main.tf
deleted file mode 100644
index 80f479ff89..0000000000
--- a/examples/networking/ilb-next-hop/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.
- */
-
-locals {
- addresses = {
- for k, v in module.addresses.internal_addresses :
- trimprefix(k, local.prefix) => v.address
- }
- prefix = var.prefix == null || var.prefix == "" ? "" : "${var.prefix}-"
- zones = { for z in var.zones : z => "${var.region}-${z}" }
-}
-
-module "project" {
- source = "../../../modules/project"
- name = var.project_id
- project_create = var.project_create
- services = [
- "compute.googleapis.com",
- "dns.googleapis.com",
- ]
- service_config = {
- disable_on_destroy = false
- disable_dependent_services = false
- }
-}
-
-module "service-accounts" {
- source = "../../../modules/iam-service-account"
- project_id = module.project.project_id
- name = "${local.prefix}gce-vm"
- iam_project_roles = {
- (var.project_id) = [
- "roles/logging.logWriter",
- "roles/monitoring.metricWriter",
- ]
- }
-}
-
-module "addresses" {
- source = "../../../modules/net-address"
- project_id = module.project.project_id
- internal_addresses = {
- "${local.prefix}ilb-left" = {
- region = var.region,
- subnetwork = values(module.vpc-left.subnet_self_links)[0]
- },
- "${local.prefix}ilb-right" = {
- region = var.region,
- subnetwork = values(module.vpc-right.subnet_self_links)[0]
- }
- }
-}
diff --git a/examples/networking/ilb-next-hop/outputs.tf b/examples/networking/ilb-next-hop/outputs.tf
deleted file mode 100644
index 17702e832d..0000000000
--- a/examples/networking/ilb-next-hop/outputs.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.
- */
-
-output "addresses" {
- description = "IP addresses."
- value = {
- gw = [for z, mod in module.gw : mod.internal_ip]
- ilb-left = module.ilb-left.forwarding_rule_address
- ilb-right = module.ilb-right.forwarding_rule_address
- vm-left = [for z, mod in module.vm-left : mod.internal_ip]
- vm-right = [for z, mod in module.vm-right : mod.internal_ip]
- }
-}
-
-output "backend_health_left" {
- description = "Command-line health status for left ILB backends."
- value = <<-EOT
- gcloud compute backend-services get-health ${local.prefix}ilb-left \
- --region ${var.region} \
- --flatten status.healthStatus \
- --format "value(status.healthStatus.ipAddress, status.healthStatus.healthState)"
- EOT
-}
-
-output "backend_health_right" {
- description = "Command-line health status for right ILB backends."
- value = <<-EOT
- gcloud compute backend-services get-health ${local.prefix}ilb-right \
- --region ${var.region} \
- --flatten status.healthStatus \
- --format "value(status.healthStatus.ipAddress, status.healthStatus.healthState)"
- EOT
-}
-
-output "ssh_gw" {
- description = "Command-line login to gateway VMs."
- value = [
- for z, mod in module.gw :
- "gcloud compute ssh ${mod.instance.name} --project ${var.project_id} --zone ${mod.instance.zone}"
- ]
-}
-
-output "ssh_vm_left" {
- description = "Command-line login to left VMs."
- value = [
- for z, mod in module.vm-left :
- "gcloud compute ssh ${mod.instance.name} --project ${var.project_id} --zone ${mod.instance.zone}"
- ]
-}
-
-output "ssh_vm_right" {
- description = "Command-line login to right VMs."
- value = [
- for z, mod in module.vm-right :
- "gcloud compute ssh ${mod.instance.name} --project ${var.project_id} --zone ${mod.instance.zone}"
- ]
-}
diff --git a/examples/networking/ilb-next-hop/variables.tf b/examples/networking/ilb-next-hop/variables.tf
deleted file mode 100644
index 2450c4eba0..0000000000
--- a/examples/networking/ilb-next-hop/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 "ilb_right_enable" {
- description = "Route right to left traffic through ILB."
- type = bool
- default = false
-}
-
-variable "ilb_session_affinity" {
- description = "Session affinity configuration for ILBs."
- type = string
- default = "CLIENT_IP"
-}
-
-variable "ip_ranges" {
- description = "IP CIDR ranges used for VPC subnets."
- type = map(string)
- default = {
- left = "10.0.0.0/24"
- right = "10.0.1.0/24"
- }
-}
-
-variable "prefix" {
- description = "Prefix used for resource names."
- type = string
- default = "ilb-test"
-}
-
-variable "project_create" {
- description = "Create project instead of using an existing one."
- type = bool
- default = false
-}
-
-variable "project_id" {
- description = "Existing project id."
- type = string
-}
-
-variable "region" {
- description = "Region used for resources."
- type = string
- default = "europe-west1"
-}
-
-variable "zones" {
- description = "Zone suffixes used for instances."
- type = list(string)
- default = ["b", "c"]
-}
diff --git a/examples/networking/ilb-next-hop/versions.tf b/examples/networking/ilb-next-hop/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/networking/ilb-next-hop/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/networking/ilb-next-hop/vpc-left.tf b/examples/networking/ilb-next-hop/vpc-left.tf
deleted file mode 100644
index 07fc91b744..0000000000
--- a/examples/networking/ilb-next-hop/vpc-left.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 "vpc-left" {
- source = "../../../modules/net-vpc"
- project_id = module.project.project_id
- name = "${local.prefix}left"
- subnets = [
- {
- ip_cidr_range = var.ip_ranges.left
- name = "${local.prefix}left"
- region = var.region
- secondary_ip_range = {}
- },
- ]
- routes = {
- to-right = {
- dest_range = var.ip_ranges.right
- priority = null
- tags = null
- next_hop_type = "ilb"
- next_hop = module.ilb-left.forwarding_rule.self_link
- }
- }
-}
-
-module "firewall-left" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.project.project_id
- network = module.vpc-left.name
- admin_ranges = values(var.ip_ranges)
- ssh_source_ranges = ["35.235.240.0/20", "35.191.0.0/16", "130.211.0.0/22"]
-}
-
-module "nat-left" {
- source = "../../../modules/net-cloudnat"
- project_id = module.project.project_id
- region = var.region
- name = "${local.prefix}left"
- router_network = module.vpc-left.name
-}
diff --git a/examples/networking/ilb-next-hop/vpc-right.tf b/examples/networking/ilb-next-hop/vpc-right.tf
deleted file mode 100644
index 0343c4d75b..0000000000
--- a/examples/networking/ilb-next-hop/vpc-right.tf
+++ /dev/null
@@ -1,68 +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 "vpc-right" {
- source = "../../../modules/net-vpc"
- project_id = module.project.project_id
- name = "${local.prefix}right"
- subnets = [
- {
- ip_cidr_range = var.ip_ranges.right
- name = "${local.prefix}right"
- region = var.region
- secondary_ip_range = {}
- },
- ]
- routes = {
- to-left-ilb = {
- dest_range = var.ip_ranges.left
- priority = var.ilb_right_enable ? 900 : 1100
- tags = null
- next_hop_type = "ilb"
- next_hop = module.ilb-right.forwarding_rule.self_link
- }
- to-left-gw-1 = {
- dest_range = var.ip_ranges.left
- priority = null
- tags = null
- next_hop_type = "instance"
- next_hop = module.gw[var.zones[0]].self_link
- }
- to-left-gw-2 = {
- dest_range = var.ip_ranges.left
- priority = null
- tags = null
- next_hop_type = "instance"
- next_hop = module.gw[var.zones[1]].self_link
- }
- }
-}
-
-module "firewall-right" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.project.project_id
- network = module.vpc-right.name
- admin_ranges = values(var.ip_ranges)
- ssh_source_ranges = ["35.235.240.0/20", "35.191.0.0/16", "130.211.0.0/22"]
-}
-
-module "nat-right" {
- source = "../../../modules/net-cloudnat"
- project_id = module.project.project_id
- region = var.region
- name = "${local.prefix}right"
- router_network = module.vpc-right.name
-}
diff --git a/examples/networking/onprem-google-access-dns/README.md b/examples/networking/onprem-google-access-dns/README.md
deleted file mode 100644
index e87853ca18..0000000000
--- a/examples/networking/onprem-google-access-dns/README.md
+++ /dev/null
@@ -1,226 +0,0 @@
-# On-prem DNS and Google Private Access
-
-This example 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:
-
-- [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)
-- [Private Access for on-premises hosts](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid)
-
-The example has been purposefully kept simple to show how to use and wire the on-prem module, but it lends itself well to experimenting and can be combined with the other [infrastructure examples](../) in this repository to test different GCP networking patterns in connection to on-prem. This is the high level diagram:
-
-![High-level diagram](diagram.png "High-level diagram")
-
-## Managed resources and services
-
-This sample creates several distinct groups of resources:
-
-- one VPC with two regions
-- one set of firewall rules
-- one Cloud NAT configuration per region
-- one test instance on each region
-- one service account for the test instances
-- one service account for the onprem instance
-- two dynamic VPN gateways in each of the regions with a single tunnel
-- two DNS zones (private and forwarding) and a DNS inbound policy
-- one emulated on-premises environment in a single GCP instance
-
-## Cloud DNS inbound forwarder entry point
-
-The Cloud DNS inbound policy reserves an IP address in the VPC, which is used by the on-prem DNS server to forward queries to Cloud DNS. This address needs of course to be explicitly set in the on-prem DNS configuration (see below for details), but since there's currently no way for Terraform to find the exact address (cf [Google provider issue](https://github.com/terraform-providers/terraform-provider-google/issues/3753)), the following manual workaround needs to be applied.
-
-### 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]:
-
-```bash
-gcloud compute addresses list --project [your project id]
-```
-
-In the list of addresses, look for the address with purpose `DNS_RESOLVER` in the subnet `to-onprem-default`. If its IP address is `10.0.0.2` it matches the default value in the Terraform `forwarder_address` variable, which means you're all set. If it's different, proceed to the next step.
-
-### Update the forwarder address variable and recreate on-prem
-
-If the forwader address does not match the Terraform variable, add the correct value in your `terraform.tfvars` (or change the default value in `variables.tf`), then taint the onprem instance and apply to recreate it with the correct value in the DNS configuration:
-
-```bash
-tf apply
-tf taint 'module.vm-onprem.google_compute_instance.default["onprem-1"]'
-tf apply
-```
-
-## CoreDNS configuration for on-premises
-
-The on-prem module uses a CoreDNS container to expose its DNS service, configured with foru distinct blocks:
-
-- the onprem block serving static records for the `onprem.example.com` zone that map to each of the on-prem containers
-- the forwarding block for the `gcp.example.com` zone and for Google Private Access, that map to the IP address of the Cloud DNS inbound policy
-- the `google.internal` block that exposes to containers a name for the instance metadata address
-- the default block that forwards to Google public DNS resolvers
-
-This is the CoreDNS configuration:
-
-```coredns
-onprem.example.com {
- root /etc/coredns
- hosts onprem.hosts
- log
- errors
-}
-gcp.example.com googleapis.com {
- forward . ${resolver_address}
- log
- errors
-}
-google.internal {
- hosts {
- 169.254.169.254 metadata.google.internal
- }
-}
-. {
- forward . 8.8.8.8
- log
- errors
-}
-```
-
-## Testing
-
-### Onprem to cloud
-
-```bash
-# check containers are running
-sudo docker ps
-
-# connect to the onprem instance
-gcloud compute ssh onprem-1
-
-# check that the VPN tunnels are up
-sudo docker exec -it onprem_vpn_1 ipsec statusall
-
-Status of IKE charon daemon (strongSwan 5.8.1, Linux 5.4.0-1029-gcp, x86_64):
- uptime: 6 minutes, since Nov 30 08:42:08 2020
- worker threads: 11 of 16 idle, 5/0/0/0 working, job queue: 0/0/0/0, scheduled: 8
- loaded plugins: charon aesni mgf1 random nonce x509 revocation constraints pubkey pkcs1 pkcs7 pkcs8 pkcs12 pgp dnskey sshkey pem openssl fips-prf gmp curve25519 xcbc cmac curl sqlite attr kernel-netlink resolve socket-default farp stroke vici updown eap-identity eap-sim eap-aka eap-aka-3gpp2 eap-simaka-pseudonym eap-simaka-reauth eap-md5 eap-mschapv2 eap-radius eap-tls xauth-generic xauth-eap dhcp unity counters
-Listening IP addresses:
- 10.0.16.2
- 169.254.1.2
- 169.254.2.2
-Connections:
- gcp: %any...35.233.104.67,0.0.0.0/0,::/0 IKEv2, dpddelay=30s
- gcp: local: uses pre-shared key authentication
- gcp: remote: [35.233.104.67] uses pre-shared key authentication
- gcp: child: 0.0.0.0/0 === 0.0.0.0/0 TUNNEL, dpdaction=restart
- gcp2: %any...35.246.101.51,0.0.0.0/0,::/0 IKEv2, dpddelay=30s
- gcp2: local: uses pre-shared key authentication
- gcp2: remote: [35.246.101.51] uses pre-shared key authentication
- gcp2: child: 0.0.0.0/0 === 0.0.0.0/0 TUNNEL, dpdaction=restart
-Security Associations (2 up, 0 connecting):
- gcp2[4]: ESTABLISHED 6 minutes ago, 10.0.16.2[34.76.57.103]...35.246.101.51[35.246.101.51]
- gcp2[4]: IKEv2 SPIs: 227cb2c52085a743_i 13b18b0ad5d4de2b_r*, pre-shared key reauthentication in 9 hours
- gcp2[4]: IKE proposal: AES_GCM_16_256/PRF_HMAC_SHA2_512/MODP_2048
- gcp2{4}: INSTALLED, TUNNEL, reqid 2, ESP in UDP SPIs: cb6fdb84_i eea28dee_o
- gcp2{4}: AES_GCM_16_256, 3298 bytes_i, 3051 bytes_o (48 pkts, 3s ago), rekeying in 2 hours
- gcp2{4}: 0.0.0.0/0 === 0.0.0.0/0
- gcp[3]: ESTABLISHED 6 minutes ago, 10.0.16.2[34.76.57.103]...35.233.104.67[35.233.104.67]
- gcp[3]: IKEv2 SPIs: e2cffed5395b63dd_i 99f343468625507c_r*, pre-shared key reauthentication in 9 hours
- gcp[3]: IKE proposal: AES_GCM_16_256/PRF_HMAC_SHA2_512/MODP_2048
- gcp{3}: INSTALLED, TUNNEL, reqid 1, ESP in UDP SPIs: c3f09701_i 4e8cc8d5_o
- gcp{3}: AES_GCM_16_256, 3438 bytes_i, 3135 bytes_o (49 pkts, 8s ago), rekeying in 2 hours
- gcp{3}: 0.0.0.0/0 === 0.0.0.0/0
-
-# check that the BGP sessions works and the advertised routes are set
-sudo docker exec -it onprem_bird_1 ip route
-default via 10.0.16.1 dev eth0
-10.0.0.0/24 proto bird src 10.0.16.2
- nexthop via 169.254.1.1 dev vti0 weight 1
- nexthop via 169.254.2.1 dev vti1 weight 1
-10.0.16.0/24 dev eth0 proto kernel scope link src 10.0.16.2
-10.10.0.0/24 proto bird src 10.0.16.2
- nexthop via 169.254.1.1 dev vti0 weight 1
- nexthop via 169.254.2.1 dev vti1 weight 1
-35.199.192.0/19 proto bird src 10.0.16.2
- nexthop via 169.254.1.1 dev vti0 weight 1
- nexthop via 169.254.2.1 dev vti1 weight 1
-169.254.1.0/30 dev vti0 proto kernel scope link src 169.254.1.2
-169.254.2.0/30 dev vti1 proto kernel scope link src 169.254.2.2
-199.36.153.4/30 proto bird src 10.0.16.2
- nexthop via 169.254.1.1 dev vti0 weight 1
- nexthop via 169.254.2.1 dev vti1 weight 1
-199.36.153.8/30 proto bird src 10.0.16.2
- nexthop via 169.254.1.1 dev vti0 weight 1
- nexthop via 169.254.2.1 dev vti1 weight 1
-
-
-# get a shell on the toolbox container
-sudo docker exec -it onprem_toolbox_1 sh
-
-# test pinging the IP address of the test instances (check outputs for it)
-ping 10.0.0.3
-ping 10.10.0.3
-
-# note: if you are able to ping the IP but the DNS tests below do not work,
-# refer to the sections above on configuring the DNS inbound fwd IP
-
-# test forwarding from CoreDNS via the Cloud DNS inbound policy
-dig test-1-1.gcp.example.org +short
-10.0.0.3
-dig test-2-1.gcp.example.org +short
-10.10.0.3
-
-# test that Private Access is configured correctly
-dig compute.googleapis.com +short
-private.googleapis.com.
-199.36.153.8
-199.36.153.9
-199.36.153.10
-199.36.153.11
-
-# issue an API call via Private Access
-gcloud config set project [your project id]
-gcloud compute instances list
-```
-
-### Cloud to onprem
-
-```bash
-# connect to the test instance
-gcloud compute ssh test-1
-
-# test forwarding from Cloud DNS to onprem CoreDNS (address may differ)
-dig gw.onprem.example.org +short
-10.0.16.1
-
-# test a request to the onprem web server
-curl www.onprem.example.org -s |grep h1
-string
| ✓ | |
-| [bgp_asn](variables.tf#L17) | BGP ASNs. | map(number)
| | {…}
|
-| [bgp_interface_ranges](variables.tf#L28) | BGP interface IP CIDR ranges. | map(string)
| | {…}
|
-| [dns_forwarder_address](variables.tf#L37) | Address of the DNS server used to forward queries from on-premises. | string
| | "10.0.0.2"
|
-| [forwarder_address](variables.tf#L43) | GCP DNS inbound policy forwarder address. | string
| | "10.0.0.2"
|
-| [ip_ranges](variables.tf#L49) | IP CIDR ranges. | map(string)
| | {…}
|
-| [region](variables.tf#L64) | VPC region. | map(string)
| | {…}
|
-| [ssh_source_ranges](variables.tf#L73) | IP CIDR ranges that will be allowed to connect via SSH to the onprem instance. | list(string)
| | ["0.0.0.0/0"]
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [onprem-instance](outputs.tf#L17) | Onprem instance details. | |
-| [test-instance1](outputs.tf#L26) | Test instance details. | |
-| [test-instance2](outputs.tf#L33) | Test instance details. | |
-
-
diff --git a/examples/networking/onprem-google-access-dns/main.tf b/examples/networking/onprem-google-access-dns/main.tf
deleted file mode 100644
index 7266f855a1..0000000000
--- a/examples/networking/onprem-google-access-dns/main.tf
+++ /dev/null
@@ -1,330 +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 {
- bgp_interface_gcp1 = cidrhost(var.bgp_interface_ranges.gcp1, 1)
- bgp_interface_onprem1 = cidrhost(var.bgp_interface_ranges.gcp1, 2)
- bgp_interface_gcp2 = cidrhost(var.bgp_interface_ranges.gcp2, 1)
- bgp_interface_onprem2 = cidrhost(var.bgp_interface_ranges.gcp2, 2)
- netblocks = {
- dns = data.google_netblock_ip_ranges.dns-forwarders.cidr_blocks_ipv4.0
- private = data.google_netblock_ip_ranges.private-googleapis.cidr_blocks_ipv4.0
- restricted = data.google_netblock_ip_ranges.restricted-googleapis.cidr_blocks_ipv4.0
- }
- vips = {
- private = [for i in range(4) : cidrhost(local.netblocks.private, i)]
- restricted = [for i in range(4) : cidrhost(local.netblocks.restricted, i)]
- }
- vm-startup-script = join("\n", [
- "#! /bin/bash",
- "apt-get update && apt-get install -y bash-completion dnsutils kubectl"
- ])
-}
-
-data "google_netblock_ip_ranges" "dns-forwarders" {
- range_type = "dns-forwarders"
-}
-
-data "google_netblock_ip_ranges" "private-googleapis" {
- range_type = "private-googleapis"
-}
-
-data "google_netblock_ip_ranges" "restricted-googleapis" {
- range_type = "restricted-googleapis"
-}
-
-################################################################################
-# Networking #
-################################################################################
-
-module "vpc" {
- source = "../../../modules/net-vpc"
- project_id = var.project_id
- name = "to-onprem"
- subnets = [
- {
- ip_cidr_range = var.ip_ranges.gcp1
- name = "subnet1"
- region = var.region.gcp1
- secondary_ip_range = {}
- },
- {
- ip_cidr_range = var.ip_ranges.gcp2
- name = "subnet2"
- region = var.region.gcp2
- secondary_ip_range = {}
- }
- ]
-}
-
-module "vpc-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = var.project_id
- network = module.vpc.name
- admin_ranges = values(var.ip_ranges)
- ssh_source_ranges = var.ssh_source_ranges
-}
-
-module "vpn1" {
- source = "../../../modules/net-vpn-dynamic"
- project_id = var.project_id
- region = var.region.gcp1
- network = module.vpc.name
- name = "to-onprem1"
- router_asn = var.bgp_asn.gcp1
- tunnels = {
- onprem = {
- bgp_peer = {
- address = local.bgp_interface_onprem1
- asn = var.bgp_asn.onprem1
- }
- bgp_peer_options = {
- advertise_groups = ["ALL_SUBNETS"]
- advertise_ip_ranges = {
- (local.netblocks.dns) = "DNS resolvers"
- (local.netblocks.private) = "private.gooogleapis.com"
- (local.netblocks.restricted) = "restricted.gooogleapis.com"
- }
- advertise_mode = "CUSTOM"
- route_priority = 1000
- }
- bgp_session_range = "${local.bgp_interface_gcp1}/30"
- ike_version = 2
- peer_ip = module.vm-onprem.external_ip
- router = null
- shared_secret = ""
- }
- }
-}
-
-module "vpn2" {
- source = "../../../modules/net-vpn-dynamic"
- project_id = var.project_id
- region = var.region.gcp2
- network = module.vpc.name
- name = "to-onprem2"
- router_asn = var.bgp_asn.gcp2
- tunnels = {
- onprem = {
- bgp_peer = {
- address = local.bgp_interface_onprem2
- asn = var.bgp_asn.onprem2
- }
- bgp_peer_options = {
- advertise_groups = ["ALL_SUBNETS"]
- advertise_ip_ranges = {
- (local.netblocks.dns) = "DNS resolvers"
- (local.netblocks.private) = "private.gooogleapis.com"
- (local.netblocks.restricted) = "restricted.gooogleapis.com"
- }
- advertise_mode = "CUSTOM"
- route_priority = 1000
- }
- bgp_session_range = "${local.bgp_interface_gcp2}/30"
- ike_version = 2
- peer_ip = module.vm-onprem.external_ip
- router = null
- shared_secret = ""
- }
- }
-}
-
-module "nat1" {
- source = "../../../modules/net-cloudnat"
- project_id = var.project_id
- region = var.region.gcp1
- name = "default"
- router_create = false
- router_name = module.vpn1.router_name
-}
-module "nat2" {
- source = "../../../modules/net-cloudnat"
- project_id = var.project_id
- region = var.region.gcp2
- name = "default"
- router_create = false
- router_name = module.vpn2.router_name
-}
-
-################################################################################
-# DNS #
-################################################################################
-
-module "dns-gcp" {
- source = "../../../modules/dns"
- project_id = var.project_id
- type = "private"
- name = "gcp-example"
- domain = "gcp.example.org."
- client_networks = [module.vpc.self_link]
- recordsets = {
- "A localhost" = { ttl = 300, records = ["127.0.0.1"] }
- "A test-1" = { ttl = 300, records = [module.vm-test1.internal_ip] }
- "A test-2" = { ttl = 300, records = [module.vm-test2.internal_ip] }
- }
-}
-
-module "dns-api" {
- source = "../../../modules/dns"
- project_id = var.project_id
- type = "private"
- name = "googleapis"
- domain = "googleapis.com."
- client_networks = [module.vpc.self_link]
- recordsets = {
- "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
- "A private" = { ttl = 300, records = local.vips.private }
- "A restricted" = { ttl = 300, records = local.vips.restricted }
- }
-}
-
-module "dns-onprem" {
- source = "../../../modules/dns"
- project_id = var.project_id
- type = "forwarding"
- name = "onprem-example"
- domain = "onprem.example.org."
- client_networks = [module.vpc.self_link]
- forwarders = {
- "${cidrhost(var.ip_ranges.onprem, 3)}" = null
- }
-}
-
-resource "google_dns_policy" "inbound" {
- provider = google-beta
- project = var.project_id
- name = "gcp-inbound"
- enable_inbound_forwarding = true
- networks {
- network_url = module.vpc.self_link
- }
-}
-
-################################################################################
-# Test instance #
-################################################################################
-
-module "service-account-gce" {
- source = "../../../modules/iam-service-account"
- project_id = var.project_id
- name = "gce-test"
- iam_project_roles = {
- (var.project_id) = [
- "roles/logging.logWriter",
- "roles/monitoring.metricWriter",
- ]
- }
-}
-
-module "vm-test1" {
- source = "../../../modules/compute-vm"
- project_id = var.project_id
- zone = "${var.region.gcp1}-b"
- name = "test-1"
- network_interfaces = [{
- network = module.vpc.self_link
- subnetwork = module.vpc.subnet_self_links["${var.region.gcp1}/subnet1"]
- nat = false
- addresses = null
- }]
- metadata = { startup-script = local.vm-startup-script }
- service_account = module.service-account-gce.email
- service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
- tags = ["ssh"]
-}
-
-module "vm-test2" {
- source = "../../../modules/compute-vm"
- project_id = var.project_id
- zone = "${var.region.gcp2}-b"
- name = "test-2"
- network_interfaces = [{
- network = module.vpc.self_link
- subnetwork = module.vpc.subnet_self_links["${var.region.gcp2}/subnet2"]
- nat = false
- addresses = null
- }]
- metadata = { startup-script = local.vm-startup-script }
- service_account = module.service-account-gce.email
- service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
- tags = ["ssh"]
-}
-
-################################################################################
-# On prem #
-################################################################################
-
-module "config-onprem" {
- source = "../../../modules/cloud-config-container/onprem"
- config_variables = { dns_forwarder_address = var.dns_forwarder_address }
- coredns_config = "${path.module}/assets/Corefile"
- local_ip_cidr_range = var.ip_ranges.onprem
- vpn_config = {
- peer_ip = module.vpn1.address
- peer_ip2 = module.vpn2.address
- shared_secret = module.vpn1.random_secret
- shared_secret2 = module.vpn2.random_secret
- type = "dynamic"
- }
- vpn_dynamic_config = {
- local_bgp_asn = var.bgp_asn.onprem1
- local_bgp_address = local.bgp_interface_onprem1
- peer_bgp_asn = var.bgp_asn.gcp1
- peer_bgp_address = local.bgp_interface_gcp1
- local_bgp_asn2 = var.bgp_asn.onprem2
- local_bgp_address2 = local.bgp_interface_onprem2
- peer_bgp_asn2 = var.bgp_asn.gcp2
- peer_bgp_address2 = local.bgp_interface_gcp2
- }
-}
-
-module "service-account-onprem" {
- source = "../../../modules/iam-service-account"
- project_id = var.project_id
- name = "gce-onprem"
- iam_project_roles = {
- (var.project_id) = [
- "roles/compute.viewer",
- "roles/logging.logWriter",
- "roles/monitoring.metricWriter",
- ]
- }
-}
-
-module "vm-onprem" {
- source = "../../../modules/compute-vm"
- project_id = var.project_id
- zone = "${var.region.gcp1}-b"
- instance_type = "f1-micro"
- name = "onprem"
- boot_disk = {
- image = "ubuntu-os-cloud/ubuntu-1804-lts"
- type = "pd-ssd"
- size = 10
- }
- metadata = {
- user-data = module.config-onprem.cloud_config
- }
- network_interfaces = [{
- network = module.vpc.name
- subnetwork = module.vpc.subnet_self_links["${var.region.gcp1}/subnet1"]
- nat = true
- addresses = null
- }]
- service_account = module.service-account-onprem.email
- service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
- tags = ["ssh"]
-}
diff --git a/examples/networking/onprem-google-access-dns/versions.tf b/examples/networking/onprem-google-access-dns/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/networking/onprem-google-access-dns/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/networking/private-cloud-function-from-onprem/main.tf b/examples/networking/private-cloud-function-from-onprem/main.tf
deleted file mode 100644
index e71871805a..0000000000
--- a/examples/networking/private-cloud-function-from-onprem/main.tf
+++ /dev/null
@@ -1,260 +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 {
- psc_name = replace(var.name, "-", "")
-}
-
-module "project" {
- source = "../../../modules/project"
- name = var.project_id
- project_create = var.project_create == null ? false : true
- billing_account = try(var.project_create.billing_account_id, null)
- parent = try(var.project_create.parent, null)
- service_config = {
- disable_dependent_services = false
- disable_on_destroy = false
- }
- services = [
- "cloudfunctions.googleapis.com",
- "cloudbuild.googleapis.com",
- "compute.googleapis.com",
- "dns.googleapis.com"
- ]
-}
-
-###############################################################################
-# VPCs #
-###############################################################################
-
-module "vpc-onprem" {
- source = "../../../modules/net-vpc"
- project_id = module.project.project_id
- name = "${var.name}-onprem"
- subnets = [
- {
- ip_cidr_range = var.ip_ranges.onprem
- name = "${var.name}-onprem"
- region = var.region
- secondary_ip_range = {}
- }
- ]
- subnet_private_access = {
- "${var.region}/${var.name}-onprem" = false
- }
-}
-
-module "firewall-onprem" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.project.project_id
- network = module.vpc-onprem.name
-}
-
-module "vpc-hub" {
- source = "../../../modules/net-vpc"
- project_id = module.project.project_id
- name = "${var.name}-hub"
- subnets = [
- {
- ip_cidr_range = var.ip_ranges.hub
- name = "${var.name}-hub"
- region = var.region
- secondary_ip_range = {}
- }
- ]
-}
-
-###############################################################################
-# VPNs #
-###############################################################################
-
-module "vpn-onprem" {
- source = "../../../modules/net-vpn-ha"
- project_id = module.project.project_id
- region = var.region
- network = module.vpc-onprem.self_link
- name = "${var.name}-onprem-to-hub"
- router_asn = 65001
- router_advertise_config = {
- groups = ["ALL_SUBNETS"]
- ip_ranges = {
- }
- mode = "CUSTOM"
- }
- peer_gcp_gateway = module.vpn-hub.self_link
- tunnels = {
- tunnel-0 = {
- bgp_peer = {
- address = "169.254.0.2"
- asn = 65002
- }
- bgp_peer_options = null
- bgp_session_range = "169.254.0.1/30"
- ike_version = 2
- vpn_gateway_interface = 0
- peer_external_gateway_interface = null
- router = null
- shared_secret = ""
- }
- tunnel-1 = {
- bgp_peer = {
- address = "169.254.0.6"
- asn = 65002
- }
- bgp_peer_options = null
- bgp_session_range = "169.254.0.5/30"
- ike_version = 2
- vpn_gateway_interface = 1
- peer_external_gateway_interface = null
- router = null
- shared_secret = ""
- }
- }
-}
-
-module "vpn-hub" {
- source = "../../../modules/net-vpn-ha"
- project_id = module.project.project_id
- region = var.region
- network = module.vpc-hub.name
- name = "${var.name}-hub-to-onprem"
- router_asn = 65002
- peer_gcp_gateway = module.vpn-onprem.self_link
- router_advertise_config = {
- groups = ["ALL_SUBNETS"]
- ip_ranges = {
- (var.psc_endpoint) = "to-psc-endpoint"
- }
- mode = "CUSTOM"
- }
- tunnels = {
- tunnel-0 = {
- bgp_peer = {
- address = "169.254.0.1"
- asn = 65001
- }
- bgp_peer_options = null
- bgp_session_range = "169.254.0.2/30"
- ike_version = 2
- vpn_gateway_interface = 0
- peer_external_gateway_interface = null
- router = null
- shared_secret = module.vpn-onprem.random_secret
- }
- tunnel-1 = {
- bgp_peer = {
- address = "169.254.0.5"
- asn = 65001
- }
- bgp_peer_options = null
- bgp_session_range = "169.254.0.6/30"
- ike_version = 2
- vpn_gateway_interface = 1
- peer_external_gateway_interface = null
- router = null
- shared_secret = module.vpn-onprem.random_secret
- }
- }
-}
-
-###############################################################################
-# VMs #
-###############################################################################
-
-module "test-vm" {
- source = "../../../modules/compute-vm"
- project_id = module.project.project_id
- zone = "${var.region}-b"
- name = "${var.name}-test"
- instance_type = "e2-micro"
- boot_disk = {
- image = "projects/ubuntu-os-cloud/global/images/family/ubuntu-2104"
- type = "pd-balanced"
- size = 10
- }
- network_interfaces = [{
- addresses = null
- nat = false
- network = module.vpc-onprem.self_link
- subnetwork = module.vpc-onprem.subnet_self_links["${var.region}/${var.name}-onprem"]
- }]
- tags = ["ssh"]
-}
-
-###############################################################################
-# Cloud Function #
-###############################################################################
-
-module "function-hello" {
- source = "../../../modules/cloud-function"
- project_id = module.project.project_id
- name = var.name
- bucket_name = "${var.name}-tf-cf-deploy"
- ingress_settings = "ALLOW_INTERNAL_ONLY"
- bundle_config = {
- source_dir = "${path.module}/assets"
- output_path = "bundle.zip"
- excludes = null
- }
- bucket_config = {
- location = var.region
- lifecycle_delete_age = null
- }
- iam = {
- "roles/cloudfunctions.invoker" = ["allUsers"]
- }
-}
-
-###############################################################################
-# DNS #
-###############################################################################
-
-module "private-dns-onprem" {
- source = "../../../modules/dns"
- project_id = module.project.project_id
- type = "private"
- name = var.name
- domain = "${var.region}-${module.project.project_id}.cloudfunctions.net."
- client_networks = [module.vpc-onprem.self_link]
- recordsets = {
- "A " = { ttl = 300, records = [module.addresses.psc_addresses[local.psc_name].address] }
- }
-}
-
-###############################################################################
-# PSCs #
-###############################################################################
-
-module "addresses" {
- source = "../../../modules/net-address"
- project_id = module.project.project_id
- psc_addresses = {
- (local.psc_name) = {
- address = var.psc_endpoint
- network = module.vpc-hub.self_link
- }
- }
-}
-
-resource "google_compute_global_forwarding_rule" "psc-endpoint" {
- provider = google-beta
- project = module.project.project_id
- name = local.psc_name
- network = module.vpc-hub.self_link
- ip_address = module.addresses.psc_addresses[local.psc_name].self_link
- target = "vpc-sc"
- load_balancing_scheme = ""
-}
diff --git a/examples/networking/private-cloud-function-from-onprem/versions.tf b/examples/networking/private-cloud-function-from-onprem/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/networking/private-cloud-function-from-onprem/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/networking/shared-vpc-gke/README.md b/examples/networking/shared-vpc-gke/README.md
deleted file mode 100644
index 933a738422..0000000000
--- a/examples/networking/shared-vpc-gke/README.md
+++ /dev/null
@@ -1,72 +0,0 @@
-# Shared VPC with optional GKE cluster
-
-This sample creates a basic [Shared VPC](https://cloud.google.com/vpc/docs/shared-vpc) setup using one host project and two service projects, each with a specific subnet in the shared VPC.
-
-The setup also includes the specific IAM-level configurations needed for [GKE on Shared VPC](https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-shared-vpc) in one of the two service projects, and optionally creates a cluster with a single nodepool.
-
-If you only need a basic Shared VPC, or prefer creating a cluster manually, set the `cluster_create` variable to `False`.
-
-The sample has been purposefully kept simple so that it can be used as a basis for different Shared VPC configurations. This is the high level diagram:
-
-![High-level diagram](diagram.png "High-level diagram")
-
-## Accessing the bastion instance and GKE cluster
-
-The bastion VM has no public address so access is mediated via [IAP](https://cloud.google.com/iap/docs), which is supported transparently in the `gcloud compute ssh` command. Authentication is via OS Login set as a project default.
-
-Cluster access from the bastion can leverage the instance service account's `container.developer` role: the only configuration needed is to fetch cluster credentials via `gcloud container clusters get-credentials` passing the correct cluster name, location and project via command options.
-
-For convenience, [Tinyproxy](http://tinyproxy.github.io/) is installed on the bastion host, allowing `kubectl` use via [IAP](https://cloud.google.com/iap/docs) from an external client:
-
-```bash
-gcloud container clusters get-credentials "${CLUSTER_NAME}" \
- --zone "${CLUSTER_ZONE}" \
- --project "${CLUSTER_PROJECT_NAME}"
-
-gcloud compute ssh "${BASTION_INSTANCE_NAME}" \
- --project "${CLUSTER_PROJECT_NAME}" \
- --zone "${CLUSTER_ZONE}" \
- -- -L 8888:localhost:8888 -N -q -f
-
-# Run kubectl through the proxy
-HTTPS_PROXY=localhost:8888 kubectl get pods
-```
-
-An alias can also be created. For example:
-
-```bash
-alias k='HTTPS_PROXY=localhost:8888 kubectl $@'
-```
-
-## Destroying
-
-There's a minor glitch that can surface running `terraform destroy`, where the service project attachments to the Shared VPC will not get destroyed even with the relevant API call succeeding. We are investigating the issue, in the meantime just manually remove the attachment in the Cloud console or via the `gcloud beta compute shared-vpc associated-projects remove` command when `terraform destroy` fails, and then relaunch the command.
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [billing_account_id](variables.tf#L15) | Billing account id used as default for new projects. | string
| ✓ | |
-| [prefix](variables.tf#L62) | Prefix used for resources that need unique names. | string
| ✓ | |
-| [root_node](variables.tf#L90) | Hierarchy node where projects will be created, 'organizations/org_id' or 'folders/folder_id'. | string
| ✓ | |
-| [cluster_create](variables.tf#L20) | Create GKE cluster and nodepool. | bool
| | true
|
-| [ip_ranges](variables.tf#L26) | Subnet IP CIDR ranges. | map(string)
| | {…}
|
-| [ip_secondary_ranges](variables.tf#L35) | Secondary IP CIDR ranges. | map(string)
| | {…}
|
-| [owners_gce](variables.tf#L44) | GCE project owners, in IAM format. | list(string)
| | []
|
-| [owners_gke](variables.tf#L50) | GKE project owners, in IAM format. | list(string)
| | []
|
-| [owners_host](variables.tf#L56) | Host project owners, in IAM format. | list(string)
| | []
|
-| [private_service_ranges](variables.tf#L67) | Private service IP CIDR ranges. | map(string)
| | {…}
|
-| [project_services](variables.tf#L75) | Service APIs enabled by default in new projects. | list(string)
| | […]
|
-| [region](variables.tf#L84) | Region used. | string
| | "europe-west1"
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [gke_clusters](outputs.tf#L15) | GKE clusters information. | |
-| [projects](outputs.tf#L24) | Project ids. | |
-| [vms](outputs.tf#L33) | GCE VMs. | |
-| [vpc](outputs.tf#L40) | Shared VPC. | |
-
-
diff --git a/examples/networking/shared-vpc-gke/main.tf b/examples/networking/shared-vpc-gke/main.tf
deleted file mode 100644
index 9ee388bafb..0000000000
--- a/examples/networking/shared-vpc-gke/main.tf
+++ /dev/null
@@ -1,232 +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
-#
-# 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.
-
-###############################################################################
-# Host and service projects #
-###############################################################################
-
-# the container.hostServiceAgentUser role is needed for GKE on shared VPC
-# see: https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-shared-vpc#grant_host_service_agent_role
-
-module "project-host" {
- source = "../../../modules/project"
- parent = var.root_node
- billing_account = var.billing_account_id
- prefix = var.prefix
- name = "net"
- services = concat(var.project_services, ["dns.googleapis.com"])
- shared_vpc_host_config = {
- enabled = true
- service_projects = [] # defined later
- }
- iam = {
- "roles/owner" = var.owners_host
- }
-}
-
-module "project-svc-gce" {
- source = "../../../modules/project"
- parent = var.root_node
- billing_account = var.billing_account_id
- prefix = var.prefix
- name = "gce"
- services = var.project_services
- oslogin = true
- oslogin_admins = var.owners_gce
- shared_vpc_service_config = {
- host_project = module.project-host.project_id
- service_identity_iam = {
- "roles/compute.networkUser" = ["cloudservices"]
- }
- }
- iam = {
- "roles/owner" = var.owners_gce
- }
-}
-
-# the container.developer role assigned to the bastion instance service account
-# allows to fetch GKE credentials from bastion for clusters in this project
-
-module "project-svc-gke" {
- source = "../../../modules/project"
- parent = var.root_node
- billing_account = var.billing_account_id
- prefix = var.prefix
- name = "gke"
- services = var.project_services
- shared_vpc_service_config = {
- host_project = module.project-host.project_id
- service_identity_iam = {
- "roles/container.hostServiceAgentUser" = ["container-engine"]
- "roles/compute.networkUser" = ["container-engine"]
- }
- }
- iam = merge(
- {
- "roles/container.developer" = [module.vm-bastion.service_account_iam_email]
- "roles/owner" = var.owners_gke
- },
- var.cluster_create
- ? {
- "roles/logging.logWriter" = [module.cluster-1-nodepool-1.0.service_account_iam_email]
- "roles/monitoring.metricWriter" = [module.cluster-1-nodepool-1.0.service_account_iam_email]
- }
- : {}
- )
-}
-
-################################################################################
-# Networking #
-################################################################################
-
-# subnet IAM bindings control which identities can use the individual subnets
-
-module "vpc-shared" {
- source = "../../../modules/net-vpc"
- project_id = module.project-host.project_id
- name = "shared-vpc"
- subnets = [
- {
- ip_cidr_range = var.ip_ranges.gce
- name = "gce"
- region = var.region
- secondary_ip_range = {}
- },
- {
- ip_cidr_range = var.ip_ranges.gke
- name = "gke"
- region = var.region
- secondary_ip_range = {
- pods = var.ip_secondary_ranges.gke-pods
- services = var.ip_secondary_ranges.gke-services
- }
- }
- ]
- iam = {
- "${var.region}/gce" = {
- "roles/compute.networkUser" = concat(var.owners_gce, [
- "serviceAccount:${module.project-svc-gce.service_accounts.cloud_services}",
- ])
- }
- "${var.region}/gke" = {
- "roles/compute.networkUser" = concat(var.owners_gke, [
- "serviceAccount:${module.project-svc-gke.service_accounts.cloud_services}",
- "serviceAccount:${module.project-svc-gke.service_accounts.robots.container-engine}",
- ])
- "roles/compute.securityAdmin" = [
- "serviceAccount:${module.project-svc-gke.service_accounts.robots.container-engine}",
- ]
- }
- }
-}
-
-module "vpc-shared-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.project-host.project_id
- network = module.vpc-shared.name
- admin_ranges = values(var.ip_ranges)
-}
-
-module "nat" {
- source = "../../../modules/net-cloudnat"
- project_id = module.project-host.project_id
- region = var.region
- name = "vpc-shared"
- router_create = true
- router_network = module.vpc-shared.name
-}
-
-################################################################################
-# DNS #
-################################################################################
-
-module "host-dns" {
- source = "../../../modules/dns"
- project_id = module.project-host.project_id
- type = "private"
- name = "example"
- domain = "example.com."
- client_networks = [module.vpc-shared.self_link]
- recordsets = {
- "A localhost" = { ttl = 300, records = ["127.0.0.1"] }
- "A bastion" = { ttl = 300, records = [module.vm-bastion.internal_ip] }
- }
-}
-
-################################################################################
-# VM #
-################################################################################
-
-module "vm-bastion" {
- source = "../../../modules/compute-vm"
- project_id = module.project-svc-gce.project_id
- zone = "${var.region}-b"
- name = "bastion"
- network_interfaces = [{
- network = module.vpc-shared.self_link
- subnetwork = lookup(module.vpc-shared.subnet_self_links, "${var.region}/gce", null)
- nat = false
- addresses = null
- }]
- tags = ["ssh"]
- metadata = {
- startup-script = join("\n", [
- "#! /bin/bash",
- "apt-get update",
- "apt-get install -y bash-completion kubectl dnsutils tinyproxy",
- "grep -qxF 'Allow localhost' /etc/tinyproxy/tinyproxy.conf || echo 'Allow localhost' >> /etc/tinyproxy/tinyproxy.conf",
- "service tinyproxy restart"
- ])
- }
- service_account_create = true
-}
-
-################################################################################
-# GKE #
-################################################################################
-
-module "cluster-1" {
- source = "../../../modules/gke-cluster"
- count = var.cluster_create ? 1 : 0
- name = "cluster-1"
- project_id = module.project-svc-gke.project_id
- location = "${var.region}-b"
- network = module.vpc-shared.self_link
- subnetwork = module.vpc-shared.subnet_self_links["${var.region}/gke"]
- secondary_range_pods = "pods"
- secondary_range_services = "services"
- default_max_pods_per_node = 32
- labels = {
- environment = "test"
- }
- master_authorized_ranges = {
- internal-vms = var.ip_ranges.gce
- }
- private_cluster_config = {
- enable_private_nodes = true
- enable_private_endpoint = true
- master_ipv4_cidr_block = var.private_service_ranges.cluster-1
- master_global_access = true
- }
-}
-
-module "cluster-1-nodepool-1" {
- source = "../../../modules/gke-nodepool"
- count = var.cluster_create ? 1 : 0
- name = "nodepool-1"
- project_id = module.project-svc-gke.project_id
- location = module.cluster-1.0.location
- cluster_name = module.cluster-1.0.name
- node_service_account_create = true
-}
diff --git a/examples/networking/shared-vpc-gke/variables.tf b/examples/networking/shared-vpc-gke/variables.tf
deleted file mode 100644
index daa1d72de7..0000000000
--- a/examples/networking/shared-vpc-gke/variables.tf
+++ /dev/null
@@ -1,93 +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
-#
-# 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.
-
-variable "billing_account_id" {
- description = "Billing account id used as default for new projects."
- type = string
-}
-
-variable "cluster_create" {
- description = "Create GKE cluster and nodepool."
- type = bool
- default = true
-}
-
-variable "ip_ranges" {
- description = "Subnet IP CIDR ranges."
- type = map(string)
- default = {
- gce = "10.0.16.0/24"
- gke = "10.0.32.0/24"
- }
-}
-
-variable "ip_secondary_ranges" {
- description = "Secondary IP CIDR ranges."
- type = map(string)
- default = {
- gke-pods = "10.128.0.0/18"
- gke-services = "172.16.0.0/24"
- }
-}
-
-variable "owners_gce" {
- description = "GCE project owners, in IAM format."
- type = list(string)
- default = []
-}
-
-variable "owners_gke" {
- description = "GKE project owners, in IAM format."
- type = list(string)
- default = []
-}
-
-variable "owners_host" {
- description = "Host project owners, in IAM format."
- type = list(string)
- default = []
-}
-
-variable "prefix" {
- description = "Prefix used for resources that need unique names."
- type = string
-}
-
-variable "private_service_ranges" {
- description = "Private service IP CIDR ranges."
- type = map(string)
- default = {
- cluster-1 = "192.168.0.0/28"
- }
-}
-
-variable "project_services" {
- description = "Service APIs enabled by default in new projects."
- type = list(string)
- default = [
- "container.googleapis.com",
- "stackdriver.googleapis.com",
- ]
-}
-
-variable "region" {
- description = "Region used."
- type = string
- default = "europe-west1"
-}
-
-variable "root_node" {
- description = "Hierarchy node where projects will be created, 'organizations/org_id' or 'folders/folder_id'."
- type = string
-}
diff --git a/examples/networking/shared-vpc-gke/versions.tf b/examples/networking/shared-vpc-gke/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/networking/shared-vpc-gke/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/examples/serverless/api-gateway/README.md b/examples/serverless/api-gateway/README.md
deleted file mode 100644
index 3b5cad3816..0000000000
--- a/examples/serverless/api-gateway/README.md
+++ /dev/null
@@ -1,41 +0,0 @@
-# Creating multi-region deployments for API Gateway
-
-This tutorial shows you how to configure an HTTP(S) load balancer to enable multi-region deployments for API Gateway. For more details on how this set up work have a look at the article [here](https://cloud.google.com/api-gateway/docs/multi-region-deployment).
-
-The diagram below depicts the architecture that this example sets up.
-
-![Architecture](architecture.png)
-
-# Running the example
-
-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=examples%2Fserverless%2Fapi-gateway), then go through the following steps to create resources:
-
-* `terraform init`
-* `terraform apply -var project_id=my-project-id`
-
-## Testing the example
-
-1. Copy the IP address returned as output
-
-2. Execute the following command
-
- curl -v http://string
| ✓ | |
-| [regions](variables.tf#L31) | List of regions to deploy the proxy in. | list(string)
| ✓ | |
-| [project_create](variables.tf#L17) | Parameters for the creation of the new project. | object({…})
| | null
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [ip_address](outputs.tf#L17) | The reserved global IP address. | |
-
-
diff --git a/examples/serverless/api-gateway/function/package-lock.json b/examples/serverless/api-gateway/function/package-lock.json
deleted file mode 100644
index 6bf7ab5c18..0000000000
--- a/examples/serverless/api-gateway/function/package-lock.json
+++ /dev/null
@@ -1,2560 +0,0 @@
-{
- "name": "function",
- "version": "1.0.0",
- "lockfileVersion": 2,
- "requires": true,
- "packages": {
- "": {
- "name": "function",
- "version": "1.0.0",
- "license": "ISC",
- "dependencies": {
- "@google-cloud/functions-framework": "^3.0.0",
- "express": "^4.17.3"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/@babel/code-frame": {
- "version": "7.16.7",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz",
- "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==",
- "dependencies": {
- "@babel/highlight": "^7.16.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.16.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
- "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/highlight": {
- "version": "7.16.10",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz",
- "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.16.7",
- "chalk": "^2.0.0",
- "js-tokens": "^4.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@google-cloud/functions-framework": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-3.0.0.tgz",
- "integrity": "sha512-+K9+y39/5ig4QrbnaCM8BOzt4+Qx5SRMu2dj5QDTNFc5s8f/Lubty8u3aBQN6JC86M0NuHL9zIj8xs8Awj7C+w==",
- "dependencies": {
- "body-parser": "^1.18.3",
- "cloudevents": "^5.1.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/@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/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/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "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/array-flatten": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
- "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
- },
- "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/body-parser": {
- "version": "1.19.2",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz",
- "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==",
- "dependencies": {
- "bytes": "3.1.2",
- "content-type": "~1.0.4",
- "debug": "2.6.9",
- "depd": "~1.1.2",
- "http-errors": "1.8.1",
- "iconv-lite": "0.4.24",
- "on-finished": "~2.3.0",
- "qs": "6.9.7",
- "raw-body": "2.4.3",
- "type-is": "~1.6.18"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "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/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/cloudevents": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/cloudevents/-/cloudevents-5.3.2.tgz",
- "integrity": "sha512-ZjEFjx0BJnio8SED1TzD7GHA118zCk04Mz6aDMMii+4/ZvX5LPgn1D4lT5Jj7HodCbdeRS6dX88unH06Qc3mkA==",
- "dependencies": {
- "ajv": "~6.12.3",
- "util": "^0.12.4",
- "uuid": "~8.3.0"
- }
- },
- "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/color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
- },
- "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.4.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
- "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
- "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": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
- },
- "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/define-properties": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
- "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
- "dependencies": {
- "object-keys": "^1.0.12"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/depd": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
- "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/destroy": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
- "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
- },
- "node_modules/ee-first": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
- },
- "node_modules/encodeurl": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "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/es-abstract": {
- "version": "1.19.1",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz",
- "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==",
- "dependencies": {
- "call-bind": "^1.0.2",
- "es-to-primitive": "^1.2.1",
- "function-bind": "^1.1.1",
- "get-intrinsic": "^1.1.1",
- "get-symbol-description": "^1.0.0",
- "has": "^1.0.3",
- "has-symbols": "^1.0.2",
- "internal-slot": "^1.0.3",
- "is-callable": "^1.2.4",
- "is-negative-zero": "^2.0.1",
- "is-regex": "^1.1.4",
- "is-shared-array-buffer": "^1.0.1",
- "is-string": "^1.0.7",
- "is-weakref": "^1.0.1",
- "object-inspect": "^1.11.0",
- "object-keys": "^1.1.1",
- "object.assign": "^4.1.2",
- "string.prototype.trimend": "^1.0.4",
- "string.prototype.trimstart": "^1.0.4",
- "unbox-primitive": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/es-to-primitive": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
- "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
- "dependencies": {
- "is-callable": "^1.1.4",
- "is-date-object": "^1.0.1",
- "is-symbol": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
- },
- "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": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
- "engines": {
- "node": ">=0.8.0"
- }
- },
- "node_modules/etag": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
- "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/express": {
- "version": "4.17.3",
- "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz",
- "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==",
- "dependencies": {
- "accepts": "~1.3.8",
- "array-flatten": "1.1.1",
- "body-parser": "1.19.2",
- "content-disposition": "0.5.4",
- "content-type": "~1.0.4",
- "cookie": "0.4.2",
- "cookie-signature": "1.0.6",
- "debug": "2.6.9",
- "depd": "~1.1.2",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "finalhandler": "~1.1.2",
- "fresh": "0.5.2",
- "merge-descriptors": "1.0.1",
- "methods": "~1.1.2",
- "on-finished": "~2.3.0",
- "parseurl": "~1.3.3",
- "path-to-regexp": "0.1.7",
- "proxy-addr": "~2.0.7",
- "qs": "6.9.7",
- "range-parser": "~1.2.1",
- "safe-buffer": "5.2.1",
- "send": "0.17.2",
- "serve-static": "1.14.2",
- "setprototypeof": "1.2.0",
- "statuses": "~1.5.0",
- "type-is": "~1.6.18",
- "utils-merge": "1.0.1",
- "vary": "~1.1.2"
- },
- "engines": {
- "node": ">= 0.10.0"
- }
- },
- "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-json-stable-stringify": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
- },
- "node_modules/finalhandler": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
- "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
- "dependencies": {
- "debug": "2.6.9",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "on-finished": "~2.3.0",
- "parseurl": "~1.3.3",
- "statuses": "~1.5.0",
- "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/foreach": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
- "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k="
- },
- "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": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "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/get-intrinsic": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
- "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
- "dependencies": {
- "function-bind": "^1.1.1",
- "has": "^1.0.3",
- "has-symbols": "^1.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-symbol-description": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
- "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
- "dependencies": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "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-bigints": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
- "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/has-symbols": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
- "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
- "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": "1.8.1",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
- "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
- "dependencies": {
- "depd": "~1.1.2",
- "inherits": "2.0.4",
- "setprototypeof": "1.2.0",
- "statuses": ">= 1.5.0 < 2",
- "toidentifier": "1.0.1"
- },
- "engines": {
- "node": ">= 0.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/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
- },
- "node_modules/internal-slot": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
- "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
- "dependencies": {
- "get-intrinsic": "^1.1.0",
- "has": "^1.0.3",
- "side-channel": "^1.0.4"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "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": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
- },
- "node_modules/is-bigint": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
- "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
- "dependencies": {
- "has-bigints": "^1.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-boolean-object": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
- "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
- "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-callable": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
- "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-core-module": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
- "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
- "dependencies": {
- "has": "^1.0.3"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-date-object": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
- "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
- "dependencies": {
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "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-negative-zero": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
- "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-number-object": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz",
- "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==",
- "dependencies": {
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-regex": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
- "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
- "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-shared-array-buffer": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz",
- "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-string": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
- "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
- "dependencies": {
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-symbol": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
- "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
- "dependencies": {
- "has-symbols": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-typed-array": {
- "version": "1.1.8",
- "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.8.tgz",
- "integrity": "sha512-HqH41TNZq2fgtGT8WHVFVJhBVGuY3AnP3Q36K8JKXUxSxRgk/d+7NjmwG2vo2mYmXK8UYZKu0qH8bVP5gEisjA==",
- "dependencies": {
- "available-typed-arrays": "^1.0.5",
- "call-bind": "^1.0.2",
- "es-abstract": "^1.18.5",
- "foreach": "^2.0.5",
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-weakref": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
- "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
- "dependencies": {
- "call-bind": "^1.0.2"
- },
- "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/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": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
- },
- "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/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/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/media-typer": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
- "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
- "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": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
- },
- "node_modules/methods": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
- "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
- "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.51.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
- "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "2.1.34",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
- "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
- "dependencies": {
- "mime-db": "1.51.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/minimist": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
- "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
- },
- "node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
- },
- "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/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-inspect": {
- "version": "1.12.0",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
- "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/object-keys": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
- "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/object.assign": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
- "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
- "dependencies": {
- "call-bind": "^1.0.0",
- "define-properties": "^1.1.3",
- "has-symbols": "^1.0.1",
- "object-keys": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/on-finished": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
- "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
- "dependencies": {
- "ee-first": "1.1.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "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-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": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
- },
- "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/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.9.7",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz",
- "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==",
- "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.4.3",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz",
- "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==",
- "dependencies": {
- "bytes": "3.1.2",
- "http-errors": "1.8.1",
- "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/resolve": {
- "version": "1.22.0",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
- "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
- "dependencies": {
- "is-core-module": "^2.8.1",
- "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/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/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.5",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
- "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/send": {
- "version": "0.17.2",
- "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
- "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
- "dependencies": {
- "debug": "2.6.9",
- "depd": "~1.1.2",
- "destroy": "~1.0.4",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "fresh": "0.5.2",
- "http-errors": "1.8.1",
- "mime": "1.6.0",
- "ms": "2.1.3",
- "on-finished": "~2.3.0",
- "range-parser": "~1.2.1",
- "statuses": "~1.5.0"
- },
- "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.14.2",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
- "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==",
- "dependencies": {
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "parseurl": "~1.3.3",
- "send": "0.17.2"
- },
- "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/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.11",
- "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz",
- "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g=="
- },
- "node_modules/statuses": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
- "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/string.prototype.trimend": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
- "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/string.prototype.trimstart": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
- "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "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/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/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/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/unbox-primitive": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
- "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==",
- "dependencies": {
- "function-bind": "^1.1.1",
- "has-bigints": "^1.0.1",
- "has-symbols": "^1.0.2",
- "which-boxed-primitive": "^1.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/unpipe": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
- "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
- "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.4",
- "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz",
- "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==",
- "dependencies": {
- "inherits": "^2.0.3",
- "is-arguments": "^1.0.4",
- "is-generator-function": "^1.0.7",
- "is-typed-array": "^1.1.3",
- "safe-buffer": "^5.1.2",
- "which-typed-array": "^1.1.2"
- }
- },
- "node_modules/utils-merge": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
- "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
- "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": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/which-boxed-primitive": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
- "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
- "dependencies": {
- "is-bigint": "^1.0.1",
- "is-boolean-object": "^1.1.0",
- "is-number-object": "^1.0.4",
- "is-string": "^1.0.5",
- "is-symbol": "^1.0.3"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/which-typed-array": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.7.tgz",
- "integrity": "sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw==",
- "dependencies": {
- "available-typed-arrays": "^1.0.5",
- "call-bind": "^1.0.2",
- "es-abstract": "^1.18.5",
- "foreach": "^2.0.5",
- "has-tostringtag": "^1.0.0",
- "is-typed-array": "^1.1.7"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
- }
- },
- "dependencies": {
- "@babel/code-frame": {
- "version": "7.16.7",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz",
- "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==",
- "requires": {
- "@babel/highlight": "^7.16.7"
- }
- },
- "@babel/helper-validator-identifier": {
- "version": "7.16.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
- "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw=="
- },
- "@babel/highlight": {
- "version": "7.16.10",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz",
- "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==",
- "requires": {
- "@babel/helper-validator-identifier": "^7.16.7",
- "chalk": "^2.0.0",
- "js-tokens": "^4.0.0"
- }
- },
- "@google-cloud/functions-framework": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-3.0.0.tgz",
- "integrity": "sha512-+K9+y39/5ig4QrbnaCM8BOzt4+Qx5SRMu2dj5QDTNFc5s8f/Lubty8u3aBQN6JC86M0NuHL9zIj8xs8Awj7C+w==",
- "requires": {
- "body-parser": "^1.18.3",
- "cloudevents": "^5.1.0",
- "express": "^4.16.4",
- "minimist": "^1.2.5",
- "on-finished": "^2.3.0",
- "read-pkg-up": "^7.0.1",
- "semver": "^7.3.5"
- }
- },
- "@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=="
- },
- "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"
- }
- },
- "ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "requires": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- }
- },
- "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"
- }
- },
- "array-flatten": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
- "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
- },
- "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=="
- },
- "body-parser": {
- "version": "1.19.2",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz",
- "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==",
- "requires": {
- "bytes": "3.1.2",
- "content-type": "~1.0.4",
- "debug": "2.6.9",
- "depd": "~1.1.2",
- "http-errors": "1.8.1",
- "iconv-lite": "0.4.24",
- "on-finished": "~2.3.0",
- "qs": "6.9.7",
- "raw-body": "2.4.3",
- "type-is": "~1.6.18"
- }
- },
- "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"
- }
- },
- "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"
- }
- },
- "cloudevents": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/cloudevents/-/cloudevents-5.3.2.tgz",
- "integrity": "sha512-ZjEFjx0BJnio8SED1TzD7GHA118zCk04Mz6aDMMii+4/ZvX5LPgn1D4lT5Jj7HodCbdeRS6dX88unH06Qc3mkA==",
- "requires": {
- "ajv": "~6.12.3",
- "util": "^0.12.4",
- "uuid": "~8.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": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
- },
- "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.4.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
- "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="
- },
- "cookie-signature": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
- "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
- },
- "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"
- }
- },
- "define-properties": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
- "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
- "requires": {
- "object-keys": "^1.0.12"
- }
- },
- "depd": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
- "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
- },
- "destroy": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
- "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
- },
- "ee-first": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
- },
- "encodeurl": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
- },
- "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"
- }
- },
- "es-abstract": {
- "version": "1.19.1",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz",
- "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==",
- "requires": {
- "call-bind": "^1.0.2",
- "es-to-primitive": "^1.2.1",
- "function-bind": "^1.1.1",
- "get-intrinsic": "^1.1.1",
- "get-symbol-description": "^1.0.0",
- "has": "^1.0.3",
- "has-symbols": "^1.0.2",
- "internal-slot": "^1.0.3",
- "is-callable": "^1.2.4",
- "is-negative-zero": "^2.0.1",
- "is-regex": "^1.1.4",
- "is-shared-array-buffer": "^1.0.1",
- "is-string": "^1.0.7",
- "is-weakref": "^1.0.1",
- "object-inspect": "^1.11.0",
- "object-keys": "^1.1.1",
- "object.assign": "^4.1.2",
- "string.prototype.trimend": "^1.0.4",
- "string.prototype.trimstart": "^1.0.4",
- "unbox-primitive": "^1.0.1"
- }
- },
- "es-to-primitive": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
- "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
- "requires": {
- "is-callable": "^1.1.4",
- "is-date-object": "^1.0.1",
- "is-symbol": "^1.0.2"
- }
- },
- "escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
- },
- "escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
- },
- "etag": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
- "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
- },
- "express": {
- "version": "4.17.3",
- "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz",
- "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==",
- "requires": {
- "accepts": "~1.3.8",
- "array-flatten": "1.1.1",
- "body-parser": "1.19.2",
- "content-disposition": "0.5.4",
- "content-type": "~1.0.4",
- "cookie": "0.4.2",
- "cookie-signature": "1.0.6",
- "debug": "2.6.9",
- "depd": "~1.1.2",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "finalhandler": "~1.1.2",
- "fresh": "0.5.2",
- "merge-descriptors": "1.0.1",
- "methods": "~1.1.2",
- "on-finished": "~2.3.0",
- "parseurl": "~1.3.3",
- "path-to-regexp": "0.1.7",
- "proxy-addr": "~2.0.7",
- "qs": "6.9.7",
- "range-parser": "~1.2.1",
- "safe-buffer": "5.2.1",
- "send": "0.17.2",
- "serve-static": "1.14.2",
- "setprototypeof": "1.2.0",
- "statuses": "~1.5.0",
- "type-is": "~1.6.18",
- "utils-merge": "1.0.1",
- "vary": "~1.1.2"
- }
- },
- "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-json-stable-stringify": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
- },
- "finalhandler": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
- "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
- "requires": {
- "debug": "2.6.9",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "on-finished": "~2.3.0",
- "parseurl": "~1.3.3",
- "statuses": "~1.5.0",
- "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"
- }
- },
- "foreach": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
- "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k="
- },
- "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": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
- },
- "function-bind": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
- },
- "get-intrinsic": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
- "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
- "requires": {
- "function-bind": "^1.1.1",
- "has": "^1.0.3",
- "has-symbols": "^1.0.1"
- }
- },
- "get-symbol-description": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
- "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
- "requires": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.1.1"
- }
- },
- "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-bigints": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
- "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA=="
- },
- "has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
- },
- "has-symbols": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
- "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
- },
- "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": "1.8.1",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
- "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
- "requires": {
- "depd": "~1.1.2",
- "inherits": "2.0.4",
- "setprototypeof": "1.2.0",
- "statuses": ">= 1.5.0 < 2",
- "toidentifier": "1.0.1"
- }
- },
- "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"
- }
- },
- "inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
- },
- "internal-slot": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
- "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
- "requires": {
- "get-intrinsic": "^1.1.0",
- "has": "^1.0.3",
- "side-channel": "^1.0.4"
- }
- },
- "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": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
- },
- "is-bigint": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
- "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
- "requires": {
- "has-bigints": "^1.0.1"
- }
- },
- "is-boolean-object": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
- "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
- "requires": {
- "call-bind": "^1.0.2",
- "has-tostringtag": "^1.0.0"
- }
- },
- "is-callable": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
- "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w=="
- },
- "is-core-module": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
- "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
- "requires": {
- "has": "^1.0.3"
- }
- },
- "is-date-object": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
- "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
- "requires": {
- "has-tostringtag": "^1.0.0"
- }
- },
- "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-negative-zero": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
- "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA=="
- },
- "is-number-object": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz",
- "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==",
- "requires": {
- "has-tostringtag": "^1.0.0"
- }
- },
- "is-regex": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
- "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
- "requires": {
- "call-bind": "^1.0.2",
- "has-tostringtag": "^1.0.0"
- }
- },
- "is-shared-array-buffer": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz",
- "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA=="
- },
- "is-string": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
- "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
- "requires": {
- "has-tostringtag": "^1.0.0"
- }
- },
- "is-symbol": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
- "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
- "requires": {
- "has-symbols": "^1.0.2"
- }
- },
- "is-typed-array": {
- "version": "1.1.8",
- "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.8.tgz",
- "integrity": "sha512-HqH41TNZq2fgtGT8WHVFVJhBVGuY3AnP3Q36K8JKXUxSxRgk/d+7NjmwG2vo2mYmXK8UYZKu0qH8bVP5gEisjA==",
- "requires": {
- "available-typed-arrays": "^1.0.5",
- "call-bind": "^1.0.2",
- "es-abstract": "^1.18.5",
- "foreach": "^2.0.5",
- "has-tostringtag": "^1.0.0"
- }
- },
- "is-weakref": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
- "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
- "requires": {
- "call-bind": "^1.0.2"
- }
- },
- "js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
- },
- "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": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
- },
- "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=="
- },
- "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"
- }
- },
- "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"
- }
- },
- "media-typer": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
- "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
- },
- "merge-descriptors": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
- "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
- },
- "methods": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
- "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
- },
- "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.51.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
- "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="
- },
- "mime-types": {
- "version": "2.1.34",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
- "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
- "requires": {
- "mime-db": "1.51.0"
- }
- },
- "minimist": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
- "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
- },
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
- },
- "negotiator": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
- "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
- },
- "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-inspect": {
- "version": "1.12.0",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
- "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g=="
- },
- "object-keys": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
- "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
- },
- "object.assign": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
- "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
- "requires": {
- "call-bind": "^1.0.0",
- "define-properties": "^1.1.3",
- "has-symbols": "^1.0.1",
- "object-keys": "^1.1.1"
- }
- },
- "on-finished": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
- "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
- "requires": {
- "ee-first": "1.1.1"
- }
- },
- "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-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": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
- },
- "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"
- }
- },
- "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.9.7",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz",
- "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw=="
- },
- "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.4.3",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz",
- "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==",
- "requires": {
- "bytes": "3.1.2",
- "http-errors": "1.8.1",
- "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"
- }
- },
- "resolve": {
- "version": "1.22.0",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
- "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
- "requires": {
- "is-core-module": "^2.8.1",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- }
- },
- "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=="
- },
- "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.5",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
- "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
- "requires": {
- "lru-cache": "^6.0.0"
- }
- },
- "send": {
- "version": "0.17.2",
- "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
- "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
- "requires": {
- "debug": "2.6.9",
- "depd": "~1.1.2",
- "destroy": "~1.0.4",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "fresh": "0.5.2",
- "http-errors": "1.8.1",
- "mime": "1.6.0",
- "ms": "2.1.3",
- "on-finished": "~2.3.0",
- "range-parser": "~1.2.1",
- "statuses": "~1.5.0"
- },
- "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.14.2",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
- "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==",
- "requires": {
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "parseurl": "~1.3.3",
- "send": "0.17.2"
- }
- },
- "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"
- }
- },
- "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.11",
- "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz",
- "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g=="
- },
- "statuses": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
- "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
- },
- "string.prototype.trimend": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
- "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3"
- }
- },
- "string.prototype.trimstart": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
- "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3"
- }
- },
- "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"
- }
- },
- "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=="
- },
- "toidentifier": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
- "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
- },
- "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"
- }
- },
- "unbox-primitive": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
- "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==",
- "requires": {
- "function-bind": "^1.1.1",
- "has-bigints": "^1.0.1",
- "has-symbols": "^1.0.2",
- "which-boxed-primitive": "^1.0.2"
- }
- },
- "unpipe": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
- "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
- },
- "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.4",
- "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz",
- "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==",
- "requires": {
- "inherits": "^2.0.3",
- "is-arguments": "^1.0.4",
- "is-generator-function": "^1.0.7",
- "is-typed-array": "^1.1.3",
- "safe-buffer": "^5.1.2",
- "which-typed-array": "^1.1.2"
- }
- },
- "utils-merge": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
- "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
- },
- "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": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
- },
- "which-boxed-primitive": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
- "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
- "requires": {
- "is-bigint": "^1.0.1",
- "is-boolean-object": "^1.1.0",
- "is-number-object": "^1.0.4",
- "is-string": "^1.0.5",
- "is-symbol": "^1.0.3"
- }
- },
- "which-typed-array": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.7.tgz",
- "integrity": "sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw==",
- "requires": {
- "available-typed-arrays": "^1.0.5",
- "call-bind": "^1.0.2",
- "es-abstract": "^1.18.5",
- "foreach": "^2.0.5",
- "has-tostringtag": "^1.0.0",
- "is-typed-array": "^1.1.7"
- }
- },
- "yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
- }
- }
-}
diff --git a/examples/serverless/api-gateway/main.tf b/examples/serverless/api-gateway/main.tf
deleted file mode 100644
index 664ee990f4..0000000000
--- a/examples/serverless/api-gateway/main.tf
+++ /dev/null
@@ -1,145 +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 {
- api_id_prefix = "api"
- function_name_prefix = "cf-hello"
- specs = { for region in var.regions : region =>
- templatefile("${path.module}/spec.yaml", {
- api_id = "${local.api_id_prefix}-${region}"
- function_name = "${local.function_name_prefix}-${region}"
- region = region
- project_id = var.project_id
- })
- }
- backends = [for region in var.regions : {
- group = google_compute_region_network_endpoint_group.serverless-negs[region].id
- options = null
- }
- ]
-}
-
-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 = [
- "apigateway.googleapis.com",
- "cloudbuild.googleapis.com",
- "cloudfunctions.googleapis.com",
- "compute.googleapis.com",
- "servicemanagement.googleapis.com",
- "servicecontrol.googleapis.com"
- ]
- project_create = var.project_create != null
-}
-
-module "sa" {
- source = "../../../modules/iam-service-account"
- project_id = module.project.project_id
- name = "sa-api"
-}
-
-
-module "functions" {
- for_each = toset(var.regions)
- source = "../../../modules/cloud-function"
- project_id = module.project.project_id
- name = "${local.function_name_prefix}-${each.value}"
- bucket_name = "bkt-${module.project.project_id}-${each.value}"
- region = each.value
- ingress_settings = "ALLOW_ALL"
- bucket_config = {
- location = null
- lifecycle_delete_age = 1
- }
- bundle_config = {
- source_dir = "${path.module}/function"
- output_path = "${path.module}/bundle.zip"
- excludes = null
- }
- function_config = {
- entry_point = "helloGET"
- instances = null
- memory = null
- runtime = "nodejs16"
- timeout = null
- }
- service_account_create = true
- iam = {
- "roles/cloudfunctions.invoker" = [module.sa.iam_email]
- }
-}
-
-module "gateways" {
- for_each = toset(var.regions)
- source = "../../../modules/api-gateway"
- project_id = module.project.project_id
- api_id = "${local.api_id_prefix}-${each.value}"
- region = each.value
- spec = local.specs[each.value]
- service_account_email = module.sa.email
-}
-
-module "glb" {
- source = "../../../modules/net-glb"
- name = "glb"
- project_id = module.project.project_id
- # This is important as serverless backends require no HCs
- health_checks_config_defaults = null
- reserve_ip_address = true
- backend_services_config = {
- serverless-backend = {
- bucket_config = null
- enable_cdn = false
- cdn_config = null
- group_config = {
- backends = [for region in var.regions : {
- group = google_compute_region_network_endpoint_group.serverless-negs[region].id
- options = null
- }
- ],
- health_checks = []
- log_config = null
- options = null
- }
- }
- }
-}
-
-resource "google_compute_region_network_endpoint_group" "serverless-negs" {
- for_each = toset(var.regions)
- provider = google-beta
- name = "serverless-neg-${module.gateways[each.value].gateway_id}"
- project = module.project.project_id
- network_endpoint_type = "SERVERLESS"
- region = each.value
- serverless_deployment {
- platform = "apigateway.googleapis.com"
- resource = module.gateways[each.value].gateway_id
- url_mask = ""
- }
- lifecycle {
- create_before_destroy = true
- }
-}
diff --git a/examples/serverless/api-gateway/outputs.tf b/examples/serverless/api-gateway/outputs.tf
deleted file mode 100644
index 9cc48f5bdb..0000000000
--- a/examples/serverless/api-gateway/outputs.tf
+++ /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.
- */
-
-output "ip_address" {
- description = "The reserved global IP address."
- value = module.glb.ip_address
-}
diff --git a/examples/third-party-solutions/README.md b/examples/third-party-solutions/README.md
deleted file mode 100644
index b3e2d5f610..0000000000
--- a/examples/third-party-solutions/README.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# Third Party Solutions
-
-The examples in this folder show how to automate installation of specific third party products on GCP, following typical best practices.
-
-## Examples
-
-### OpenShift cluster bootstrap on Shared VPC
-
- This [example](./openshift/) shows how to quickly bootstrap an OpenShift 4.7 cluster on GCP, using typical enterprise features like Shared VPC and CMEK for instance disks.
diff --git a/examples/third-party-solutions/openshift/tf/README.md b/examples/third-party-solutions/openshift/tf/README.md
deleted file mode 100644
index 9ff776f532..0000000000
--- a/examples/third-party-solutions/openshift/tf/README.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# OpenShift Cluster Bootstrap
-
-This example is a companion setup to the Python script in the parent folder, and is used to bootstrap OpenShift clusters on GCP. Refer to the documentation in the parent folder for usage instructions.
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [cluster_name](variables.tf#L23) | Name used for the cluster and DNS zone. | string
| ✓ | |
-| [domain](variables.tf#L28) | Domain name used to derive the DNS zone. | string
| ✓ | |
-| [fs_paths](variables.tf#L87) | Filesystem paths for commands and data, supports home path expansion. | object({…})
| ✓ | |
-| [host_project](variables.tf#L44) | Shared VPC project and network configuration. | object({…})
| ✓ | |
-| [service_project](variables.tf#L124) | Service project configuration. | object({…})
| ✓ | |
-| [allowed_ranges](variables.tf#L17) | Ranges that can SSH to the boostrap VM and API endpoint. | list(any)
| | ["10.0.0.0/8"]
|
-| [disk_encryption_key](variables.tf#L33) | Optional CMEK for disk encryption. | object({…})
| | null
|
-| [install_config_params](variables.tf#L57) | OpenShift cluster configuration. | object({…})
| | {…}
|
-| [post_bootstrap_config](variables.tf#L102) | Name of the service account for the machine operator. Removes bootstrap resources when set. | object({…})
| | null
|
-| [region](variables.tf#L110) | Region where resources will be created. | string
| | "europe-west1"
|
-| [rhcos_gcp_image](variables.tf#L116) | RHCOS image used. | string
| | "projects/rhcos-cloud/global/images/rhcos-47-83-202102090044-0-gcp-x86-64"
|
-| [tags](variables.tf#L131) | Additional tags for instances. | list(string)
| | ["ssh"]
|
-| [zones](variables.tf#L137) | Zones used for instances. | list(string)
| | ["b", "c", "d"]
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [backend-health](outputs.tf#L17) | Command to monitor API internal backend health. | |
-| [bootstrap-ssh](outputs.tf#L27) | Command to SSH to the bootstrap instance. | |
-| [masters-ssh](outputs.tf#L37) | Command to SSH to the master instances. | |
-
-
diff --git a/examples/third-party-solutions/openshift/tf/variables.tf b/examples/third-party-solutions/openshift/tf/variables.tf
deleted file mode 100644
index 3017403e36..0000000000
--- a/examples/third-party-solutions/openshift/tf/variables.tf
+++ /dev/null
@@ -1,141 +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 "allowed_ranges" {
- description = "Ranges that can SSH to the boostrap VM and API endpoint."
- type = list(any)
- default = ["10.0.0.0/8"]
-}
-
-variable "cluster_name" {
- description = "Name used for the cluster and DNS zone."
- type = string
-}
-
-variable "domain" {
- description = "Domain name used to derive the DNS zone."
- type = string
-}
-
-variable "disk_encryption_key" {
- description = "Optional CMEK for disk encryption."
- type = object({
- keyring = string
- location = string
- name = string
- project_id = string
- })
- default = null
-}
-
-variable "host_project" {
- description = "Shared VPC project and network configuration."
- type = object({
- default_subnet_name = string
- masters_subnet_name = string
- project_id = string
- vpc_name = string
- workers_subnet_name = string
- })
-}
-
-# https://github.com/openshift/installer/blob/master/docs/user/customization.md
-
-variable "install_config_params" {
- description = "OpenShift cluster configuration."
- type = object({
- disk_size = number
- labels = map(string)
- network = object({
- cluster = string
- host_prefix = number
- machine = string
- service = string
- })
- proxy = object({
- http = string
- https = string
- noproxy = string
- })
- })
- default = {
- disk_size = 16
- labels = {}
- network = {
- cluster = "10.128.0.0/14"
- host_prefix = 23
- machine = "10.0.0.0/16"
- service = "172.30.0.0/16"
- }
- proxy = null
- }
-}
-
-variable "fs_paths" {
- description = "Filesystem paths for commands and data, supports home path expansion."
- type = object({
- credentials = string
- config_dir = string
- openshift_install = string
- pull_secret = string
- ssh_key = string
- })
-}
-
-# oc -n openshift-cloud-credential-operator get CredentialsRequest \
-# openshift-machine-api-gcp \
-# -o jsonpath='{.status.providerStatus.serviceAccountID}{"\n"}'
-
-variable "post_bootstrap_config" {
- description = "Name of the service account for the machine operator. Removes bootstrap resources when set."
- type = object({
- machine_op_sa_prefix = string
- })
- default = null
-}
-
-variable "region" {
- description = "Region where resources will be created."
- type = string
- default = "europe-west1"
-}
-
-variable "rhcos_gcp_image" {
- description = "RHCOS image used."
- type = string
- # okd
- # default = "projects/fedora-coreos-cloud/global/images/fedora-coreos-33-20210217-3-0-gcp-x86-64"
- default = "projects/rhcos-cloud/global/images/rhcos-47-83-202102090044-0-gcp-x86-64"
-}
-
-variable "service_project" {
- description = "Service project configuration."
- type = object({
- project_id = string
- })
-}
-
-variable "tags" {
- description = "Additional tags for instances."
- type = list(string)
- default = ["ssh"]
-}
-
-variable "zones" {
- description = "Zones used for instances."
- type = list(string)
- default = ["b", "c", "d"]
-}
diff --git a/examples/third-party-solutions/openshift/tf/versions.tf b/examples/third-party-solutions/openshift/tf/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/examples/third-party-solutions/openshift/tf/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/fast/README.md b/fast/README.md
index de860a4515..e35a483728 100644
--- a/fast/README.md
+++ b/fast/README.md
@@ -20,6 +20,8 @@ From the perspective of FAST's overall design, stages also work as contacts or i
+Please refer to the [stages](./stages/) section for further details on each stage.
+
### Security-first design
Security was, from the beginning, one of the most critical elements in the design of Fabric FAST. Many of FAST's design decisions aim to build the foundations of a secure organization. In fact, the first two stages deal mainly with the organization-wide security setup.
@@ -32,10 +34,9 @@ A resource factory consumes a simple representation of a resource (e.g., in YAML
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.
-## Stages and high level design
+### CI/CD
-As mentioned before, fast relies on multiple stages to progressively bring up your GCP organization(s).
-Please refer to the [stages](./stages/) section for further details.
+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.
## Implementation
@@ -60,5 +61,5 @@ Besides the features already described, FAST roadmap 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
+- Reference implementation to use FAST in CI/CD pipelines (in progress)
- Static policy enforcement
diff --git a/fast/assets/schemas/firewall_rules.schema.yaml b/fast/assets/schemas/firewall_rules.schema.yaml
index 1fd96caf17..6f8a8054dd 100644
--- a/fast/assets/schemas/firewall_rules.schema.yaml
+++ b/fast/assets/schemas/firewall_rules.schema.yaml
@@ -12,18 +12,23 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-map(include('firewall_rule'))
+egress: map(include('firewall_rule'), required=False)
+ingress: map(include('firewall_rule'), required=False)
---
firewall_rule:
- description: str()
- direction: enum("INGRESS", "EGRESS")
- action: enum("allow", "deny")
- sources: list(str())
- ranges: list(str())
- targets: list(str())
- use_service_accounts: bool()
- rules: list(include('rule'))
+ deny: bool(required=False)
+ description: str(required=False)
+ destination_ranges: list(str(), required=False)
+ disabled: bool(required=False)
+ # enable_logging:
+ # include_metadata: bool(required=False)
+ priority: int(required=False)
+ source_ranges: list(str(), required=False)
+ sources: list(str(), required=False)
+ targets: list(str(), required=False)
+ use_service_accounts: bool(required=False)
+ rules: list(include('rule'), required=False)
---
rule:
- protocol: enum("tcp", "udp", "all")
+ protocol: str()
ports: list(num())
diff --git a/fast/assets/schemas/project.schema.yaml b/fast/assets/schemas/project.schema.yaml
index 49e4bf8956..2155c71a29 100644
--- a/fast/assets/schemas/project.schema.yaml
+++ b/fast/assets/schemas/project.schema.yaml
@@ -24,6 +24,7 @@ labels: map(str(), key=str(), required=False)
org_policies: include('org_policies', required=False)
secrets: map(list(str()), key=str(), required=False)
service_accounts: map(list(str()), required=False)
+service_accounts_iam: map(map(list(str())), required=False)
services: list(str(matches='^[a-z-]*\.googleapis\.com$'), required=False)
service_identities_iam: map(list(str()), key=str(), required=False)
vpc: include('vpc', required=False)
diff --git a/fast/assets/schemas/subnet.schema.yaml b/fast/assets/schemas/subnet.schema.yaml
index add0d74b9c..c928a1b90e 100644
--- a/fast/assets/schemas/subnet.schema.yaml
+++ b/fast/assets/schemas/subnet.schema.yaml
@@ -16,14 +16,16 @@ region: str()
description: str()
ip_cidr_range: str()
# optional attributes
-private_ip_google_access: bool(required=False) # defaults to true
+enable_private_access: bool(required=False) # defaults to true
iam_users: list(str(), required=False)
iam_groups: list(str(), required=False)
iam_service_accounts: list(str(), required=False)
-secondary_ip_ranges: list(map(str()), key=str(), required=False)
+secondary_ip_ranges: map(str(), key=str(), required=False)
flow_logs: any(include('flow_logs'), required=False)
---
flow_logs:
- aggregation_interval: enum('INTERVAL_5_SEC', 'INTERVAL_30_SEC', 'INTERVAL_1_MIN', 'INTERVAL_5_MIN', 'INTERVAL_10_MIN', 'INTERVAL_15_MIN', required=False)
+ - filter_expression: str()
- flow_sampling: num(min=0, max=1, required=False)
- metadata: enum('EXCLUDE_ALL_METADATA', 'INCLUDE_ALL_METADATA', 'CUSTOM_METADATA', required=False)
+ - metadata_fields: map(str(), key=str(), required=False)
diff --git a/fast/assets/templates/providers.tpl b/fast/assets/templates/providers.tf.tpl
similarity index 100%
rename from fast/assets/templates/providers.tpl
rename to fast/assets/templates/providers.tf.tpl
diff --git a/fast/assets/templates/workflow-github.yaml b/fast/assets/templates/workflow-github.yaml
new file mode 100644
index 0000000000..8a946d84c8
--- /dev/null
+++ b/fast/assets/templates/workflow-github.yaml
@@ -0,0 +1,186 @@
+# 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
+ run: |
+ terraform init -no-color
+
+ - id: tf-validate
+ 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 }}\`
+
+ + +
+ +## 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: + +```tfvars +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/00-cicd-github/cicd-versions.tf b/fast/extras/00-cicd-github/cicd-versions.tf
new file mode 100644
index 0000000000..09f544cba0
--- /dev/null
+++ b/fast/extras/00-cicd-github/cicd-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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 Provider version.
+
+terraform {
+ required_version = ">= 1.3.1"
+ required_providers {
+ github = {
+ source = "integrations/github"
+ version = "~> 4.0"
+ }
+ }
+}
+
+
diff --git a/fast/extras/00-cicd-github/github_token.png b/fast/extras/00-cicd-github/github_token.png
new file mode 100644
index 0000000000..02426484ce
Binary files /dev/null and b/fast/extras/00-cicd-github/github_token.png differ
diff --git a/fast/extras/00-cicd-github/main.tf b/fast/extras/00-cicd-github/main.tf
new file mode 100644
index 0000000000..ac6028c17e
--- /dev/null
+++ b/fast/extras/00-cicd-github/main.tf
@@ -0,0 +1,141 @@
+/**
+ * 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 {
+ _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(
+ [for f in fileset(path.module, "${v.populate_from}/*.md") : f],
+ [for f in fileset(path.module, "${v.populate_from}/*.tf") : f]
+ ) : {
+ repository = k
+ file = f
+ name = replace(f, "${v.populate_from}/", "")
+ }
+ ] 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
+ )
+ repositories = {
+ for k, v in var.repositories :
+ k => v.create_options == null ? k : github_repository.default[k].name
+ }
+ repository_files = merge(
+ {
+ for k in local._repository_files :
+ "${k.repository}/${k.name}" => k
+ if !endswith(k.name, ".tf") || (
+ !startswith(k.name, "0") && k.name != "globals.tf"
+ )
+ },
+ {
+ for k, v in var.repositories :
+ "${k}/templates/providers.tf.tpl" => {
+ repository = k
+ file = "../../assets/templates/providers.tf.tpl"
+ name = "templates/providers.tf.tpl"
+ }
+ if v.populate_from != null
+ }
+ )
+}
+
+resource "github_repository" "default" {
+ for_each = {
+ for k, v in var.repositories : k => v if v.create_options != null
+ }
+ name = each.key
+ description = (
+ each.value.create_options.description != null
+ ? each.value.create_options.description
+ : "FAST stage ${each.key}."
+ )
+ visibility = each.value.create_options.visibility
+ auto_init = each.value.create_options.auto_init
+ allow_auto_merge = try(each.value.create_options.allow.auto_merge, null)
+ allow_merge_commit = try(each.value.create_options.allow.merge_commit, null)
+ allow_rebase_merge = try(each.value.create_options.allow.rebase_merge, null)
+ allow_squash_merge = try(each.value.create_options.allow.squash_merge, null)
+ has_issues = try(each.value.create_options.features.issues, null)
+ has_projects = try(each.value.create_options.features.projects, null)
+ has_wiki = try(each.value.create_options.features.wiki, null)
+ gitignore_template = try(each.value.create_options.templates.gitignore, null)
+ license_template = try(each.value.create_options.templates.license, null)
+
+ dynamic "template" {
+ for_each = (
+ try(each.value.create_options.templates.repository, null) != null
+ ? [""]
+ : []
+ )
+ content {
+ owner = each.value.create_options.templates.repository.owner
+ repository = each.value.create_options.templates.repository.name
+ }
+ }
+}
+
+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
+ title = "Modules repository access"
+ repository = local.modules_repository
+ key = tls_private_key.default.0.public_key_openssh
+ 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
+}
+
+resource "github_repository_file" "default" {
+ for_each = (
+ local.modules_repository == 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
+ ? replace(
+ file(each.value.file),
+ "/source\\s*=\\s*\"../../../modules/([^/\"]+)\"/",
+ "source = \"git@github.com:${var.organization}/${local.modules_repository}.git//$1${local.modules_ref}\"" # "
+ )
+ : file(each.value.file)
+ )
+ commit_message = "${var.commmit_config.message} (${each.value.name})"
+ commit_author = var.commmit_config.author
+ commit_email = var.commmit_config.email
+ overwrite_on_create = true
+}
diff --git a/fast/extras/00-cicd-github/outputs.tf b/fast/extras/00-cicd-github/outputs.tf
new file mode 100644
index 0000000000..cb580e1fe2
--- /dev/null
+++ b/fast/extras/00-cicd-github/outputs.tf
@@ -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.
+ */
+
+output "clone" {
+ description = "Clone repository commands."
+ value = {
+ for k, v in var.repositories :
+ k => "git clone git@github.com:${var.organization}/${k}.git"
+ }
+}
diff --git a/fast/extras/00-cicd-github/providers.tf b/fast/extras/00-cicd-github/providers.tf
new file mode 100644
index 0000000000..29be30ae98
--- /dev/null
+++ b/fast/extras/00-cicd-github/providers.tf
@@ -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.
+ */
+
+# tfdoc:file:description Provider configuration.
+
+provider "github" {
+ owner = var.organization
+}
diff --git a/fast/extras/00-cicd-github/variables.tf b/fast/extras/00-cicd-github/variables.tf
new file mode 100644
index 0000000000..0d9cb7fd6d
--- /dev/null
+++ b/fast/extras/00-cicd-github/variables.tf
@@ -0,0 +1,78 @@
+/**
+ * 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 "commmit_config" {
+ description = "Configure commit metadata."
+ type = object({
+ author = optional(string, "FAST loader")
+ email = optional(string, "fast-loader@fast.gcp.tf")
+ message = optional(string, "FAST initial loading")
+ })
+ default = {}
+ nullable = false
+}
+
+variable "modules_ref" {
+ description = "Optional git ref used in module sources."
+ type = string
+ default = null
+}
+
+variable "organization" {
+ description = "GitHub organization."
+ type = string
+}
+
+variable "repositories" {
+ description = "Repositories to create."
+ type = map(object({
+ create_options = optional(object({
+ allow = optional(object({
+ auto_merge = optional(bool)
+ merge_commit = optional(bool)
+ rebase_merge = optional(bool)
+ squash_merge = optional(bool)
+ }))
+ auto_init = optional(bool)
+ description = optional(string)
+ features = optional(object({
+ issues = optional(bool)
+ projects = optional(bool)
+ wiki = optional(bool)
+ }))
+ templates = optional(object({
+ gitignore = optional(string, "Terraform")
+ license = optional(string)
+ repository = optional(object({
+ name = string
+ owner = string
+ }))
+ }), {})
+ visibility = optional(string, "private")
+ }))
+ has_modules = optional(bool, false)
+ populate_from = optional(string)
+ }))
+ default = {}
+ nullable = true
+ validation {
+ condition = alltrue([
+ for k, v in var.repositories :
+ try(regex("^[a-zA-Z0-9_.]+$", k), null) != null
+ ])
+ error_message = "Repository names must match '^[a-zA-Z0-9_.]+$'."
+ }
+}
diff --git a/fast/extras/README.md b/fast/extras/README.md
new file mode 100644
index 0000000000..121fa4b049
--- /dev/null
+++ b/fast/extras/README.md
@@ -0,0 +1,5 @@
+# FAST extra stages
+
+This folder contains additional helper stages for FAST, which can be used to simplify specific operational tasks:
+
+- [GitHub repository management](./00-cicd-github/)
diff --git a/fast/stages/00-bootstrap/IAM.md b/fast/stages/00-bootstrap/IAM.md
index 2d71904631..b938c44f18 100644
--- a/fast/stages/00-bootstrap/IAM.md
+++ b/fast/stages/00-bootstrap/IAM.md
@@ -6,13 +6,14 @@ Legend: +
additive, •
conditional.
| members | roles |
|---|---|
-|+
+
|
+|gcp-devops+
+
|
-|gcp-organization-admins+
+
|
+|gcp-organization-admins+
+
+
|
|gcp-security-admins+
+
+
|
-|gcp-support+
+
|
-|prod-resman-0•
+
+
|
+|prod-bootstrap-0+
+
+
|
+|prod-resman-0•
+
+
+
|
## Project prod-audit-logs-0
@@ -31,6 +32,9 @@ Legend: +
additive, •
conditional.
| members | roles |
|---|---|
|gcp-devops+
|
|prod-bootstrap-0+
|
+|prod-resman-0+
|
diff --git a/fast/stages/00-bootstrap/README.md b/fast/stages/00-bootstrap/README.md
index 8edf9f1992..6e0e1b5559 100644
--- a/fast/stages/00-bootstrap/README.md
+++ b/fast/stages/00-bootstrap/README.md
@@ -54,6 +54,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,6 +80,23 @@ 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
+
+This stage also implements initial support for two interrelated features
+
+- configuration of [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) pools and providers
+- configuration of CI/CD repositories to allow impersonation via Workload identity Federation, and stage running via provided workflow templates
+
+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.
+
+
+
+For details on how to configure both features, refer to the Customizations sections below on [Workload Identity Federation](#workload-identity-federation) and [CI/CD repositories](#cicd-repositories).
+
+These features are optional and only enabled if the relevant variables have been populated.
+
## How to run this stage
This stage has straightforward initial requirements, as it is designed to work on newly created GCP organizations. Four steps are needed to bring up this stage:
@@ -153,12 +172,15 @@ gcloud beta billing accounts add-iam-policy-binding $FAST_BILLING_ACCOUNT_ID \
Before the first run, the following IAM groups must exist to allow IAM bindings to be created (actual names are flexible, see the [Customization](#customizations) section):
-- gcp-billing-admins
-- gcp-devops
-- gcp-network-admins
-- gcp-organization-admins
-- gcp-security-admins
-- gcp-support
+- `gcp-billing-admins`
+- `gcp-devops`
+- `gcp-network-admins`
+- `gcp-organization-admins`
+- `gcp-security-admins`
+
+You can refer to [this animated image](./groups.gif) for a step by step on group creation.
+
+Please note that FAST also supports an additional group for users with permissions to create support tickets and view logging and monitoring data. To remain consistent with the [Google Cloud Enterprise Checklist](https://cloud.google.com/docs/enterprise/setup-checklist) we map these permissions to the `gcp-devops` by default. However, we recommend creating a dedicated `gcp-support` group and updating the `groups` variable with the right value.
#### Configure variables
@@ -175,40 +197,42 @@ Then make sure you have configured the correct values for the following variable
You can also adapt the example that follows to your needs:
-```hcl
-# fetch the required id by running `gcloud beta billing accounts list`
-billing_account={
- id="012345-67890A-BCDEF0"
- organization_id="01234567890"
+```tfvars
+# 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
}
-# get the required info by running `gcloud organizations list`
-organization={
- id="01234567890"
- domain="fast.example.com"
- customer_id="Cxxxxxxx"
+
+# use `gcloud organizations list`
+organization = {
+ domain = "example.org"
+ id = 1234567890
+ customer_id = "C000001"
}
-# create your own 4-letters prefix
-prefix="fast"
-# comment out if you want to leverage automatic generation of configs
-# outputs_location = "~/fast-config"
+outputs_location = "~/fast-config"
+
+# use something unique and no longer than 9 characters
+prefix = "abcd"
```
### Output files and cross-stage variables
-At any time during the life of this stage, you can configure it to automatically generate provider configurations and variable files consumed by the following stages, to simplify passing outputs to input variables by not having to edit files manually.
+Each foundational FAST stage generates provider configurations and variable files can be consumed by the following stages, and saves them in a dedicated GCS bucket in the automation project. These files are a handy way to simplify stage configuration, and are also used by our CI/CD workflows to configure the repository files in the pipelines that validate and apply the code.
-Automatic generation of files is disabled by default. To enable the mechanism, set the `outputs_location` variable to a valid path on a local filesystem, e.g.
+Alongisde the GCS stored files, you can also configure a second copy to be saves on the local filesystem, as a convenience when developing or bringing up the infrastructure before a proper CI/CD setup is in place.
-```hcl
+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.
+
+```tfvars
outputs_location = "~/fast-config"
```
-This is especially suited for initial bootstrapping and development. You might want to adapt it to your practices for production deployments.
-
Once the variable is set, `apply` will generate and manage providers and variables files, including the initial one used for this stage after the first run. You can then link these files in the relevant stages, instead of manually transfering outputs from one stage, to Terraform variables in another.
-Below is the outline of the output files generated by all stages:
+Below is the outline of the output files generated by all stages, which is identical for both the GCS and local filesystem copies:
```bash
[path specified in outputs_location]
@@ -221,10 +245,12 @@ Below is the outline of the output files generated by all stages:
│ ├── 03-project-factory-prod-providers.tf
│ └── 99-sandbox-providers.tf
└── tfvars
- ├── 00-bootstrap.auto.tfvars.json
- ├── 01-resman.auto.tfvars.json
- ├── 02-networking.auto.tfvars.json
- └── 02-security.auto.tfvars.json
+│ ├── 00-bootstrap.auto.tfvars.json
+│ ├── 01-resman.auto.tfvars.json
+│ ├── 02-networking.auto.tfvars.json
+│ └── 02-security.auto.tfvars.json
+└── workflows
+ └── [optional depending on the configured CI/CD repositories]
```
### Running the stage
@@ -238,6 +264,7 @@ 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`:
@@ -270,10 +297,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.
@@ -322,6 +350,100 @@ If a different convention is needed, identify names via search/grep (e.g. with `
Names used in internal references (e.g. `module.foo-prod.id`) are only used by Terraform and do not influence resource naming, so they are best left untouched to avoid having to debug complex errors.
+### Workload Identity Federation
+
+At any time during this stage's lifecycle you can configure a Workload Identity Federation pool, and one or more providers. These are part of this stage's interface, included in the automatically generated `.tfvars` files and accepted by the Resource Managent stage that follows.
+
+The variable maps each provider's `issuer` attribute with the definitions in the `identity-providers.tf` file. We currently support GitHub and Gitlab directly, and extending to definitions to support more providers is trivial (send us a PR if you do!).
+
+Provider key names are used by the `cicd_repositories` variable to configure authentication for CI/CD repositories, and generally from your Terraform code whenever you need to configure IAM access or impersonation for federated identities.
+
+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:
+
+```tfvars
+federated_identity_providers = {
+ github-sample = {
+ attribute_condition = "attribute.repository_owner==\"my-github-org\""
+ issuer = "github"
+ custom_settings = null
+ }
+ gitlab-sample = {
+ attribute_condition = "attribute.namespace_path==\"my-gitlab-org\""
+ issuer = "gitlab"
+ custom_settings = null
+ }
+ 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"]
+ }
+ }
+}
+```
+
+### CI/CD repositories
+
+FAST is designed to directly support running in automated workflows from separate repositories for each stage. The `cicd_repositories` variable allows you to configure impersonation from external repositories leveraging Workload identity Federation, and pre-configures a FAST workflow file that can be used to validate and apply the code in each repository.
+
+The repository design we support is fairly simple, with a repository for modules that enables centralization and versioning, and one repository for each stage optionally configured from the previous stage.
+
+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.
+
+```tfvars
+cicd_repositories = {
+ bootstrap = {
+ branch = null
+ identity_provider = "github-sample"
+ 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"
+ name = "my-gh-org/fast-resman"
+ type = "github"
+ }
+}
+```
+
+The `type` attribute can be set to one of the supported repository types: `github`, `gitlab`, or `sourcerepo`.
+
+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).
+
+The remaining configuration is manual, as it regards the repositories themselves:
+
+- create a repository for modules
+ - clone and populate it with the Fabric modules
+ - configure authentication to the modules repository
+ - for GitHub
+ - create a key pair
+ - create a [deploy key](https://docs.github.com/en/developers/overview/managing-deploy-keys#deploy-keys) in the modules repository with the public key
+ - create a `CICD_MODULES_KEY` secret with the private key in each of the repositories that need to access modules (for Gitlab, please Base64 encode the private key for masking)
+ - for Gitlab
+ - TODO
+ - for Source Repositories
+ - assign the reader role to the CI/CD service accounts
+- create one repository for each stage
+ - clone and populate them with the stage source
+ - edit the modules source to match your modules repository
+ - a simple way is using the "Replace in files" function of your editor
+ - search for `source\s*= "../../../modules/([^"]+)"`
+ - replace with `source = "git@github.com:my-org/fast-modules.git//$1?ref=v1.0"`
+ - copy the generated workflow file for the stage from the GCS output files bucket or from the local clone if enabled
+ - for GitHub, place it in a `.github/workflows` folder in the repository root
+ - for Gitlab, rename it to `.gitlab-ci.yml` and place it in the repository root
+ - for Source Repositories, place it in `.cloudbuild/workflow.yaml`
+
@@ -331,10 +453,14 @@ Names used in internal references (e.g. `module.foo-prod.id`) are only used by T
|---|---|---|---|
| [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
|
+| [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. | | |
| [organization.tf](./organization.tf) | Organization-level IAM. | organization
| google_organization_iam_binding
|
-| [outputs.tf](./outputs.tf) | Module outputs. | | local_file
|
+| [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
@@ -342,24 +468,34 @@ Names used in internal references (e.g. `module.foo-prod.id`) are only used by T
| name | description | type | required | default | producer |
|---|---|:---:|:---:|:---:|:---:|
| [billing_account](variables.tf#L17) | Billing account id and organization id ('nnnnnnnn' or null). | object({…})
| ✓ | | |
-| [organization](variables.tf#L96) | Organization details. | object({…})
| ✓ | | |
-| [prefix](variables.tf#L111) | Prefix used for resources that need unique names. Use 9 characters or less. | string
| ✓ | | |
+| [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
| |
-| [custom_role_names](variables.tf#L31) | Names of custom roles defined at the org level. | object({…})
| | {…}
| |
-| [groups](variables.tf#L43) | Group names to grant organization-level permissions. | map(string)
| | {…}
| |
-| [iam](variables.tf#L57) | Organization-level custom IAM settings in role => [principal] format. | map(list(string))
| | {}
| |
-| [iam_additive](variables.tf#L63) | Organization-level custom IAM settings in role => [principal] format for non-authoritative bindings. | map(list(string))
| | {}
| |
-| [log_sinks](variables.tf#L71) | Org-level log sinks, in name => {type, filter} format. | map(object({…}))
| | {…}
| |
-| [outputs_location](variables.tf#L105) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | 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({…})
| | {…}
| |
## Outputs
| name | description | sensitive | consumers |
|---|---|:---:|---|
-| [billing_dataset](outputs.tf#L58) | BigQuery dataset prepared for billing export. | | |
-| [custom_roles](outputs.tf#L63) | Organization-level custom roles. | | |
-| [project_ids](outputs.tf#L68) | Projects created by this stage. | | |
-| [providers](outputs.tf#L79) | Terraform provider files for this stage and dependent stages. | ✓ | stage-01
|
-| [tfvars](outputs.tf#L88) | Terraform variable files for the following stages. | ✓ | |
+| [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. | ✓ | |
diff --git a/fast/stages/00-bootstrap/automation.tf b/fast/stages/00-bootstrap/automation.tf
index d5bf3a4a4a..1475c811c9 100644
--- a/fast/stages/00-bootstrap/automation.tf
+++ b/fast/stages/00-bootstrap/automation.tf
@@ -20,8 +20,10 @@ module "automation-project" {
source = "../../../modules/project"
billing_account = var.billing_account.id
name = "iac-core-0"
- parent = "organizations/${var.organization.id}"
- prefix = local.prefix
+ parent = coalesce(
+ var.project_parent_ids.automation, "organizations/${var.organization.id}"
+ )
+ prefix = local.prefix
# human (groups) IAM bindings
group_iam = {
(local.groups.gcp-devops) = [
@@ -30,6 +32,7 @@ module "automation-project" {
]
(local.groups.gcp-organization-admins) = [
"roles/iam.serviceAccountTokenCreator",
+ "roles/iam.workloadIdentityPoolAdmin"
]
}
# machine (service accounts) IAM bindings
@@ -37,9 +40,18 @@ module "automation-project" {
"roles/owner" = [
module.automation-tf-bootstrap-sa.iam_email
]
+ "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
]
@@ -51,48 +63,113 @@ module "automation-project" {
"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 = var.locations.gcs
+ storage_class = local.gcs_storage_class
+ versioning = true
+ depends_on = [module.organization]
+}
+
# this stage's bucket and service account
module "automation-tf-bootstrap-gcs" {
- source = "../../../modules/gcs"
- project_id = module.automation-project.project_id
- name = "iac-core-bootstrap-0"
- prefix = local.prefix
- versioning = true
- depends_on = [module.organization]
+ source = "../../../modules/gcs"
+ project_id = module.automation-project.project_id
+ name = "iac-core-bootstrap-0"
+ prefix = local.prefix
+ location = var.locations.gcs
+ storage_class = local.gcs_storage_class
+ versioning = true
+ depends_on = [module.organization]
}
module "automation-tf-bootstrap-sa" {
- source = "../../../modules/iam-service-account"
- project_id = module.automation-project.project_id
- name = "bootstrap-0"
- description = "Terraform organization bootstrap service account."
- prefix = local.prefix
+ source = "../../../modules/iam-service-account"
+ project_id = module.automation-project.project_id
+ name = "bootstrap-0"
+ display_name = "Terraform organization bootstrap 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["bootstrap"].iam_email, null)
+ ])
+ }
+ iam_storage_roles = {
+ (module.automation-tf-output-gcs.name) = ["roles/storage.admin"]
+ }
+}
+
+# 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" {
- source = "../../../modules/gcs"
- project_id = module.automation-project.project_id
- name = "iac-core-resman-0"
- prefix = local.prefix
- versioning = true
+ source = "../../../modules/gcs"
+ project_id = module.automation-project.project_id
+ name = "iac-core-resman-0"
+ prefix = local.prefix
+ location = var.locations.gcs
+ storage_class = local.gcs_storage_class
+ versioning = true
iam = {
"roles/storage.objectAdmin" = [module.automation-tf-resman-sa.iam_email]
}
@@ -100,9 +177,18 @@ module "automation-tf-resman-gcs" {
}
module "automation-tf-resman-sa" {
- source = "../../../modules/iam-service-account"
- project_id = module.automation-project.project_id
- name = "resman-0"
- description = "Terraform organization bootstrap service account."
- prefix = local.prefix
+ 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"].iam_email, null)
+ ])
+ }
+ iam_storage_roles = {
+ (module.automation-tf-output-gcs.name) = ["roles/storage.admin"]
+ }
}
diff --git a/fast/stages/00-bootstrap/billing.tf b/fast/stages/00-bootstrap/billing.tf
index 70053c7168..df10e8f085 100644
--- a/fast/stages/00-bootstrap/billing.tf
+++ b/fast/stages/00-bootstrap/billing.tf
@@ -19,6 +19,7 @@
locals {
# used here for convenience, in organization.tf members are explicit
billing_ext_admins = [
+ local.groups_iam.gcp-billing-admins,
local.groups_iam.gcp-organization-admins,
module.automation-tf-bootstrap-sa.iam_email,
module.automation-tf-resman-sa.iam_email
@@ -32,8 +33,10 @@ module "billing-export-project" {
count = local.billing_org ? 1 : 0
billing_account = var.billing_account.id
name = "billing-exp-0"
- parent = "organizations/${var.organization.id}"
- prefix = local.prefix
+ parent = coalesce(
+ var.project_parent_ids.billing, "organizations/${var.organization.id}"
+ )
+ prefix = local.prefix
iam = {
"roles/owner" = [module.automation-tf-bootstrap-sa.iam_email]
}
@@ -53,6 +56,7 @@ module "billing-export-dataset" {
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
@@ -103,3 +107,12 @@ resource "google_billing_account_iam_member" "billing_ext_admin" {
role = "roles/billing.admin"
member = each.key
}
+
+resource "google_billing_account_iam_member" "billing_ext_cost_manager" {
+ for_each = toset(
+ local.billing_ext ? local.billing_ext_admins : []
+ )
+ billing_account_id = var.billing_account.id
+ role = "roles/billing.costsManager"
+ member = each.key
+}
diff --git a/fast/stages/00-bootstrap/cicd.tf b/fast/stages/00-bootstrap/cicd.tf
new file mode 100644
index 0000000000..7cdae41c98
--- /dev/null
+++ b/fast/stages/00-bootstrap/cicd.tf
@@ -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.
+ */
+
+# tfdoc:file:description Workload Identity Federation configurations for CI/CD.
+
+locals {
+ cicd_repositories = {
+ for k, v in coalesce(var.cicd_repositories, {}) : k => v
+ if(
+ v != null
+ &&
+ (
+ try(v.type, null) == "sourcerepo"
+ ||
+ contains(keys(local.identity_providers), coalesce(try(v.identity_provider, null), ":"))
+ )
+ &&
+ fileexists(format("${path.module}/templates/workflow-%s.yaml", try(v.type, "")))
+ )
+ }
+ cicd_workflow_providers = {
+ bootstrap = "00-bootstrap-providers.tf"
+ cicd = "00-cicd-providers.tf"
+ resman = "01-resman-providers.tf"
+ }
+ cicd_workflow_var_files = {
+ bootstrap = []
+ cicd = [
+ "00-bootstrap.auto.tfvars.json",
+ "globals.auto.tfvars.json"
+ ]
+ resman = [
+ "00-bootstrap.auto.tfvars.json",
+ "globals.auto.tfvars.json"
+ ]
+ }
+}
+
+# source repository
+
+module "automation-tf-cicd-repo" {
+ source = "../../../modules/source-repository"
+ for_each = {
+ for k, v in local.cicd_repositories : k => v if v.type == "sourcerepo"
+ }
+ project_id = module.automation-project.project_id
+ name = each.value.name
+ iam = {
+ "roles/source.admin" = [
+ each.key == "bootstrap"
+ ? module.automation-tf-bootstrap-sa.iam_email
+ : module.automation-tf-resman-sa.iam_email
+ ]
+ "roles/source.reader" = [
+ module.automation-tf-cicd-sa[each.key].iam_email
+ ]
+ }
+ triggers = {
+ "fast-00-${each.key}" = {
+ filename = ".cloudbuild/workflow.yaml"
+ included_files = ["**/*tf", ".cloudbuild/workflow.yaml"]
+ service_account = module.automation-tf-cicd-sa[each.key].id
+ substitutions = {}
+ template = {
+ project_id = null
+ branch_name = each.value.branch
+ repo_name = each.value.name
+ tag_name = null
+ }
+ }
+ }
+}
+
+# SAs used by CI/CD workflows to impersonate automation SAs
+
+module "automation-tf-cicd-sa" {
+ source = "../../../modules/iam-service-account"
+ for_each = local.cicd_repositories
+ project_id = module.automation-project.project_id
+ name = "${each.key}-1"
+ display_name = "Terraform CI/CD ${each.key} service account."
+ 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.identity_providers_defs[each.value.type].principalset_tpl,
+ google_iam_workload_identity_pool.default.0.name,
+ each.value.name
+ )
+ : format(
+ local.identity_providers_defs[each.value.type].principal_tpl,
+ google_iam_workload_identity_pool.default.0.name,
+ 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/00-bootstrap/groups.gif b/fast/stages/00-bootstrap/groups.gif
new file mode 100644
index 0000000000..0744cb0995
Binary files /dev/null and b/fast/stages/00-bootstrap/groups.gif differ
diff --git a/fast/stages/00-bootstrap/identity-providers.tf b/fast/stages/00-bootstrap/identity-providers.tf
new file mode 100644
index 0000000000..d315bdb38a
--- /dev/null
+++ b/fast/stages/00-bootstrap/identity-providers.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.
+ */
+
+# 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/00-bootstrap/log-export.tf b/fast/stages/00-bootstrap/log-export.tf
index 682d473d93..1c9f5a87a3 100644
--- a/fast/stages/00-bootstrap/log-export.tf
+++ b/fast/stages/00-bootstrap/log-export.tf
@@ -21,9 +21,11 @@ locals {
}
module "log-export-project" {
- source = "../../../modules/project"
- name = "audit-logs-0"
- parent = "organizations/${var.organization.id}"
+ source = "../../../modules/project"
+ name = "audit-logs-0"
+ parent = coalesce(
+ var.project_parent_ids.logging, "organizations/${var.organization.id}"
+ )
prefix = local.prefix
billing_account = var.billing_account.id
iam = {
@@ -47,27 +49,32 @@ module "log-export-dataset" {
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
+ 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"
- count = contains(local.log_types, "logging") ? 1 : 0
+ 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-0"
+ 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 == "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/00-bootstrap/main.tf b/fast/stages/00-bootstrap/main.tf
index 49fa9140bb..839019f3cf 100644
--- a/fast/stages/00-bootstrap/main.tf
+++ b/fast/stages/00-bootstrap/main.tf
@@ -15,6 +15,11 @@
*/
locals {
+ gcs_storage_class = (
+ length(split("-", var.locations.gcs)) < 2
+ ? "MULTI_REGIONAL"
+ : "REGIONAL"
+ )
groups = {
for k, v in var.groups :
k => "${v}@${var.organization.domain}"
diff --git a/fast/stages/00-bootstrap/organization.tf b/fast/stages/00-bootstrap/organization.tf
index c9527522d4..0700d564e2 100644
--- a/fast/stages/00-bootstrap/organization.tf
+++ b/fast/stages/00-bootstrap/organization.tf
@@ -34,13 +34,17 @@ locals {
[module.automation-tf-bootstrap-sa.iam_email],
local._iam_bootstrap_user
)
- "roles/resourcemanager.organizationViewer" = [
- "domain:${var.organization.domain}"
- ]
+ # 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],
local._iam_bootstrap_user
)
+ "roles/resourcemanager.projectMover" = [
+ module.automation-tf-bootstrap-sa.iam_email
+ ]
"roles/resourcemanager.tagAdmin" = [
module.automation-tf-resman-sa.iam_email
]
@@ -63,17 +67,26 @@ locals {
]
# use additive to support cross-org roles for billing
"roles/iam.organizationRoleAdmin" = [
+ # uncomment if roles/owner is removed to organization admins
+ # local.groups.gcp-organization-admins,
local.groups_iam.gcp-security-admins,
module.automation-tf-bootstrap-sa.iam_email
]
"roles/orgpolicy.policyAdmin" = [
- module.automation-tf-resman-sa.iam_email,
+ local.groups_iam.gcp-organization-admins,
local.groups_iam.gcp-security-admins,
- local.groups_iam.gcp-organization-admins
+ module.automation-tf-resman-sa.iam_email
]
},
local.billing_org ? {
"roles/billing.admin" = [
+ local.groups_iam.gcp-billing-admins,
+ local.groups_iam.gcp-organization-admins,
+ module.automation-tf-bootstrap-sa.iam_email,
+ module.automation-tf-resman-sa.iam_email
+ ],
+ "roles/billing.costsManager" = [
+ local.groups_iam.gcp-billing-admins,
local.groups_iam.gcp-organization-admins,
module.automation-tf-bootstrap-sa.iam_email,
module.automation-tf-resman-sa.iam_email
@@ -83,11 +96,6 @@ locals {
_iam_bootstrap_user = (
var.bootstrap_user == null ? [] : ["user:${var.bootstrap_user}"]
)
- _log_sink_destinations = {
- bigquery = try(module.log-export-dataset.0.id, null),
- logging = try(module.log-export-logbucket.0.id, null),
- storage = try(module.log-export-gcs.0.name, null)
- }
iam = {
for role in local.iam_roles : role => distinct(concat(
try(sort(local._iam[role]), []),
@@ -106,13 +114,16 @@ locals {
iam_roles_additive = distinct(concat(
keys(local._iam_additive), keys(var.iam_additive)
))
- log_sink_destinations = {
- for k, v in var.log_sinks : k => (
- v.type == "pubsub"
- ? module.log-export-pubsub[k]
- : local._log_sink_destinations[v.type]
- )
- }
+ 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" {
@@ -126,6 +137,9 @@ module "organization" {
"roles/compute.osAdminLogin",
"roles/compute.osLoginExternalUser",
"roles/owner",
+ # granted via additive roles
+ # roles/iam.organizationRoleAdmin
+ # roles/orgpolicy.policyAdmin
"roles/resourcemanager.folderAdmin",
"roles/resourcemanager.organizationAdmin",
"roles/resourcemanager.projectCreator",
@@ -160,6 +174,13 @@ module "organization" {
]
(var.custom_role_names.service_project_network_admin) = [
"compute.globalOperations.get",
+ # compute.networks.updatePeering and compute.networks.get are
+ # used by automation service accounts who manage service
+ # projects where peering creation might be needed (e.g. GKE). If
+ # you remove them your network administrators should create
+ # peerings for service projects
+ "compute.networks.updatePeering",
+ "compute.networks.get",
"compute.organizations.disableXpnResource",
"compute.organizations.enableXpnResource",
"compute.projects.get",
@@ -172,11 +193,8 @@ module "organization" {
logging_sinks = {
for name, attrs in var.log_sinks : name => {
bq_partitioned_table = attrs.type == "bigquery"
- destination = local.log_sink_destinations[name]
- exclusions = {}
+ destination = local.log_sink_destinations[name].id
filter = attrs.filter
- iam = true
- include_children = true
type = attrs.type
}
}
diff --git a/fast/stages/00-bootstrap/outputs-files.tf b/fast/stages/00-bootstrap/outputs-files.tf
new file mode 100644
index 0000000000..ded88cd56d
--- /dev/null
+++ b/fast/stages/00-bootstrap/outputs-files.tf
@@ -0,0 +1,45 @@
+/**
+ * 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:file:description Output files persistence to local filesystem.
+
+resource "local_file" "providers" {
+ for_each = var.outputs_location == null ? {} : local.providers
+ file_permission = "0644"
+ filename = "${try(pathexpand(var.outputs_location), "")}/providers/${each.key}-providers.tf"
+ content = try(each.value, null)
+}
+
+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"
+ content = jsonencode(local.tfvars)
+}
+
+resource "local_file" "tfvars_globals" {
+ for_each = var.outputs_location == null ? {} : { 1 = 1 }
+ file_permission = "0644"
+ filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/globals.auto.tfvars.json"
+ content = jsonencode(local.tfvars_globals)
+}
+
+resource "local_file" "workflows" {
+ for_each = var.outputs_location == null ? {} : local.cicd_workflows
+ file_permission = "0644"
+ filename = "${try(pathexpand(var.outputs_location), "")}/workflows/${each.key}-workflow.yaml"
+ content = try(each.value, null)
+}
diff --git a/fast/stages/00-bootstrap/outputs-gcs.tf b/fast/stages/00-bootstrap/outputs-gcs.tf
new file mode 100644
index 0000000000..2c281d4ccb
--- /dev/null
+++ b/fast/stages/00-bootstrap/outputs-gcs.tf
@@ -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.
+ */
+
+# tfdoc:file:description Output files persistence to automation GCS bucket.
+
+resource "google_storage_bucket_object" "providers" {
+ for_each = local.providers
+ bucket = module.automation-tf-output-gcs.name
+ # provider suffix allows excluding via .gitignore when linked from stages
+ name = "providers/${each.key}-providers.tf"
+ content = each.value
+}
+
+resource "google_storage_bucket_object" "tfvars" {
+ bucket = module.automation-tf-output-gcs.name
+ name = "tfvars/00-bootstrap.auto.tfvars.json"
+ content = jsonencode(local.tfvars)
+}
+
+resource "google_storage_bucket_object" "tfvars_globals" {
+ bucket = module.automation-tf-output-gcs.name
+ name = "tfvars/globals.auto.tfvars.json"
+ content = jsonencode(local.tfvars_globals)
+}
+
+resource "google_storage_bucket_object" "workflows" {
+ for_each = local.cicd_workflows
+ bucket = module.automation-tf-output-gcs.name
+ name = "workflows/${each.key}-workflow.yaml"
+ content = each.value
+}
diff --git a/fast/stages/00-bootstrap/outputs.tf b/fast/stages/00-bootstrap/outputs.tf
index 46677b6a01..73dd64f4e9 100644
--- a/fast/stages/00-bootstrap/outputs.tf
+++ b/fast/stages/00-bootstrap/outputs.tf
@@ -15,56 +15,119 @@
*/
locals {
+ _tpl_providers = "${path.module}/templates/providers.tf.tpl"
+ # render CI/CD workflow templates
+ cicd_workflows = {
+ 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, ""
+ )
+ outputs_bucket = module.automation-tf-output-gcs.name
+ service_account = try(
+ module.automation-tf-cicd-sa[k].email, ""
+ )
+ stage_name = k
+ tf_providers_file = local.cicd_workflow_providers[k]
+ tf_var_files = local.cicd_workflow_var_files[k]
+ }
+ )
+ }
custom_roles = {
for k, v in var.custom_role_names :
- k => module.organization.custom_role_id[v]
+ k => try(module.organization.custom_role_id[v], null)
}
providers = {
- "00-bootstrap" = templatefile("${path.module}/../../assets/templates/providers.tpl", {
+ "00-bootstrap" = templatefile(local._tpl_providers, {
bucket = module.automation-tf-bootstrap-gcs.name
name = "bootstrap"
sa = module.automation-tf-bootstrap-sa.email
})
- "01-resman" = templatefile("${path.module}/../../assets/templates/providers.tpl", {
+ "00-cicd" = templatefile(local._tpl_providers, {
+ bucket = module.automation-tf-cicd-gcs.name
+ name = "cicd"
+ sa = module.automation-tf-cicd-provisioning-sa.email
+ })
+ "01-resman" = templatefile(local._tpl_providers, {
bucket = module.automation-tf-resman-gcs.name
name = "resman"
sa = module.automation-tf-resman-sa.email
})
}
tfvars = {
- automation_project_id = module.automation-project.project_id
- custom_roles = local.custom_roles
+ automation = {
+ federated_identity_pool = try(
+ google_iam_workload_identity_pool.default.0.name, null
+ )
+ federated_identity_providers = local.wif_providers
+ outputs_bucket = module.automation-tf-output-gcs.name
+ project_id = module.automation-project.project_id
+ project_number = module.automation-project.number
+ }
+ custom_roles = local.custom_roles
+ }
+ tfvars_globals = {
+ billing_account = var.billing_account
+ fast_features = var.fast_features
+ groups = var.groups
+ locations = var.locations
+ 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
+ }
}
}
-# optionally generate providers and tfvars files for subsequent stages
-
-resource "local_file" "providers" {
- for_each = var.outputs_location == null ? {} : local.providers
- file_permission = "0644"
- filename = "${pathexpand(var.outputs_location)}/providers/${each.key}-providers.tf"
- content = each.value
-}
-
-resource "local_file" "tfvars" {
- for_each = var.outputs_location == null ? {} : { 1 = 1 }
- file_permission = "0644"
- filename = "${pathexpand(var.outputs_location)}/tfvars/00-bootstrap.auto.tfvars.json"
- content = jsonencode(local.tfvars)
+output "automation" {
+ description = "Automation resources."
+ value = local.tfvars.automation
}
-# outputs
-
output "billing_dataset" {
description = "BigQuery dataset prepared for billing export."
value = try(module.billing-export-dataset.0.id, null)
}
+output "cicd_repositories" {
+ description = "CI/CD repository configurations."
+ value = {
+ for k, v in local.cicd_repositories : k => {
+ branch = v.branch
+ name = v.name
+ provider = try(local.wif_providers[v.identity_provider].name, null)
+ service_account = try(module.automation-tf-cicd-sa[k].email, null)
+ }
+ }
+}
+
output "custom_roles" {
description = "Organization-level custom roles."
value = local.custom_roles
}
+output "federated_identity" {
+ description = "Workload Identity Federation pool and providers."
+ value = {
+ pool = try(
+ google_iam_workload_identity_pool.default.0.name, null
+ )
+ providers = local.wif_providers
+ }
+}
+
+output "outputs_bucket" {
+ description = "GCS bucket where generated output files are stored."
+ value = module.automation-tf-output-gcs.name
+}
+
output "project_ids" {
description = "Projects created by this stage."
value = {
@@ -75,7 +138,6 @@ output "project_ids" {
}
# ready to use provider configurations for subsequent stages when not using files
-
output "providers" {
# tfdoc:output:consumers stage-01
description = "Terraform provider files for this stage and dependent stages."
@@ -83,8 +145,16 @@ output "providers" {
value = local.providers
}
-# ready to use variable values for subsequent stages
+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
+ }
+}
+# ready to use variable values for subsequent stages
output "tfvars" {
description = "Terraform variable files for the following stages."
sensitive = true
diff --git a/fast/stages/00-bootstrap/templates b/fast/stages/00-bootstrap/templates
new file mode 120000
index 0000000000..bcb6967bec
--- /dev/null
+++ b/fast/stages/00-bootstrap/templates
@@ -0,0 +1 @@
+../../assets/templates
\ No newline at end of file
diff --git a/fast/stages/00-bootstrap/terraform.tfvars.sample b/fast/stages/00-bootstrap/terraform.tfvars.sample
index 7dbe2e6b6b..66710ba4a6 100644
--- a/fast/stages/00-bootstrap/terraform.tfvars.sample
+++ b/fast/stages/00-bootstrap/terraform.tfvars.sample
@@ -14,6 +14,6 @@ organization = {
outputs_location = "~/fast-config"
-# use something unique and short
-prefix = "abcd"
+# use something unique and no longer than 9 characters
+prefix = "abcd"
diff --git a/fast/stages/00-bootstrap/variables.tf b/fast/stages/00-bootstrap/variables.tf
index 68d54650c3..0b9f37c20c 100644
--- a/fast/stages/00-bootstrap/variables.tf
+++ b/fast/stages/00-bootstrap/variables.tf
@@ -28,6 +28,58 @@ variable "bootstrap_user" {
default = null
}
+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({
+ branch = string
+ identity_provider = string
+ name = string
+ type = string
+ })
+ cicd = object({
+ branch = string
+ identity_provider = string
+ name = string
+ type = string
+ })
+ resman = 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_role_names" {
description = "Names of custom roles defined at the org level."
type = object({
@@ -40,6 +92,39 @@ variable "custom_role_names" {
}
}
+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
+ })
+ default = {
+ data_platform = true
+ gke = true
+ project_factory = true
+ sandbox = true
+ teams = true
+ }
+ 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 "groups" {
# https://cloud.google.com/docs/enterprise/setup-checklist
description = "Group names to grant organization-level permissions."
@@ -50,7 +135,11 @@ variable "groups" {
gcp-network-admins = "gcp-network-admins"
gcp-organization-admins = "gcp-organization-admins"
gcp-security-admins = "gcp-security-admins"
- gcp-support = "gcp-support"
+ # gcp-support is not included in the official GCP Enterprise
+ # Checklist, so by default we map gcp-support to gcp-devops.
+ # However, we recommend creating gcp-support and updating the
+ # value in the following line
+ gcp-support = "gcp-devops"
}
}
@@ -66,6 +155,23 @@ variable "iam_additive" {
default = {}
}
+variable "locations" {
+ 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
+}
+
# See https://cloud.google.com/architecture/exporting-stackdriver-logging-for-security-and-access-analytics
# for additional logging filter examples
variable "log_sinks" {
@@ -103,7 +209,7 @@ variable "organization" {
}
variable "outputs_location" {
- description = "Path where providers and tfvars files for the following stages are written. Leave empty to disable."
+ description = "Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable."
type = string
default = null
}
@@ -117,3 +223,18 @@ variable "prefix" {
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 organization as parent."
+ type = object({
+ automation = string
+ billing = string
+ logging = string
+ })
+ default = {
+ automation = null
+ billing = null
+ logging = null
+ }
+ nullable = false
+}
diff --git a/fast/stages/01-resman/IAM.md b/fast/stages/01-resman/IAM.md
index cbd989d980..403bd96cce 100644
--- a/fast/stages/01-resman/IAM.md
+++ b/fast/stages/01-resman/IAM.md
@@ -7,18 +7,39 @@ Legend: +
additive, •
conditional.
| members | roles |
|---|---|
|dev-resman-dp-0+
•
+
|
+|dev-resman-gke-0+
|
|dev-resman-pf-0+
•
+
+
|
|prod-resman-dp-0+
•
+
|
+|prod-resman-gke-0+
|
|prod-resman-net-0+
+
+
|
|prod-resman-pf-0+
•
+
+
|
|prod-resman-sec-0+
+
|
-## Folder development
+## Folder development [#0]
| members | roles |
|---|---|
-|dev-resman-dp-0+
additive, •
conditional.
|gcp-network-admins+
additive, •
conditional.
|---|---|
|gcp-security-admins+
|
+|dev-resman-dp-1+
|
+|dev-resman-gke-1+
|
+|prod-pf-resman-pf-1+
|
+|prod-resman-dp-1+
|
+|prod-resman-gke-1+
|
+|prod-resman-net-1+
|
+|prod-resman-sec-1+
|
diff --git a/fast/stages/01-resman/README.md b/fast/stages/01-resman/README.md
index 3ac9c09e3b..d36563523d 100644
--- a/fast/stages/01-resman/README.md
+++ b/fast/stages/01-resman/README.md
@@ -36,6 +36,12 @@ Additionally, a few critical benefits are directly provided by this design:
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.
+### 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).
+
## 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.
@@ -76,16 +82,15 @@ There are two broad sets of variables you will need to fill in:
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.
-If you configured a valid path for `outputs_location` in the bootstrap stage, simply link the relevant `terraform-*.auto.tfvars.json` files from the outputs folder. For this stage, you need the `.tfvars` file compiled manually for the bootstrap stage, and the one generated by it:
+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:
```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 .
-# 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, create an extra `terraform.tfvars` file or add them to the file copied from bootstrap.
+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.
@@ -102,9 +107,9 @@ terraform apply
This stage provides a single built-in customization that offers a minimal (but usable) implementation of the "application" or "business" grouping for resources discussed above. The `team_folders` variable allows you to specify a map of team name and groups, that will result in folders, automation service accounts, and IAM policies applied.
-Consider the following example
+Consider the following example in a `tfvars` file:
-```hcl
+```tfvars
team_folders = {
team-a = {
descriptive_name = "Team A"
@@ -153,40 +158,56 @@ Due to its simplicity, this stage lends itself easily to customizations: adding
|---|---|---|---|
| [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
| |
+| [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
· iam-service-account
| |
| [branch-security.tf](./branch-security.tf) | Security stage resources. | folder
· gcs
· iam-service-account
| |
-| [branch-teams.tf](./branch-teams.tf) | Team stages 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. | | |
| [organization.tf](./organization.tf) | Organization policies. | organization
| google_organization_iam_member
|
-| [outputs.tf](./outputs.tf) | Module outputs. | | local_file
|
+| [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_project_id](variables.tf#L20) | Project id for the automation project created by the bootstrap stage. | string
| ✓ | | 00-bootstrap
|
-| [billing_account](variables.tf#L26) | Billing account id and organization id ('nnnnnnnn' or null). | object({…})
| ✓ | | 00-bootstrap
|
-| [organization](variables.tf#L59) | Organization details. | object({…})
| ✓ | | 00-bootstrap
|
-| [prefix](variables.tf#L83) | Prefix used for resources that need unique names. Use 9 characters or less. | string
| ✓ | | 00-bootstrap
|
-| [custom_roles](variables.tf#L35) | Custom roles defined at the org level, in key => id format. | object({…})
| | null
| 00-bootstrap
|
-| [groups](variables.tf#L44) | Group names to grant organization-level permissions. | map(string)
| | {…}
| 00-bootstrap
|
-| [organization_policy_configs](variables.tf#L69) | Organization policies customization. | object({…})
| | null
| |
-| [outputs_location](variables.tf#L77) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string
| | null
| |
-| [team_folders](variables.tf#L94) | 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({…})
| ✓ | | 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
| |
## Outputs
| name | description | sensitive | consumers |
|---|---|:---:|---|
-| [dataplatform](outputs.tf#L114) | Data for the Data Platform stage. | | |
-| [networking](outputs.tf#L130) | Data for the networking stage. | | |
-| [project_factories](outputs.tf#L139) | Data for the project factories stage. | | |
-| [providers](outputs.tf#L155) | Terraform provider files for this stage and dependent stages. | ✓ | 02-networking
· 02-security
· 03-dataplatform
· xx-sandbox
· xx-teams
|
-| [sandbox](outputs.tf#L162) | Data for the sandbox stage. | | xx-sandbox
|
-| [security](outputs.tf#L172) | Data for the networking stage. | | 02-security
|
-| [teams](outputs.tf#L182) | Data for the teams stage. | | |
-| [tfvars](outputs.tf#L195) | Terraform variable files for the following stages. | ✓ | |
+| [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. | ✓ | |
diff --git a/fast/stages/01-resman/billing.tf b/fast/stages/01-resman/billing.tf
index 3e2020e798..fe497c7c34 100644
--- a/fast/stages/01-resman/billing.tf
+++ b/fast/stages/01-resman/billing.tf
@@ -23,12 +23,12 @@ locals {
module.branch-network-sa.iam_email,
module.branch-security-sa.iam_email,
],
- local.branch_dataplatform_sa_iam_emails,
- # enable if individual teams can create their own projects
- # [
- # for k, v in module.branch-teams-team-sa : v.iam_email
- # ],
- local.branch_teams_pf_sa_iam_emails,
+ local.branch_optional_sa_lists.dp-dev,
+ local.branch_optional_sa_lists.dp-prod,
+ local.branch_optional_sa_lists.gke-dev,
+ local.branch_optional_sa_lists.gke-prod,
+ local.branch_optional_sa_lists.pf-dev,
+ local.branch_optional_sa_lists.pf-prod,
)
}
@@ -41,7 +41,8 @@ module "billing-organization-ext" {
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.user" = local.billing_ext_users
+ "roles/billing.costsManager" = local.billing_ext_users
}
}
@@ -55,3 +56,12 @@ resource "google_billing_account_iam_member" "billing_ext_admin" {
role = "roles/billing.user"
member = each.key
}
+
+resource "google_billing_account_iam_member" "billing_ext_costsmanager" {
+ for_each = toset(
+ local.billing_ext ? local.billing_ext_users : []
+ )
+ billing_account_id = var.billing_account.id
+ role = "roles/billing.costsManager"
+ member = each.key
+}
diff --git a/fast/stages/01-resman/branch-data-platform.tf b/fast/stages/01-resman/branch-data-platform.tf
index e6ecad3f42..66cc9fbb08 100644
--- a/fast/stages/01-resman/branch-data-platform.tf
+++ b/fast/stages/01-resman/branch-data-platform.tf
@@ -16,91 +16,124 @@
# tfdoc:file:description Data Platform stages resources.
-# top-level Data Platform folder and service account
-
module "branch-dp-folder" {
source = "../../../modules/folder"
+ count = var.fast_features.data_platform ? 1 : 0
parent = "organizations/${var.organization.id}"
name = "Data Platform"
tag_bindings = {
- context = module.organization.tag_values["context/data"].id
+ context = try(
+ module.organization.tag_values["${var.tag_names.context}/data"].id, null
+ )
}
}
-# environment: development folder
-
module "branch-dp-dev-folder" {
source = "../../../modules/folder"
- parent = module.branch-dp-folder.id
+ 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) = [module.branch-dp-dev-sa.iam_email]
+ (local.custom_roles.service_project_network_admin) = [
+ module.branch-dp-dev-sa.0.iam_email
+ ]
# remove owner here and at project level if SA does not manage project resources
- "roles/owner" = [module.branch-dp-dev-sa.iam_email]
- "roles/logging.admin" = [module.branch-dp-dev-sa.iam_email]
- "roles/resourcemanager.folderAdmin" = [module.branch-dp-dev-sa.iam_email]
- "roles/resourcemanager.projectCreator" = [module.branch-dp-dev-sa.iam_email]
+ "roles/owner" = [module.branch-dp-dev-sa.0.iam_email]
+ "roles/logging.admin" = [module.branch-dp-dev-sa.0.iam_email]
+ "roles/resourcemanager.folderAdmin" = [module.branch-dp-dev-sa.0.iam_email]
+ "roles/resourcemanager.projectCreator" = [module.branch-dp-dev-sa.0.iam_email]
}
tag_bindings = {
- context = module.organization.tag_values["environment/development"].id
+ context = try(
+ module.organization.tag_values["${var.tag_names.environment}/development"].id,
+ null
+ )
}
}
-module "branch-dp-dev-sa" {
- source = "../../../modules/iam-service-account"
- project_id = var.automation_project_id
- name = "dev-resman-dp-0"
- description = "Terraform Data Platform development service account."
- prefix = var.prefix
-}
-
-module "branch-dp-dev-gcs" {
- source = "../../../modules/gcs"
- project_id = var.automation_project_id
- name = "dev-resman-dp-0"
- prefix = var.prefix
- versioning = true
- iam = {
- "roles/storage.objectAdmin" = [module.branch-dp-dev-sa.iam_email]
- }
-}
-
-# environment: production folder
-
module "branch-dp-prod-folder" {
source = "../../../modules/folder"
- parent = module.branch-dp-folder.id
+ 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) = [module.branch-dp-prod-sa.iam_email]
+ (local.custom_roles.service_project_network_admin) = [module.branch-dp-prod-sa.0.iam_email]
# remove owner here and at project level if SA does not manage project resources
- "roles/owner" = [module.branch-dp-prod-sa.iam_email]
- "roles/logging.admin" = [module.branch-dp-prod-sa.iam_email]
- "roles/resourcemanager.folderAdmin" = [module.branch-dp-prod-sa.iam_email]
- "roles/resourcemanager.projectCreator" = [module.branch-dp-prod-sa.iam_email]
+ "roles/owner" = [module.branch-dp-prod-sa.0.iam_email]
+ "roles/logging.admin" = [module.branch-dp-prod-sa.0.iam_email]
+ "roles/resourcemanager.folderAdmin" = [module.branch-dp-prod-sa.0.iam_email]
+ "roles/resourcemanager.projectCreator" = [module.branch-dp-prod-sa.0.iam_email]
}
tag_bindings = {
- context = module.organization.tag_values["environment/production"].id
+ context = try(
+ module.organization.tag_values["${var.tag_names.environment}/production"].id,
+ null
+ )
+ }
+}
+
+# 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 = "dev-resman-dp-0"
+ display_name = "Terraform data platform development service account."
+ prefix = var.prefix
+ 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"
- project_id = var.automation_project_id
- name = "prod-resman-dp-0"
- description = "Terraform Data Platform production service account."
- prefix = var.prefix
+ source = "../../../modules/iam-service-account"
+ count = var.fast_features.data_platform ? 1 : 0
+ project_id = var.automation.project_id
+ name = "prod-resman-dp-0"
+ display_name = "Terraform data platform production service account."
+ prefix = var.prefix
+ 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" = [module.branch-dp-dev-sa.0.iam_email]
+ }
}
module "branch-dp-prod-gcs" {
- source = "../../../modules/gcs"
- project_id = var.automation_project_id
- name = "prod-resman-dp-0"
- prefix = var.prefix
- versioning = true
+ 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" = [module.branch-dp-prod-sa.iam_email]
+ "roles/storage.objectAdmin" = [module.branch-dp-prod-sa.0.iam_email]
}
}
diff --git a/fast/stages/01-resman/branch-gke.tf b/fast/stages/01-resman/branch-gke.tf
new file mode 100644
index 0000000000..84ca41ed59
--- /dev/null
+++ b/fast/stages/01-resman/branch-gke.tf
@@ -0,0 +1,133 @@
+/**
+ * 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:file:description GKE multitenant stage resources.
+
+module "branch-gke-folder" {
+ source = "../../../modules/folder"
+ count = var.fast_features.gke ? 1 : 0
+ parent = "organizations/${var.organization.id}"
+ name = "GKE"
+ tag_bindings = {
+ context = try(
+ module.organization.tag_values["${var.tag_names.context}/gke"].id, null
+ )
+ }
+}
+
+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" = [module.branch-gke-dev-sa.0.iam_email]
+ "roles/logging.admin" = [module.branch-gke-dev-sa.0.iam_email]
+ "roles/resourcemanager.folderAdmin" = [module.branch-gke-dev-sa.0.iam_email]
+ "roles/resourcemanager.projectCreator" = [module.branch-gke-dev-sa.0.iam_email]
+ "roles/compute.xpnAdmin" = [module.branch-gke-dev-sa.0.iam_email]
+ }
+ tag_bindings = {
+ context = try(
+ module.organization.tag_values["${var.tag_names.environment}/development"].id,
+ null
+ )
+ }
+}
+
+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" = [module.branch-gke-prod-sa.0.iam_email]
+ "roles/logging.admin" = [module.branch-gke-prod-sa.0.iam_email]
+ "roles/resourcemanager.folderAdmin" = [module.branch-gke-prod-sa.0.iam_email]
+ "roles/resourcemanager.projectCreator" = [module.branch-gke-prod-sa.0.iam_email]
+ "roles/compute.xpnAdmin" = [module.branch-gke-prod-sa.0.iam_email]
+ }
+ tag_bindings = {
+ context = try(
+ module.organization.tag_values["${var.tag_names.environment}/production"].id,
+ null
+ )
+ }
+}
+
+module "branch-gke-dev-sa" {
+ source = "../../../modules/iam-service-account"
+ count = var.fast_features.gke ? 1 : 0
+ project_id = var.automation.project_id
+ name = "dev-resman-gke-0"
+ display_name = "Terraform gke multitenant dev service account."
+ prefix = var.prefix
+ iam = {
+ "roles/iam.serviceAccountTokenCreator" = concat(
+ ["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 = "prod-resman-gke-0"
+ display_name = "Terraform gke multitenant prod service account."
+ prefix = var.prefix
+ iam = {
+ "roles/iam.serviceAccountTokenCreator" = concat(
+ ["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" = [module.branch-gke-dev-sa.0.iam_email]
+ }
+}
+
+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" = [module.branch-gke-prod-sa.0.iam_email]
+ }
+}
diff --git a/fast/stages/01-resman/branch-networking.tf b/fast/stages/01-resman/branch-networking.tf
index 43ababfc35..530cf6b09f 100644
--- a/fast/stages/01-resman/branch-networking.tf
+++ b/fast/stages/01-resman/branch-networking.tf
@@ -39,26 +39,9 @@ module "branch-network-folder" {
"roles/compute.xpnAdmin" = [module.branch-network-sa.iam_email]
}
tag_bindings = {
- context = module.organization.tag_values["context/networking"].id
- }
-}
-
-module "branch-network-sa" {
- source = "../../../modules/iam-service-account"
- project_id = var.automation_project_id
- name = "prod-resman-net-0"
- description = "Terraform resman networking service account."
- prefix = var.prefix
-}
-
-module "branch-network-gcs" {
- source = "../../../modules/gcs"
- project_id = var.automation_project_id
- name = "prod-resman-net-0"
- prefix = var.prefix
- versioning = true
- iam = {
- "roles/storage.objectAdmin" = [module.branch-network-sa.iam_email]
+ context = try(
+ module.organization.tag_values["${var.tag_names.context}/networking"].id, null
+ )
}
}
@@ -67,13 +50,17 @@ module "branch-network-prod-folder" {
parent = module.branch-network-folder.id
name = "Production"
iam = {
- "roles/compute.xpnAdmin" = [
- module.branch-dp-prod-sa.iam_email,
- module.branch-teams-prod-pf-sa.iam_email
- ]
+ (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 = module.organization.tag_values["environment/production"].id
+ environment = try(
+ module.organization.tag_values["${var.tag_names.environment}/production"].id,
+ null
+ )
}
}
@@ -82,12 +69,47 @@ module "branch-network-dev-folder" {
parent = module.branch-network-folder.id
name = "Development"
iam = {
- (local.custom_roles.service_project_network_admin) = [
- module.branch-dp-dev-sa.iam_email,
- module.branch-teams-dev-pf-sa.iam_email
- ]
+ (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 = module.organization.tag_values["environment/development"].id
+ environment = try(
+ module.organization.tag_values["${var.tag_names.environment}/development"].id,
+ null
+ )
+ }
+}
+
+# automation service account and bucket
+
+module "branch-network-sa" {
+ source = "../../../modules/iam-service-account"
+ project_id = var.automation.project_id
+ name = "prod-resman-net-0"
+ display_name = "Terraform resman networking service account."
+ prefix = var.prefix
+ 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" = [module.branch-network-sa.iam_email]
}
}
diff --git a/fast/stages/01-resman/branch-project-factory.tf b/fast/stages/01-resman/branch-project-factory.tf
new file mode 100644
index 0000000000..41651a28c3
--- /dev/null
+++ b/fast/stages/01-resman/branch-project-factory.tf
@@ -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.
+ */
+
+# 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 = "dev-resman-pf-0"
+ # naming: environment in description
+ display_name = "Terraform project factory development service account."
+ prefix = var.prefix
+ 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 = "prod-resman-pf-0"
+ # naming: environment in description
+ display_name = "Terraform project factory production service account."
+ prefix = var.prefix
+ 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" = [module.branch-pf-dev-sa.0.iam_email]
+ }
+}
+
+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" = [module.branch-pf-prod-sa.0.iam_email]
+ }
+}
diff --git a/fast/stages/01-resman/branch-sandbox.tf b/fast/stages/01-resman/branch-sandbox.tf
index fa5441ef41..8b54e749a9 100644
--- a/fast/stages/01-resman/branch-sandbox.tf
+++ b/fast/stages/01-resman/branch-sandbox.tf
@@ -16,47 +16,62 @@
# tfdoc:file:description Sandbox stage resources.
+moved {
+ from = module.branch-sandbox-folder
+ to = module.branch-sandbox-folder.0
+}
+
module "branch-sandbox-folder" {
source = "../../../modules/folder"
+ count = var.fast_features.sandbox ? 1 : 0
parent = "organizations/${var.organization.id}"
name = "Sandbox"
iam = {
- "roles/logging.admin" = [module.branch-sandbox-sa.iam_email]
- "roles/owner" = [module.branch-sandbox-sa.iam_email]
- "roles/resourcemanager.folderAdmin" = [module.branch-sandbox-sa.iam_email]
- "roles/resourcemanager.projectCreator" = [module.branch-sandbox-sa.iam_email]
+ "roles/logging.admin" = [module.branch-sandbox-sa.0.iam_email]
+ "roles/owner" = [module.branch-sandbox-sa.0.iam_email]
+ "roles/resourcemanager.folderAdmin" = [module.branch-sandbox-sa.0.iam_email]
+ "roles/resourcemanager.projectCreator" = [module.branch-sandbox-sa.0.iam_email]
}
- policy_boolean = {
- "constraints/sql.restrictPublicIp" = false
- }
- policy_list = {
- "constraints/compute.vmExternalIpAccess" = {
- inherit_from_parent = false
- suggested_value = null
- status = true
- values = []
- }
+ org_policies = {
+ "constraints/sql.restrictPublicIp" = { enforce = false }
+ "constraints/compute.vmExternalIpAccess" = { allow = { all = true } }
}
tag_bindings = {
- context = module.organization.tag_values["context/sandbox"].id
+ context = try(
+ module.organization.tag_values["${var.tag_names.context}/sandbox"].id, null
+ )
}
}
+moved {
+ from = module.branch-sandbox-gcs
+ to = module.branch-sandbox-gcs.0
+}
+
module "branch-sandbox-gcs" {
- source = "../../../modules/gcs"
- project_id = var.automation_project_id
- name = "dev-resman-sbox-0"
- prefix = var.prefix
- versioning = true
+ 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" = [module.branch-sandbox-sa.iam_email]
+ "roles/storage.objectAdmin" = [module.branch-sandbox-sa.0.iam_email]
}
}
+moved {
+ from = module.branch-sandbox-sa
+ to = module.branch-sandbox-sa.0
+}
+
module "branch-sandbox-sa" {
- source = "../../../modules/iam-service-account"
- project_id = var.automation_project_id
- name = "dev-resman-sbox-0"
- description = "Terraform resman sandbox service account."
- prefix = var.prefix
+ source = "../../../modules/iam-service-account"
+ count = var.fast_features.sandbox ? 1 : 0
+ project_id = var.automation.project_id
+ name = "dev-resman-sbox-0"
+ display_name = "Terraform resman sandbox service account."
+ prefix = var.prefix
}
diff --git a/fast/stages/01-resman/branch-security.tf b/fast/stages/01-resman/branch-security.tf
index 7f0344a605..c7b4fc9708 100644
--- a/fast/stages/01-resman/branch-security.tf
+++ b/fast/stages/01-resman/branch-security.tf
@@ -40,24 +40,38 @@ module "branch-security-folder" {
"roles/resourcemanager.projectCreator" = [module.branch-security-sa.iam_email]
}
tag_bindings = {
- context = module.organization.tag_values["context/security"].id
+ context = try(
+ module.organization.tag_values["${var.tag_names.context}/security"].id, null
+ )
}
}
+# automation service account and bucket
+
module "branch-security-sa" {
- source = "../../../modules/iam-service-account"
- project_id = var.automation_project_id
- name = "prod-resman-sec-0"
- description = "Terraform resman security service account."
- prefix = var.prefix
+ source = "../../../modules/iam-service-account"
+ project_id = var.automation.project_id
+ name = "prod-resman-sec-0"
+ display_name = "Terraform resman security service account."
+ prefix = var.prefix
+ 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
- versioning = true
+ 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" = [module.branch-security-sa.iam_email]
}
diff --git a/fast/stages/01-resman/branch-teams.tf b/fast/stages/01-resman/branch-teams.tf
index 8f615931d0..8b0e89b3aa 100644
--- a/fast/stages/01-resman/branch-teams.tf
+++ b/fast/stages/01-resman/branch-teams.tf
@@ -14,44 +14,81 @@
* limitations under the License.
*/
-# tfdoc:file:description Team stages resources.
+# tfdoc:file:description Team stage resources.
-# top-level teams folder and service account
+# 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 = "organizations/${var.organization.id}"
name = "Teams"
+ iam = {
+ "roles/logging.admin" = [module.branch-teams-sa.0.iam_email]
+ "roles/owner" = [module.branch-teams-sa.0.iam_email]
+ "roles/resourcemanager.folderAdmin" = [module.branch-teams-sa.0.iam_email]
+ "roles/resourcemanager.projectCreator" = [module.branch-teams-sa.0.iam_email]
+ "roles/compute.xpnAdmin" = [module.branch-teams-sa.0.iam_email]
+ }
tag_bindings = {
- context = module.organization.tag_values["context/teams"].id
+ context = try(
+ module.organization.tag_values["${var.tag_names.context}/teams"].id, null
+ )
+ }
+}
+
+module "branch-teams-sa" {
+ source = "../../../modules/iam-service-account"
+ count = var.fast_features.teams ? 1 : 0
+ project_id = var.automation.project_id
+ name = "prod-resman-teams-0"
+ display_name = "Terraform resman teams service account."
+ prefix = var.prefix
+ iam_storage_roles = {
+ (var.automation.outputs_bucket) = ["roles/storage.admin"]
}
}
-module "branch-teams-prod-sa" {
- source = "../../../modules/iam-service-account"
- project_id = var.automation_project_id
- name = "prod-resman-teams-0"
- description = "Terraform resman production service account."
- prefix = var.prefix
+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]
+ }
}
-# Team-level folders, service accounts and buckets for each individual team
+################## per-team folders and automation resources ##################
module "branch-teams-team-folder" {
- source = "../../../modules/folder"
- for_each = coalesce(var.team_folders, {})
- parent = module.branch-teams-folder.id
- name = each.value.descriptive_name
+ 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 = coalesce(var.team_folders, {})
- project_id = var.automation_project_id
- name = "prod-teams-${each.key}-0"
- description = "Terraform team ${each.key} service account."
- prefix = var.prefix
+ 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
@@ -62,119 +99,67 @@ module "branch-teams-team-sa" {
}
module "branch-teams-team-gcs" {
- source = "../../../modules/gcs"
- for_each = coalesce(var.team_folders, {})
- project_id = var.automation_project_id
- name = "prod-teams-${each.key}-0"
- prefix = var.prefix
- versioning = true
+ 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]
}
}
-# environment: development folder and project factory automation resources
+# per-team environment folders where project factory SAs can create projects
module "branch-teams-team-dev-folder" {
source = "../../../modules/folder"
- for_each = coalesce(var.team_folders, {})
+ 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) = [module.branch-teams-dev-pf-sa.iam_email]
+ (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" = [module.branch-teams-dev-pf-sa.iam_email]
- "roles/logging.admin" = [module.branch-teams-dev-pf-sa.iam_email]
- "roles/resourcemanager.folderAdmin" = [module.branch-teams-dev-pf-sa.iam_email]
- "roles/resourcemanager.projectCreator" = [module.branch-teams-dev-pf-sa.iam_email]
+ "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 = module.organization.tag_values["environment/development"].id
- }
-}
-
-moved {
- from = module.branch-teams-dev-projectfactory-sa
- to = module.branch-teams-dev-pf-sa
-}
-
-module "branch-teams-dev-pf-sa" {
- source = "../../../modules/iam-service-account"
- project_id = var.automation_project_id
- name = "dev-resman-pf-0"
- # naming: environment in description
- description = "Terraform project factory development service account."
- prefix = var.prefix
-}
-
-moved {
- from = module.branch-teams-dev-projectfactory-gcs
- to = module.branch-teams-dev-pf-gcs
-}
-
-module "branch-teams-dev-pf-gcs" {
- source = "../../../modules/gcs"
- project_id = var.automation_project_id
- name = "dev-resman-pf-0"
- prefix = var.prefix
- versioning = true
- iam = {
- "roles/storage.objectAdmin" = [module.branch-teams-dev-pf-sa.iam_email]
+ environment = try(
+ module.organization.tag_values["${var.tag_names.environment}/development"].id, null
+ )
}
}
-# environment: production folder and project factory automation resources
-
module "branch-teams-team-prod-folder" {
source = "../../../modules/folder"
- for_each = coalesce(var.team_folders, {})
+ 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) = [module.branch-teams-prod-pf-sa.iam_email]
+ (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" = [module.branch-teams-prod-pf-sa.iam_email]
- "roles/logging.admin" = [module.branch-teams-prod-pf-sa.iam_email]
- "roles/resourcemanager.folderAdmin" = [module.branch-teams-prod-pf-sa.iam_email]
- "roles/resourcemanager.projectCreator" = [module.branch-teams-prod-pf-sa.iam_email]
+ "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 = module.organization.tag_values["environment/production"].id
- }
-}
-
-moved {
- from = module.branch-teams-prod-projectfactory-sa
- to = module.branch-teams-prod-pf-sa
-}
-
-module "branch-teams-prod-pf-sa" {
- source = "../../../modules/iam-service-account"
- project_id = var.automation_project_id
- name = "prod-resman-pf-0"
- # naming: environment in description
- description = "Terraform project factory production service account."
- prefix = var.prefix
-}
-
-moved {
- from = module.branch-teams-prod-projectfactory-gcs
- to = module.branch-teams-prod-pf-gcs
-}
-
-module "branch-teams-prod-pf-gcs" {
- source = "../../../modules/gcs"
- project_id = var.automation_project_id
- name = "prod-resman-pf-0"
- prefix = var.prefix
- versioning = true
- iam = {
- "roles/storage.objectAdmin" = [module.branch-teams-prod-pf-sa.iam_email]
+ environment = try(
+ module.organization.tag_values["${var.tag_names.environment}/production"].id, null
+ )
}
}
diff --git a/fast/stages/01-resman/cicd-data-platform.tf b/fast/stages/01-resman/cicd-data-platform.tf
new file mode 100644
index 0000000000..5b07883c44
--- /dev/null
+++ b/fast/stages/01-resman/cicd-data-platform.tf
@@ -0,0 +1,175 @@
+/**
+ * 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: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" = compact([
+ try(module.branch-dp-dev-sa.0.iam_email, "")
+ ])
+ "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" = [module.branch-dp-prod-sa.0.iam_email]
+ "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
+ }
+ # impersonated via workload identity federation for external repos
+ : {
+ "roles/iam.workloadIdentityUser" = [
+ each.value.branch == null
+ ? format(
+ local.identity_providers[each.value.identity_provider].principalset_tpl,
+ var.automation.federated_identity_pool,
+ each.value.name
+ )
+ : format(
+ local.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"]
+ }
+}
+
+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
+ }
+ # impersonated via workload identity federation for external repos
+ : {
+ "roles/iam.workloadIdentityUser" = [
+ each.value.branch == null
+ ? format(
+ local.identity_providers[each.value.identity_provider].principalset_tpl,
+ var.automation.federated_identity_pool,
+ each.value.name
+ )
+ : format(
+ local.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/01-resman/cicd-gke.tf b/fast/stages/01-resman/cicd-gke.tf
new file mode 100644
index 0000000000..fa4f8767ca
--- /dev/null
+++ b/fast/stages/01-resman/cicd-gke.tf
@@ -0,0 +1,175 @@
+/**
+ * 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: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
+ }
+ # impersonated via workload identity federation for external repos
+ : {
+ "roles/iam.workloadIdentityUser" = [
+ each.value.branch == null
+ ? format(
+ local.identity_providers[each.value.identity_provider].principalset_tpl,
+ var.automation.federated_identity_pool,
+ each.value.name
+ )
+ : format(
+ local.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"]
+ }
+}
+
+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
+ }
+ # impersonated via workload identity federation for external repos
+ : {
+ "roles/iam.workloadIdentityUser" = [
+ each.value.branch == null
+ ? format(
+ local.identity_providers[each.value.identity_provider].principalset_tpl,
+ var.automation.federated_identity_pool,
+ each.value.name
+ )
+ : format(
+ local.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/01-resman/cicd-networking.tf b/fast/stages/01-resman/cicd-networking.tf
new file mode 100644
index 0000000000..894348ff3b
--- /dev/null
+++ b/fast/stages/01-resman/cicd-networking.tf
@@ -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.
+ */
+
+# 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
+ }
+ # impersonated via workload identity federation for external repos
+ : {
+ "roles/iam.workloadIdentityUser" = [
+ each.value.branch == null
+ ? format(
+ local.identity_providers[each.value.identity_provider].principalset_tpl,
+ var.automation.federated_identity_pool,
+ each.value.name
+ )
+ : format(
+ local.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/01-resman/cicd-project-factory.tf b/fast/stages/01-resman/cicd-project-factory.tf
new file mode 100644
index 0000000000..8f357ce6c0
--- /dev/null
+++ b/fast/stages/01-resman/cicd-project-factory.tf
@@ -0,0 +1,191 @@
+/**
+ * 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: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" = [module.branch-pf-dev-sa.0.iam_email]
+ "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" = [module.branch-pf-prod-sa.0.iam_email]
+ "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
+ }
+ # impersonated via workload identity federation for external repos
+ : {
+ "roles/iam.workloadIdentityUser" = [
+ each.value.branch == null
+ ? format(
+ local.identity_providers[each.value.identity_provider].principalset_tpl,
+ var.automation.federated_identity_pool,
+ each.value.name
+ )
+ : format(
+ local.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"]
+ }
+}
+
+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
+ }
+ # impersonated via workload identity federation for external repos
+ : {
+ "roles/iam.workloadIdentityUser" = [
+ each.value.branch == null
+ ? format(
+ local.identity_providers[each.value.identity_provider].principalset_tpl,
+ var.automation.federated_identity_pool,
+ each.value.name
+ )
+ : format(
+ local.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/01-resman/cicd-security.tf b/fast/stages/01-resman/cicd-security.tf
new file mode 100644
index 0000000000..dd27a47331
--- /dev/null
+++ b/fast/stages/01-resman/cicd-security.tf
@@ -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.
+ */
+
+# 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
+ }
+ # impersonated via workload identity federation for external repos
+ : {
+ "roles/iam.workloadIdentityUser" = [
+ each.value.branch == null
+ ? format(
+ local.identity_providers[each.value.identity_provider].principalset_tpl,
+ var.automation.federated_identity_pool,
+ each.value.name
+ )
+ : format(
+ local.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/01-resman/data/org-policies/compute.yaml b/fast/stages/01-resman/data/org-policies/compute.yaml
new file mode 100644
index 0000000000..0d27ac426d
--- /dev/null
+++ b/fast/stages/01-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/01-resman/data/org-policies/iam.yaml b/fast/stages/01-resman/data/org-policies/iam.yaml
new file mode 100644
index 0000000000..4d83f827fe
--- /dev/null
+++ b/fast/stages/01-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/01-resman/data/org-policies/serverless.yaml b/fast/stages/01-resman/data/org-policies/serverless.yaml
new file mode 100644
index 0000000000..de62e6c702
--- /dev/null
+++ b/fast/stages/01-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/01-resman/data/org-policies/sql.yaml b/fast/stages/01-resman/data/org-policies/sql.yaml
new file mode 100644
index 0000000000..88b84d9d50
--- /dev/null
+++ b/fast/stages/01-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/01-resman/data/org-policies/storage.yaml b/fast/stages/01-resman/data/org-policies/storage.yaml
new file mode 100644
index 0000000000..6c0a673f3a
--- /dev/null
+++ b/fast/stages/01-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/01-resman/main.tf b/fast/stages/01-resman/main.tf
index 0cc1c6bbc0..0651ee3fac 100644
--- a/fast/stages/01-resman/main.tf
+++ b/fast/stages/01-resman/main.tf
@@ -16,10 +16,59 @@
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
+ )],
+ []
+ )
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
- custom_roles = coalesce(var.custom_roles, {})
+ 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, "")])
+ gke-dev = compact([try(module.branch-gke-dev-sa.0.iam_email, "")])
+ gke-prod = compact([try(module.branch-gke-prod-sa.0.iam_email, "")])
+ pf-dev = compact([try(module.branch-pf-dev-sa.0.iam_email, "")])
+ pf-prod = compact([try(module.branch-pf-prod-sa.0.iam_email, "")])
+ }
+ cicd_repositories = {
+ for k, v in coalesce(var.cicd_repositories, {}) : k => v
+ if(
+ v != null &&
+ (
+ try(v.type, null) == "sourcerepo"
+ ||
+ contains(
+ keys(local.identity_providers),
+ coalesce(try(v.identity_provider, null), ":")
+ )
+ ) &&
+ fileexists("${path.module}/templates/workflow-${try(v.type, "")}.yaml")
+ )
+ }
+ cicd_workflow_var_files = {
+ stage_2 = [
+ "00-bootstrap.auto.tfvars.json",
+ "01-resman.auto.tfvars.json",
+ "globals.auto.tfvars.json"
+ ]
+ stage_3 = [
+ "00-bootstrap.auto.tfvars.json",
+ "01-resman.auto.tfvars.json",
+ "globals.auto.tfvars.json",
+ "02-networking.auto.tfvars.json",
+ "02-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}@${var.organization.domain}"
@@ -28,4 +77,11 @@ locals {
for k, v in local.groups :
k => "group:${v}"
}
+ identity_providers = coalesce(
+ try(var.automation.federated_identity_providers, null), {}
+ )
+}
+
+data "google_client_openid_userinfo" "provider_identity" {
+ count = length(local.cicd_repositories) > 0 ? 1 : 0
}
diff --git a/fast/stages/01-resman/organization.tf b/fast/stages/01-resman/organization.tf
index 2e4cb35ef3..7ecf795232 100644
--- a/fast/stages/01-resman/organization.tf
+++ b/fast/stages/01-resman/organization.tf
@@ -18,28 +18,11 @@
locals {
- # set to the empty list if you remove the data platform branch
- branch_dataplatform_sa_iam_emails = [
- module.branch-dp-dev-sa.iam_email,
- module.branch-dp-prod-sa.iam_email
- ]
- # set to the empty list if you remove the teams branch
- branch_teams_pf_sa_iam_emails = [
- module.branch-teams-dev-pf-sa.iam_email,
- module.branch-teams-prod-pf-sa.iam_email
- ]
- list_allow = {
- inherit_from_parent = false
- suggested_value = null
- status = true
- values = []
- }
- list_deny = {
- inherit_from_parent = false
- suggested_value = null
- status = false
- values = []
- }
+ all_drs_domains = concat(
+ [var.organization.customer_id],
+ try(local.policy_configs.allowed_policy_member_domains, [])
+ )
+
policy_configs = (
var.organization_policy_configs == null
? {}
@@ -65,81 +48,45 @@ module "organization" {
]
},
local.billing_org ? {
- "roles/billing.costsManager" = local.branch_teams_pf_sa_iam_emails
+ "roles/billing.costsManager" = concat(
+ local.branch_optional_sa_lists.pf-dev,
+ local.branch_optional_sa_lists.pf-prod
+ )
"roles/billing.user" = concat(
[
module.branch-network-sa.iam_email,
module.branch-security-sa.iam_email,
],
- local.branch_dataplatform_sa_iam_emails,
- # enable if individual teams can create their own projects
- # [
- # for k, v in module.branch-teams-team-sa : v.iam_email
- # ],
- local.branch_teams_pf_sa_iam_emails
+ local.branch_optional_sa_lists.dp-dev,
+ local.branch_optional_sa_lists.dp-prod,
+ local.branch_optional_sa_lists.gke-dev,
+ local.branch_optional_sa_lists.gke-prod,
+ local.branch_optional_sa_lists.pf-dev,
+ local.branch_optional_sa_lists.pf-prod,
)
} : {}
)
+
# sample subset of useful organization policies, edit to suit requirements
- policy_boolean = {
- "constraints/cloudfunctions.requireVPCConnector" = true
- "constraints/compute.disableGuestAttributesAccess" = true
- "constraints/compute.disableInternetNetworkEndpointGroup" = true
- "constraints/compute.disableNestedVirtualization" = true
- "constraints/compute.disableSerialPortAccess" = true
- "constraints/compute.requireOsLogin" = true
- "constraints/compute.restrictXpnProjectLienRemoval" = true
- "constraints/compute.skipDefaultNetworkCreation" = true
- "constraints/compute.setNewProjectDefaultToZonalDNSOnly" = true
- "constraints/iam.automaticIamGrantsForDefaultServiceAccounts" = true
- "constraints/iam.disableServiceAccountKeyCreation" = true
- "constraints/iam.disableServiceAccountKeyUpload" = true
- "constraints/sql.restrictPublicIp" = true
- "constraints/sql.restrictAuthorizedNetworks" = true
- "constraints/storage.uniformBucketLevelAccess" = true
- }
- policy_list = {
- "constraints/cloudfunctions.allowedIngressSettings" = merge(
- local.list_allow, { values = ["is:ALLOW_INTERNAL_ONLY"] }
- )
- "constraints/cloudfunctions.allowedVpcConnectorEgressSettings" = merge(
- local.list_allow, { values = ["is:PRIVATE_RANGES_ONLY"] }
- )
- "constraints/compute.restrictLoadBalancerCreationForTypes" = merge(
- local.list_allow, { values = ["in:INTERNAL"] }
- )
- "constraints/compute.vmExternalIpAccess" = local.list_deny
- "constraints/iam.allowedPolicyMemberDomains" = merge(
- local.list_allow, {
- values = concat(
- [var.organization.customer_id],
- try(local.policy_configs.allowed_policy_member_domains, [])
- )
- })
- "constraints/run.allowedIngress" = merge(
- local.list_allow, { values = ["is:internal"] }
- )
- "constraints/run.allowedVPCEgress" = merge(
- local.list_allow, { values = ["is:private-ranges-only"] }
- )
- # "constraints/compute.restrictCloudNATUsage" = local.list_deny
- # "constraints/compute.restrictDedicatedInterconnectUsage" = local.list_deny
- # "constraints/compute.restrictPartnerInterconnectUsage" = local.list_deny
- # "constraints/compute.restrictProtocolForwardingCreationForTypes" = local.list_deny
- # "constraints/compute.restrictSharedVpcHostProjects" = local.list_deny
- # "constraints/compute.restrictSharedVpcSubnetworks" = local.list_deny
- # "constraints/compute.restrictVpcPeering" = local.list_deny
- # "constraints/compute.restrictVpnPeerIPs" = local.list_deny
- # "constraints/compute.vmCanIpForward" = local.list_deny
- # "constraints/gcp.resourceLocations" = {
- # inherit_from_parent = false
- # suggested_value = null
- # status = true
- # values = local.allowed_regions
+ org_policies = {
+ "iam.allowedPolicyMemberDomains" = { allow = { values = local.all_drs_domains } }
+
+ #"gcp.resourceLocations" = {
+ # allow = { values = local.allowed_regions }
+ # }
+ # "iam.workloadIdentityPoolProviders" = {
+ # allow = {
+ # values = [
+ # for k, v in coalesce(var.automation.federated_identity_providers, {}) :
+ # v.issuer_uri
+ # ]
+ # }
# }
}
+ org_policies_data_path = "${var.data_dir}/org-policies"
+
tags = {
- context = {
+ (var.tag_names.context) = {
description = "Resource management context."
iam = {}
values = {
@@ -151,7 +98,7 @@ module "organization" {
teams = null
}
}
- environment = {
+ (var.tag_names.environment) = {
description = "Environment definition."
iam = {}
values = {
@@ -164,24 +111,40 @@ module "organization" {
# organization policy admin role assigned with a condition on tags
-resource "google_organization_iam_member" "org_policy_admin" {
- for_each = {
- data-dev = ["data", "development", module.branch-dp-dev-sa.iam_email]
- data-prod = ["data", "production", module.branch-dp-prod-sa.iam_email]
- pf-dev = ["teams", "development", module.branch-teams-dev-pf-sa.iam_email]
- pf-prod = ["teams", "production", module.branch-teams-prod-pf-sa.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_scoped"
+ 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}/context', '${each.value.0}')
+ resource.matchTag('${var.organization.id}/${var.tag_names.context}', '${each.value.0}')
&&
- resource.matchTag('${var.organization.id}/environment', '${each.value.1}')
+ 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
+ }
+}
diff --git a/fast/stages/01-resman/outputs-files.tf b/fast/stages/01-resman/outputs-files.tf
new file mode 100644
index 0000000000..bd281d451b
--- /dev/null
+++ b/fast/stages/01-resman/outputs-files.tf
@@ -0,0 +1,42 @@
+/**
+ * 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:file:description Output files persistence to local filesystem.
+
+locals {
+ outputs_location = try(pathexpand(var.outputs_location), "")
+}
+
+resource "local_file" "providers" {
+ for_each = var.outputs_location == null ? {} : local.providers
+ file_permission = "0644"
+ filename = "${local.outputs_location}/providers/${each.key}-providers.tf"
+ content = try(each.value, null)
+}
+
+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"
+ content = jsonencode(local.tfvars)
+}
+
+resource "local_file" "workflows" {
+ for_each = var.outputs_location == null ? {} : local.cicd_workflows
+ file_permission = "0644"
+ filename = "${local.outputs_location}/workflows/${replace(each.key, "_", "-")}-workflow.yaml"
+ content = try(each.value, null)
+}
diff --git a/fast/stages/01-resman/outputs-gcs.tf b/fast/stages/01-resman/outputs-gcs.tf
new file mode 100644
index 0000000000..f1db11ef5d
--- /dev/null
+++ b/fast/stages/01-resman/outputs-gcs.tf
@@ -0,0 +1,37 @@
+/**
+ * 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: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/01-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/01-resman/outputs.tf b/fast/stages/01-resman/outputs.tf
index d38d9a8ff0..9b1a676058 100644
--- a/fast/stages/01-resman/outputs.tf
+++ b/fast/stages/01-resman/outputs.tf
@@ -15,73 +15,173 @@
*/
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 = "03-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_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_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_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_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_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_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_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.identity_providers[v.identity_provider].name, null
+ )
+ outputs_bucket = var.automation.outputs_bucket
+ stage_name = k
+ })
+ )
+ }
folder_ids = merge(
{
- data-platform = module.branch-dp-dev-folder.id
- networking = module.branch-network-folder.id
- networking-dev = module.branch-network-dev-folder.id
- networking-prod = module.branch-network-prod-folder.id
- sandbox = module.branch-sandbox-folder.id
- security = module.branch-security-folder.id
- teams = module.branch-teams-folder.id
+ 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-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-dev-folder :
+ "team-${k}-dev" => v.id
},
{
- for k, v in module.branch-teams-team-prod-folder : "team-${k}-prod" => v.id
+ for k, v in module.branch-teams-team-prod-folder :
+ "team-${k}-prod" => v.id
}
)
- providers = {
- "02-networking" = templatefile("${path.module}/../../assets/templates/providers.tpl", {
- bucket = module.branch-network-gcs.name
- name = "networking"
- sa = module.branch-network-sa.email
- })
- "02-security" = templatefile("${path.module}/../../assets/templates/providers.tpl", {
- bucket = module.branch-security-gcs.name
- name = "security"
- sa = module.branch-security-sa.email
- })
- "03-data-platform-dev" = templatefile("${path.module}/../../assets/templates/providers.tpl", {
- bucket = module.branch-dp-dev-gcs.name
- name = "dp-dev"
- sa = module.branch-dp-dev-sa.email
- })
- "03-data-platform-prod" = templatefile("${path.module}/../../assets/templates/providers.tpl", {
- bucket = module.branch-dp-prod-gcs.name
- name = "dp-prod"
- sa = module.branch-dp-prod-sa.email
- })
- "03-project-factory-dev" = templatefile("${path.module}/../../assets/templates/providers.tpl", {
- bucket = module.branch-teams-dev-pf-gcs.name
- name = "team-dev"
- sa = module.branch-teams-dev-pf-sa.email
- })
- "03-project-factory-prod" = templatefile("${path.module}/../../assets/templates/providers.tpl", {
- bucket = module.branch-teams-prod-pf-gcs.name
- name = "team-prod"
- sa = module.branch-teams-prod-pf-sa.email
- })
- "99-sandbox" = templatefile("${path.module}/../../assets/templates/providers.tpl", {
- bucket = module.branch-sandbox-gcs.name
- name = "sandbox"
- sa = module.branch-sandbox-sa.email
- })
- }
+ providers = merge(
+ {
+ "02-networking" = templatefile(local._tpl_providers, {
+ 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
+ })
+ },
+ !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
+ })
+ "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
+ })
+ },
+ !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
+ })
+ "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
+ })
+ },
+ !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
+ })
+ "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
+ })
+ },
+ !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
+ })
+ },
+ !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
+ })
+ },
+ {
+ 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
+ })
+ }
+ )
+ )
service_accounts = merge(
{
- data-platform-dev = module.branch-dp-dev-sa.email
- data-platform-prod = module.branch-dp-prod-sa.email
+ data-platform-dev = try(module.branch-dp-dev-sa.0.email, null)
+ data-platform-prod = try(module.branch-dp-prod-sa.0.email, null)
+ gke-dev = try(module.branch-gke-dev-sa.0.email, null)
+ gke-prod = try(module.branch-gke-prod-sa.0.email, null)
networking = module.branch-network-sa.email
- project-factory-dev = module.branch-teams-dev-pf-sa.email
- project-factory-prod = module.branch-teams-prod-pf-sa.email
- sandbox = module.branch-sandbox-sa.email
+ project-factory-dev = try(module.branch-pf-dev-sa.0.email, null)
+ project-factory-prod = try(module.branch-pf-prod-sa.0.email, null)
+ sandbox = try(module.branch-sandbox-sa.0.email, null)
security = module.branch-security-sa.email
- teams = module.branch-teams-prod-sa.email
+ teams = try(module.branch-teams-sa.0.email, null)
},
{
for k, v in module.branch-teams-team-sa : "team-${k}" => v.email
@@ -90,43 +190,61 @@ locals {
tfvars = {
folder_ids = local.folder_ids
service_accounts = local.service_accounts
+ tag_names = var.tag_names
}
}
-# optionally generate providers and tfvars files for subsequent stages
-
-resource "local_file" "providers" {
- for_each = var.outputs_location == null ? {} : local.providers
- file_permission = "0644"
- filename = "${pathexpand(var.outputs_location)}/providers/${each.key}-providers.tf"
- content = each.value
-}
-
-resource "local_file" "tfvars" {
- for_each = var.outputs_location == null ? {} : { 1 = 1 }
- file_permission = "0644"
- filename = "${pathexpand(var.outputs_location)}/tfvars/01-resman.auto.tfvars.json"
- content = jsonencode(local.tfvars)
+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.identity_providers[v.identity_provider].name, null
+ )
+ service_account = local.cicd_workflow_attrs[k].service_account
+ } if v != null
+ }
}
-# outputs
-
output "dataplatform" {
description = "Data for the Data Platform stage."
- value = {
+ value = !var.fast_features.data_platform ? {} : {
dev = {
- folder = module.branch-dp-dev-folder.id
- gcs_bucket = module.branch-dp-dev-gcs.name
- service_account = module.branch-dp-dev-sa.email
+ 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.id
- gcs_bucket = module.branch-dp-prod-gcs.name
- service_account = module.branch-dp-prod-sa.email
+ 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 = {
@@ -138,20 +256,19 @@ output "networking" {
output "project_factories" {
description = "Data for the project factories stage."
- value = {
+ value = !var.fast_features.project_factory ? {} : {
dev = {
- bucket = module.branch-teams-dev-pf-gcs.name
- sa = module.branch-teams-dev-pf-sa.email
+ bucket = module.branch-pf-dev-gcs.0.name
+ sa = module.branch-pf-dev-sa.0.email
}
prod = {
- bucket = module.branch-teams-prod-pf-gcs.name
- sa = module.branch-teams-prod-pf-sa.email
+ bucket = module.branch-pf-prod-gcs.0.name
+ sa = module.branch-pf-prod-sa.0.email
}
}
}
# 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."
@@ -162,11 +279,15 @@ output "providers" {
output "sandbox" {
# tfdoc:output:consumers xx-sandbox
description = "Data for the sandbox stage."
- value = {
- folder = module.branch-sandbox-folder.id
- gcs_bucket = module.branch-sandbox-gcs.name
- service_account = module.branch-sandbox-sa.email
- }
+ value = (
+ var.fast_features.sandbox
+ ? {
+ folder = module.branch-sandbox-folder.0.id
+ gcs_bucket = module.branch-sandbox-gcs.0.name
+ service_account = module.branch-sandbox-sa.0.email
+ }
+ : null
+ )
}
output "security" {
@@ -191,7 +312,6 @@ output "teams" {
}
# ready to use variable values for subsequent stages
-
output "tfvars" {
description = "Terraform variable files for the following stages."
sensitive = true
diff --git a/fast/stages/01-resman/templates b/fast/stages/01-resman/templates
new file mode 120000
index 0000000000..bcb6967bec
--- /dev/null
+++ b/fast/stages/01-resman/templates
@@ -0,0 +1 @@
+../../assets/templates
\ No newline at end of file
diff --git a/fast/stages/01-resman/variables.tf b/fast/stages/01-resman/variables.tf
index 639aba6f64..8b6f866bda 100644
--- a/fast/stages/01-resman/variables.tf
+++ b/fast/stages/01-resman/variables.tf
@@ -17,10 +17,22 @@
# 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_project_id" {
+variable "automation" {
# tfdoc:variable:source 00-bootstrap
- description = "Project id for the automation project created by the bootstrap stage."
- type = string
+ description = "Automation resources created by the 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" {
@@ -32,6 +44,88 @@ variable "billing_account" {
})
}
+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 00-bootstrap
description = "Custom roles defined at the org level, in key => id format."
@@ -41,6 +135,32 @@ variable "custom_roles" {
default = null
}
+variable "data_dir" {
+ description = "Relative path for the folder storing configuration data."
+ type = string
+ default = "data"
+}
+
+variable "fast_features" {
+ # tfdoc:variable:source 00-bootstrap
+ description = "Selective control for top-level FAST features."
+ type = object({
+ data_platform = bool
+ gke = bool
+ project_factory = bool
+ sandbox = bool
+ teams = bool
+ })
+ default = {
+ data_platform = true
+ gke = true
+ project_factory = true
+ sandbox = true
+ teams = true
+ }
+ # nullable = false
+}
+
variable "groups" {
# tfdoc:variable:source 00-bootstrap
description = "Group names to grant organization-level permissions."
@@ -56,6 +176,24 @@ variable "groups" {
}
}
+variable "locations" {
+ # tfdoc:variable:source 00-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 00-bootstrap
description = "Organization details."
@@ -75,7 +213,7 @@ variable "organization_policy_configs" {
}
variable "outputs_location" {
- description = "Path where providers and tfvars files for the following stages are written. Leave empty to disable."
+ description = "Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable."
type = string
default = null
}
@@ -91,6 +229,23 @@ variable "prefix" {
}
}
+variable "tag_names" {
+ description = "Customized names for resource management tags."
+ type = object({
+ context = string
+ environment = string
+ })
+ default = {
+ context = "context"
+ environment = "environment"
+ }
+ nullable = false
+ validation {
+ condition = alltrue([for k, v in var.tag_names : v != null])
+ error_message = "Tag names cannot be null."
+ }
+}
+
variable "team_folders" {
description = "Team folders to be created. Format is described in a code comment."
type = map(object({
@@ -99,13 +254,4 @@ variable "team_folders" {
impersonation_groups = list(string)
}))
default = null
- # default = {
- # team-a = {
- # descriptive_name = "Team A"
- # group_iam = {
- # team-a-group@example.com = ["roles/owner", "roles/resourcemanager.projectCreator"]
- # }
- # impersonation_groups = ["team-a-admins@example.com"]
- # }
- # }
}
diff --git a/fast/stages/02-networking-nva/README.md b/fast/stages/02-networking-nva/README.md
index 52ea03e8e6..79c27e8de6 100644
--- a/fast/stages/02-networking-nva/README.md
+++ b/fast/stages/02-networking-nva/README.md
@@ -163,15 +163,22 @@ Rules and policies are defined in simple YAML files, described below.
### DNS
-DNS often goes hand in hand with networking, especially on GCP where Cloud DNS zones and policies are associated at VPC level. This setup implements two DNS flows:
+DNS goes hand in hand with networking, especially on GCP where Cloud DNS zones and policies are associated at the VPC level. This setup implements both DNS flows:
-- on-premises to cloud, using private DNS zones pointing cloud-managed domains, and an [inbound policy](https://cloud.google.com/dns/docs/server-policies-overview#dns-server-policy-in) used as the forwarding target
-- cloud to on-premises, leveraging Cloud DNS forwarding zones, pointing to the on-premise managed domains
+- on-prem to cloud via private zones for cloud-managed domains, and an [inbound policy](https://cloud.google.com/dns/docs/server-policies-overview#dns-server-policy-in) used as forwarding target or via delegation (requires some extra configuration) from on-prem DNS resolvers
+- cloud to on-prem via forwarding zones for the on-prem managed domains
-The DNS configuration is centralized by leveraging peering zones, so that
+DNS configuration is further centralized by leveraging peering zones, so that
-- the landing project hosts the Cloud DNS configurations for the on-premise forwarding and Google API domains. Both the trusted and the untrusted VPCs are given visibility to these zones and the spokes consume them through their DNS peering zones
-- Cloud DNS peering zones in the spokes host the environment-specific domains configurations, with the trusted and the untrusted VPCs acting as the consumers (leveraging the DNS peering zones configured in the landing project)
+- the hub/landing Cloud DNS hosts configurations for on-prem forwarding, Google API domains, and the top-level private zone/s (e.g. gcp.example.com)
+- the spokes Cloud DNS host configurations for the environment-specific domains (e.g. prod.gcp.example.com), which are bound to the hub/landing leveraging [cross-project binding](https://cloud.google.com/dns/docs/zones/zones-overview#cross-project_binding); a peering zone for the `.` (root) zone is then created on each spoke, delegating all DNS resolution to hub/landing.
+- Private Google Access is enabled for a selection of the [supported domains](https://cloud.google.com/vpc/docs/configure-private-google-access#domain-options), namely
+ - `private.googleapis.com`
+ - `restricted.googleapis.com`
+ - `gcr.io`
+ - `packages.cloud.google.com`
+ - `pkg.dev`
+ - `pki.goog`
To complete the configuration, the 35.199.192.0/19 range should be routed to the VPN tunnels from on-premises, and the following names should be configured for DNS forwarding to cloud:
@@ -221,17 +228,18 @@ There are two broad sets of variables you will need to fill in:
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:
+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 ../../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 .
+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, add them to the file copied from bootstrap.
+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.
@@ -271,7 +279,7 @@ To add a new firewall rule, create a new file or edit an existing one in the `da
The DNS ([`dns`](../../../modules/dns)) infrastructure is defined in [`dns-*.tf`] files.
-Cloud DNS manages on-premises forwarding, the main GCP zone (in this example `gcp.example.com`) and is peered to environment-specific zones (i.e. `dev.gcp.example.com` and `prod.gcp.example.com`).
+Cloud DNS manages onprem forwarding, the main GCP zone (in this example `gcp.example.com`) and environment-specific zones (i.e. `dev.gcp.example.com` and `prod.gcp.example.com`).
#### Cloud environment
@@ -300,7 +308,7 @@ Subnets created using 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).
-- 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.tf`](./dns.tf)
+- 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
@@ -336,9 +344,7 @@ Copy `vpc-peering-prod.tf` to `vpc-peering-staging.tf` and replace "prod" with "
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 managed in the `dns-*.tf` files.
-Copy the `dns-prod.tf` to `dns-staging.tf` and replace within the files "prod" with "staging", where relevant.
-Don't forget to add a peering zone in the landing project and point it to the newly created environment private zone.
+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.
@@ -353,8 +359,8 @@ Don't forget to add a peering zone in the landing project and point it to the ne
| [landing.tf](./landing.tf) | Landing VPC and related resources. | net-cloudnat
· net-vpc
· net-vpc-firewall
· project
| |
| [main.tf](./main.tf) | Networking folder and hierarchical policy. | folder
| |
| [monitoring.tf](./monitoring.tf) | Network monitoring dashboards. | | google_monitoring_dashboard
|
-| [nva.tf](./nva.tf) | None | compute-mig
· compute-vm
· net-ilb
| |
-| [outputs.tf](./outputs.tf) | Module outputs. | | local_file
|
+| [nva.tf](./nva.tf) | None | compute-mig
· compute-vm
· simple-nva
| |
+| [outputs.tf](./outputs.tf) | Module outputs. | | google_storage_bucket_object
· local_file
|
| [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
| |
@@ -365,30 +371,32 @@ Don't forget to add a peering zone in the landing project and point it to the ne
| name | description | type | required | default | producer |
|---|---|:---:|:---:|:---:|:---:|
-| [billing_account](variables.tf#L17) | Billing account id and organization id ('nnnnnnnn' or null). | object({…})
| ✓ | | 00-bootstrap
|
-| [folder_ids](variables.tf#L71) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…})
| ✓ | | 01-resman
|
-| [organization](variables.tf#L107) | Organization details. | object({…})
| ✓ | | 00-bootstrap
|
-| [prefix](variables.tf#L123) | Prefix used for resources that need unique names. Use 9 characters or less. | string
| ✓ | | 00-bootstrap
|
-| [custom_adv](variables.tf#L26) | Custom advertisement definitions in name => range format. | map(string)
| | {…}
| |
-| [custom_roles](variables.tf#L48) | Custom roles defined at the org level, in key => id format. | object({…})
| | null
| 00-bootstrap
|
-| [data_dir](variables.tf#L57) | Relative path for the folder storing configuration data for network resources. | string
| | "data"
| |
-| [dns](variables.tf#L63) | Onprem DNS resolvers | map(list(string))
| | {…}
| |
-| [l7ilb_subnets](variables.tf#L81) | Subnets used for L7 ILBs. | map(list(object({…})))
| | {…}
| |
-| [onprem_cidr](variables.tf#L99) | Onprem addresses in name => range format. | map(string)
| | {…}
| |
-| [outputs_location](variables.tf#L117) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string
| | null
| |
-| [psa_ranges](variables.tf#L134) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…})
| | null
| |
-| [router_configs](variables.tf#L175) | Configurations for CRs and onprem routers. | map(object({…}))
| | {…}
| |
-| [service_accounts](variables.tf#L198) | Automation service accounts in name => email format. | object({…})
| | null
| 01-resman
|
-| [vpn_onprem_configs](variables.tf#L210) | VPN gateway configuration for onprem interconnection. | map(object({…}))
| | {…}
| |
+| [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({…}))
| | {…}
| |
## Outputs
| name | description | sensitive | consumers |
|---|---|:---:|---|
-| [host_project_ids](outputs.tf#L52) | Network project ids. | | |
-| [host_project_numbers](outputs.tf#L57) | Network project numbers. | | |
-| [shared_vpc_self_links](outputs.tf#L62) | Shared VPC host projects. | | |
-| [tfvars](outputs.tf#L81) | Terraform variables file for the following stages. | ✓ | |
-| [vpn_gateway_endpoints](outputs.tf#L67) | External IP Addresses for the GCP VPN gateways. | | |
+| [host_project_ids](outputs.tf#L58) | Network project ids. | | |
+| [host_project_numbers](outputs.tf#L63) | Network project numbers. | | |
+| [shared_vpc_self_links](outputs.tf#L68) | Shared VPC host projects. | | |
+| [tfvars](outputs.tf#L73) | Terraform variables file for the following stages. | ✓ | |
+| [vpn_gateway_endpoints](outputs.tf#L79) | External IP Addresses for the GCP VPN gateways. | | |
diff --git a/fast/stages/02-networking-nva/data/firewall-rules/dev/rules.yaml b/fast/stages/02-networking-nva/data/firewall-rules/dev/rules.yaml
index d4df8cdc31..cab42edc94 100644
--- a/fast/stages/02-networking-nva/data/firewall-rules/dev/rules.yaml
+++ b/fast/stages/02-networking-nva/data/firewall-rules/dev/rules.yaml
@@ -1,27 +1,21 @@
# skip boilerplate check
-ingress-allow-composer-nodes:
- description: "Allow traffic to Composer nodes."
- direction: INGRESS
- action: allow
- sources: []
- ranges: ["0.0.0.0/0"]
- targets:
- - composer-worker
- use_service_accounts: false
- rules:
- - protocol: tcp
- ports: [80, 443, 3306, 3307]
-
-ingress-allow-dataflow-load:
- description: "Allow traffic to Dataflow nodes."
- direction: INGRESS
- action: allow
- sources: []
- ranges: ["0.0.0.0/0"]
- targets:
- - dataflow
- use_service_accounts: false
- rules:
- - protocol: tcp
- ports: [12345, 12346]
+ingress:
+ ingress-allow-composer-nodes:
+ description: "Allow traffic to Composer nodes."
+ sources:
+ - composer-worker
+ targets:
+ - composer-worker
+ rules:
+ - protocol: tcp
+ ports: [80, 443, 3306, 3307]
+ ingress-allow-dataflow-load:
+ description: "Allow traffic to Dataflow nodes."
+ sources:
+ - dataflow
+ targets:
+ - dataflow
+ rules:
+ - protocol: tcp
+ ports: [12345, 12346]
diff --git a/fast/stages/02-networking-nva/data/firewall-rules/landing-trusted/rules.yaml b/fast/stages/02-networking-nva/data/firewall-rules/landing-trusted/rules.yaml
index 672af07f71..1405170fb5 100644
--- a/fast/stages/02-networking-nva/data/firewall-rules/landing-trusted/rules.yaml
+++ b/fast/stages/02-networking-nva/data/firewall-rules/landing-trusted/rules.yaml
@@ -1,29 +1,19 @@
# skip boilerplate check
-allow-hc-nva-ssh-trusted:
- description: "Allow traffic from Google healthchecks to NVA appliances"
- direction: INGRESS
- action: allow
- sources: []
- ranges:
- - $healthchecks
- targets: []
- use_service_accounts: false
- rules:
- - protocol: tcp
- ports:
- - 22
-
-allow-onprem-probes-trusted-example:
- description: "Allow traffic from onprem probes"
- direction: INGRESS
- action: allow
- sources: []
- ranges:
- - $onprem_probes
- targets: []
- use_service_accounts: false
- rules:
- - protocol: tcp
- ports:
- - 12345
+ingress:
+ allow-hc-nva-ssh-trusted:
+ description: "Allow traffic from Google healthchecks to NVA appliances"
+ source_ranges:
+ - healthchecks
+ rules:
+ - protocol: tcp
+ ports:
+ - 22
+ allow-onprem-probes-trusted-example:
+ description: "Allow traffic from onprem probes"
+ source_ranges:
+ - onprem_probes
+ rules:
+ - protocol: tcp
+ ports:
+ - 12345
diff --git a/fast/stages/02-networking-nva/data/firewall-rules/landing-untrusted/rules.yaml b/fast/stages/02-networking-nva/data/firewall-rules/landing-untrusted/rules.yaml
index 15db503ba6..aa51c0fe80 100644
--- a/fast/stages/02-networking-nva/data/firewall-rules/landing-untrusted/rules.yaml
+++ b/fast/stages/02-networking-nva/data/firewall-rules/landing-untrusted/rules.yaml
@@ -1,15 +1,11 @@
# skip boilerplate check
-allow-hc-nva-ssh-untrusted:
- description: "Allow traffic from Google healthchecks to NVA appliances"
- direction: INGRESS
- action: allow
- sources: []
- ranges:
- - $healthchecks
- targets: []
- use_service_accounts: false
- rules:
- - protocol: tcp
- ports:
- - 22
+ingress:
+ allow-hc-nva-ssh-untrusted:
+ description: "Allow traffic from Google healthchecks to NVA appliances"
+ source_ranges:
+ - healthchecks
+ rules:
+ - protocol: tcp
+ ports:
+ - 22
diff --git a/fast/stages/02-networking-nva/data/subnets/dev/dev-dataplatform-ew1.yaml b/fast/stages/02-networking-nva/data/subnets/dev/dev-dataplatform-ew1.yaml
index 92994826dc..2c682405c5 100644
--- a/fast/stages/02-networking-nva/data/subnets/dev/dev-dataplatform-ew1.yaml
+++ b/fast/stages/02-networking-nva/data/subnets/dev/dev-dataplatform-ew1.yaml
@@ -3,6 +3,6 @@
region: europe-west1
description: Default subnet for dev Data Platform
ip_cidr_range: 10.128.48.0/24
-secondary_ip_range:
+secondary_ip_ranges:
pods: 100.128.48.0/20
services: 100.255.48.0/24
diff --git a/fast/stages/02-networking-nva/diagram.svg b/fast/stages/02-networking-nva/diagram.svg
index b5ccea7e02..957b7d102b 100644
--- a/fast/stages/02-networking-nva/diagram.svg
+++ b/fast/stages/02-networking-nva/diagram.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/fast/stages/02-networking-nva/dns-dev.tf b/fast/stages/02-networking-nva/dns-dev.tf
index 08c99486fa..4eb472a159 100644
--- a/fast/stages/02-networking-nva/dns-dev.tf
+++ b/fast/stages/02-networking-nva/dns-dev.tf
@@ -26,7 +26,7 @@ module "dev-dns-private-zone" {
domain = "dev.gcp.example.com."
client_networks = [module.landing-trusted-vpc.self_link, module.landing-untrusted-vpc.self_link]
recordsets = {
- "A localhost" = { type = "A", ttl = 300, records = ["127.0.0.1"] }
+ "A localhost" = { records = ["127.0.0.1"] }
}
}
diff --git a/fast/stages/02-networking-nva/dns-landing.tf b/fast/stages/02-networking-nva/dns-landing.tf
index f177dcda2b..40090279f9 100644
--- a/fast/stages/02-networking-nva/dns-landing.tf
+++ b/fast/stages/02-networking-nva/dns-landing.tf
@@ -55,11 +55,11 @@ module "gcp-example-dns-private-zone" {
module.landing-trusted-vpc.self_link
]
recordsets = {
- "A localhost" = { type = "A", ttl = 300, records = ["127.0.0.1"] }
+ "A localhost" = { records = ["127.0.0.1"] }
}
}
-# Google API zone to trigger Private Access
+# Google APIs
module "googleapis-private-zone" {
source = "../../../modules/dns"
@@ -72,12 +72,84 @@ module "googleapis-private-zone" {
module.landing-trusted-vpc.self_link
]
recordsets = {
- "A private" = { type = "A", ttl = 300, records = [
+ "A private" = { records = [
"199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
] }
- "A restricted" = { type = "A", ttl = 300, records = [
+ "A restricted" = { records = [
"199.36.153.4", "199.36.153.5", "199.36.153.6", "199.36.153.7"
] }
- "CNAME *" = { type = "CNAME", ttl = 300, records = ["private.googleapis.com."] }
+ "CNAME *" = { records = ["private.googleapis.com."] }
+ }
+}
+
+module "gcrio-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.landing-project.project_id
+ type = "private"
+ name = "gcr-io"
+ domain = "gcr.io."
+ client_networks = [
+ module.landing-untrusted-vpc.self_link,
+ module.landing-trusted-vpc.self_link
+ ]
+ recordsets = {
+ "A gcr.io." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
+ }
+}
+
+module "packages-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.landing-project.project_id
+ type = "private"
+ name = "packages-cloud"
+ domain = "packages.cloud.google.com."
+ client_networks = [
+ module.landing-untrusted-vpc.self_link,
+ module.landing-trusted-vpc.self_link
+ ]
+ recordsets = {
+ "A packages.cloud.google.com." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
+ }
+}
+
+module "pkgdev-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.landing-project.project_id
+ type = "private"
+ name = "pkg-dev"
+ domain = "pkg.dev."
+ client_networks = [
+ module.landing-untrusted-vpc.self_link,
+ module.landing-trusted-vpc.self_link
+ ]
+ recordsets = {
+ "A pkg.dev." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
+ }
+}
+
+module "pkigoog-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.landing-project.project_id
+ type = "private"
+ name = "pki-goog"
+ domain = "pki.goog."
+ client_networks = [
+ module.landing-untrusted-vpc.self_link,
+ module.landing-trusted-vpc.self_link
+ ]
+ recordsets = {
+ "A pki.goog." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
}
}
diff --git a/fast/stages/02-networking-nva/dns-prod.tf b/fast/stages/02-networking-nva/dns-prod.tf
index 335f1508e3..b54609dfdb 100644
--- a/fast/stages/02-networking-nva/dns-prod.tf
+++ b/fast/stages/02-networking-nva/dns-prod.tf
@@ -26,7 +26,7 @@ module "prod-dns-private-zone" {
domain = "prod.gcp.example.com."
client_networks = [module.landing-trusted-vpc.self_link, module.landing-untrusted-vpc.self_link]
recordsets = {
- "A localhost" = { type = "A", ttl = 300, records = ["127.0.0.1"] }
+ "A localhost" = { records = ["127.0.0.1"] }
}
}
diff --git a/fast/stages/02-networking-nva/landing.tf b/fast/stages/02-networking-nva/landing.tf
index 0af94b1179..5a990030f0 100644
--- a/fast/stages/02-networking-nva/landing.tf
+++ b/fast/stages/02-networking-nva/landing.tf
@@ -22,10 +22,6 @@ module "landing-project" {
name = "prod-net-landing-0"
parent = var.folder_ids.networking-prod
prefix = var.prefix
- service_config = {
- disable_on_destroy = false
- disable_dependent_services = false
- }
services = [
"compute.googleapis.com",
"dns.googleapis.com",
@@ -34,14 +30,15 @@ module "landing-project" {
"stackdriver.googleapis.com"
]
shared_vpc_host_config = {
- enabled = true
- service_projects = []
+ enabled = true
}
iam = {
- "roles/dns.admin" = [local.service_accounts.project-factory-prod]
- (local.custom_roles.service_project_network_admin) = [
- local.service_accounts.project-factory-prod
- ]
+ "roles/dns.admin" = compact([
+ try(local.service_accounts.project-factory-prod, null)
+ ])
+ (local.custom_roles.service_project_network_admin) = compact([
+ try(local.service_accounts.project-factory-prod, null)
+ ])
}
}
@@ -52,26 +49,24 @@ module "landing-untrusted-vpc" {
project_id = module.landing-project.project_id
name = "prod-untrusted-landing-0"
mtu = 1500
-
dns_policy = {
- inbound = false
- logging = false
- outbound = null
+ inbound = false
+ logging = false
}
-
data_folder = "${var.data_dir}/subnets/landing-untrusted"
}
module "landing-untrusted-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.landing-project.project_id
- network = module.landing-untrusted-vpc.name
- admin_ranges = []
- http_source_ranges = []
- https_source_ranges = []
- ssh_source_ranges = []
- data_folder = "${var.data_dir}/firewall-rules/landing-untrusted"
- cidr_template_file = "${var.data_dir}/cidrs.yaml"
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.landing-project.project_id
+ network = module.landing-untrusted-vpc.name
+ default_rules_config = {
+ disabled = true
+ }
+ factories_config = {
+ cidr_tpl_file = "${var.data_dir}/cidrs.yaml"
+ rules_folder = "${var.data_dir}/firewall-rules/landing-untrusted"
+ }
}
# NAT
@@ -111,37 +106,32 @@ module "landing-trusted-vpc" {
routes = {
private-googleapis = {
dest_range = "199.36.153.8/30"
- priority = 1000
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
restricted-googleapis = {
dest_range = "199.36.153.4/30"
- priority = 1000
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
}
dns_policy = {
- inbound = true
- logging = false
- outbound = null
+ inbound = true
}
data_folder = "${var.data_dir}/subnets/landing-trusted"
}
module "landing-trusted-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.landing-project.project_id
- network = module.landing-trusted-vpc.name
- admin_ranges = []
- http_source_ranges = []
- https_source_ranges = []
- ssh_source_ranges = []
- data_folder = "${var.data_dir}/firewall-rules/landing-trusted"
- cidr_template_file = "${var.data_dir}/cidrs.yaml"
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.landing-project.project_id
+ network = module.landing-trusted-vpc.name
+ default_rules_config = {
+ disabled = true
+ }
+ factories_config = {
+ cidr_tpl_file = "${var.data_dir}/cidrs.yaml"
+ rules_folder = "${var.data_dir}/firewall-rules/landing-trusted"
+ }
}
diff --git a/fast/stages/02-networking-nva/main.tf b/fast/stages/02-networking-nva/main.tf
index 8f9e94caa5..4db5061ba0 100644
--- a/fast/stages/02-networking-nva/main.tf
+++ b/fast/stages/02-networking-nva/main.tf
@@ -25,12 +25,15 @@ locals {
})]
}
service_accounts = {
- for k, v in coalesce(var.service_accounts, {}) : k => "serviceAccount:${v}"
+ for k, v in coalesce(var.service_accounts, {}) :
+ k => "serviceAccount:${v}" if v != null
}
stage3_sas_delegated_grants = [
"roles/composer.sharedVpcAgent",
"roles/compute.networkUser",
+ "roles/compute.networkViewer",
"roles/container.hostServiceAgentUser",
+ "roles/multiclusterservicediscovery.serviceAgent",
"roles/vpcaccess.user",
]
}
diff --git a/fast/stages/02-networking-nva/nva.tf b/fast/stages/02-networking-nva/nva.tf
index 1bbc7f8139..f4f7b9e5ee 100644
--- a/fast/stages/02-networking-nva/nva.tf
+++ b/fast/stages/02-networking-nva/nva.tf
@@ -15,235 +15,151 @@
*/
locals {
- _subnets = var.data_dir == null ? tomap({}) : {
- for f in fileset("${var.data_dir}/subnets", "**/*.yaml") :
- trimsuffix(basename(f), ".yaml") => yamldecode(file("${var.data_dir}/subnets/${f}"))
+ # routing_config should be aligned to the NVA network interfaces - i.e.
+ # local.routing_config[0] sets up the first interface, and so on.
+ routing_config = [
+ {
+ name = "untrusted"
+ routes = [
+ var.custom_adv.gcp_landing_untrusted_ew1,
+ var.custom_adv.gcp_landing_untrusted_ew4,
+ ]
+ },
+ {
+ 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,
+ ]
+ },
+ ]
+ 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" },
}
- subnets = merge(
- { for k, v in local._subnets : "${k}-cidr" => v.ip_cidr_range },
- { for k, v in local._subnets : "${k}-gw" => cidrhost(v.ip_cidr_range, 1) }
- )
+
}
-# europe-west1
+# NVA config
+module "nva-cloud-config" {
+ source = "../../../modules/cloud-config-container/simple-nva"
+ enable_health_checks = true
+ network_interfaces = local.routing_config
+}
-module "nva-template-ew1" {
- source = "../../../modules/compute-vm"
- project_id = module.landing-project.project_id
- name = "nva-template"
- zone = "europe-west1-b"
- tags = ["nva"]
- can_ip_forward = true
+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}"
+ zone = "${each.value.region}-${each.value.zone}"
+ instance_type = "e2-standard-2"
+ tags = ["nva"]
+ create_template = true
+ can_ip_forward = true
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["${each.value.region}/landing-untrusted-default-${each.value.trigram}"]
nat = false
addresses = null
},
{
network = module.landing-trusted-vpc.self_link
- subnetwork = module.landing-trusted-vpc.subnet_self_links["europe-west1/landing-trusted-default-ew1"]
+ subnetwork = module.landing-trusted-vpc.subnet_self_links["${each.value.region}/landing-trusted-default-${each.value.trigram}"]
nat = false
addresses = null
}
]
boot_disk = {
- image = "projects/debian-cloud/global/images/family/debian-10"
- type = "pd-balanced"
+ image = "projects/cos-cloud/global/images/family/cos-stable"
size = 10
+ type = "pd-balanced"
}
- create_template = true
- instance_type = "f1-micro"
options = {
allow_stopping_for_update = true
deletion_protection = false
- # Creates preemptible instances, cheaper than regular one. Only suitable for testing.
- preemptible = true
+ spot = true
+ termination_action = "STOP"
}
metadata = {
- startup-script = templatefile(
- "${path.module}/data/nva-startup-script.tftpl",
- {
- dev-default-ew1-cidr = local.subnets.dev-default-ew1-cidr
- dev-default-ew4-cidr = local.subnets.dev-default-ew4-cidr
- gateway-trusted = local.subnets.landing-trusted-default-ew1-gw
- gateway-untrusted = local.subnets.landing-untrusted-default-ew1-gw
- landing-trusted-other-region = local.subnets.landing-trusted-default-ew4-cidr
- landing-untrusted-other-region = local.subnets.landing-untrusted-default-ew4-cidr
- onprem-main-cidr = var.onprem_cidr.main
- prod-default-ew1-cidr = local.subnets.prod-default-ew1-cidr
- prod-default-ew4-cidr = local.subnets.prod-default-ew4-cidr
- }
- )
+ user-data = module.nva-cloud-config.cloud_config
}
}
-module "nva-mig-ew1" {
- source = "../../../modules/compute-mig"
- project_id = module.landing-project.project_id
- regional = true
- location = "europe-west1"
- name = "nva-ew1"
- target_size = 2
+module "nva-mig" {
+ for_each = local.nva_locality
+ source = "../../../modules/compute-mig"
+ project_id = module.landing-project.project_id
+ location = each.value.region
+ name = "nva-cos-${each.value.trigram}-${each.value.zone}"
+ instance_template = module.nva-template[each.key].template.self_link
+ target_size = 1
auto_healing_policies = {
- health_check = module.nva-mig-ew1.health_check.self_link
initial_delay_sec = 30
}
health_check_config = {
- type = "tcp"
- check = { port = 22 }
- config = {}
- logging = true
- }
- default_version = {
- instance_template = module.nva-template-ew1.template.self_link
- name = "default"
- }
-}
-
-module "ilb-nva-untrusted-ew1" {
- source = "../../../modules/net-ilb"
- project_id = module.landing-project.project_id
- region = "europe-west1"
- name = "ilb-nva-untrusted-ew1"
- service_label = var.prefix
- global_access = true
- network = module.landing-untrusted-vpc.self_link
- subnetwork = module.landing-untrusted-vpc.subnet_self_links["europe-west1/landing-untrusted-default-ew1"]
- backends = [{
- failover = false
- group = module.nva-mig-ew1.group_manager.instance_group
- balancing_mode = "CONNECTION"
- }]
- health_check_config = {
- type = "tcp", check = { port = 22 }, config = {}, logging = false
+ enable_logging = true
+ tcp = {
+ port = 22
+ }
}
}
-module "ilb-nva-trusted-ew1" {
+module "ilb-nva-untrusted" {
+ for_each = { for l in local.nva_locality : l.region => l.trigram... }
source = "../../../modules/net-ilb"
project_id = module.landing-project.project_id
- region = "europe-west1"
- name = "ilb-nva-trusted-ew1"
+ region = each.key
+ name = "nva-untrusted-${each.value.0}"
service_label = var.prefix
global_access = true
- network = module.landing-trusted-vpc.self_link
- subnetwork = module.landing-trusted-vpc.subnet_self_links["europe-west1/landing-trusted-default-ew1"]
- backends = [{
- failover = false
- group = module.nva-mig-ew1.group_manager.instance_group
- balancing_mode = "CONNECTION"
- }]
+ 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}"]
+ }
+ backends = [
+ for key, _ in local.nva_locality : {
+ group = module.nva-mig[key].group_manager.instance_group
+ } if local.nva_locality[key].region == each.key
+ ]
health_check_config = {
- type = "tcp", check = { port = 22 }, config = {}, logging = false
- }
-}
-
-# europe-west4
-
-module "nva-template-ew4" {
- source = "../../../modules/compute-vm"
- project_id = module.landing-project.project_id
- name = "nva-template"
- zone = "europe-west4-a"
- tags = ["nva"]
- can_ip_forward = true
- network_interfaces = [
- {
- network = module.landing-untrusted-vpc.self_link
- subnetwork = module.landing-untrusted-vpc.subnet_self_links["europe-west4/landing-untrusted-default-ew4"]
- nat = false
- addresses = null
- },
- {
- network = module.landing-trusted-vpc.self_link
- subnetwork = module.landing-trusted-vpc.subnet_self_links["europe-west4/landing-trusted-default-ew4"]
- nat = false
- addresses = null
+ enable_logging = true
+ tcp = {
+ port = 22
}
- ]
- boot_disk = {
- image = "projects/debian-cloud/global/images/family/debian-10"
- type = "pd-balanced"
- size = 10
- }
- create_template = true
- metadata = {
- startup-script = templatefile(
- "${path.module}/data/nva-startup-script.tftpl",
- {
- dev-default-ew1-cidr = local.subnets.dev-default-ew1-cidr
- dev-default-ew4-cidr = local.subnets.dev-default-ew4-cidr
- gateway-trusted = local.subnets.landing-trusted-default-ew4-gw
- gateway-untrusted = local.subnets.landing-untrusted-default-ew4-gw
- landing-trusted-other-region = local.subnets.landing-trusted-default-ew1-cidr
- landing-untrusted-other-region = local.subnets.landing-untrusted-default-ew1-cidr
- onprem-main-cidr = var.onprem_cidr.main
- prod-default-ew1-cidr = local.subnets.prod-default-ew1-cidr
- prod-default-ew4-cidr = local.subnets.prod-default-ew4-cidr
- }
- )
}
}
-module "nva-mig-ew4" {
- source = "../../../modules/compute-mig"
- project_id = module.landing-project.project_id
- regional = true
- location = "europe-west4"
- name = "nva-ew4"
- target_size = 2
- auto_healing_policies = {
- health_check = module.nva-mig-ew4.health_check.self_link
- initial_delay_sec = 30
- }
- health_check_config = {
- type = "tcp"
- check = { port = 22 }
- config = {}
- logging = true
- }
- default_version = {
- instance_template = module.nva-template-ew4.template.self_link
- name = "default"
- }
-}
-module "ilb-nva-untrusted-ew4" {
+module "ilb-nva-trusted" {
+ for_each = { for l in local.nva_locality : l.region => l.trigram... }
source = "../../../modules/net-ilb"
project_id = module.landing-project.project_id
- region = "europe-west4"
- name = "ilb-nva-untrusted-ew4"
+ region = each.key
+ name = "nva-trusted-${each.value.0}"
service_label = var.prefix
global_access = true
- network = module.landing-untrusted-vpc.self_link
- subnetwork = module.landing-untrusted-vpc.subnet_self_links["europe-west4/landing-untrusted-default-ew4"]
- backends = [{
- failover = false
- group = module.nva-mig-ew4.group_manager.instance_group
- balancing_mode = "CONNECTION"
- }]
+ 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}"]
+ }
+ backends = [
+ for key, _ in local.nva_locality : {
+ group = module.nva-mig[key].group_manager.instance_group
+ } if local.nva_locality[key].region == each.key
+ ]
health_check_config = {
- type = "tcp", check = { port = 22 }, config = {}, logging = false
+ enable_logging = true
+ tcp = {
+ port = 22
+ }
}
}
-module "ilb-nva-trusted-ew4" {
- source = "../../../modules/net-ilb"
- project_id = module.landing-project.project_id
- region = "europe-west4"
- name = "ilb-nva-trusted-ew4"
- service_label = var.prefix
- global_access = true
- network = module.landing-trusted-vpc.self_link
- subnetwork = module.landing-trusted-vpc.subnet_self_links["europe-west4/landing-trusted-default-ew4"]
- backends = [{
- failover = false
- group = module.nva-mig-ew4.group_manager.instance_group
- balancing_mode = "CONNECTION"
- }]
- health_check_config = {
- type = "tcp", check = { port = 22 }, config = {}, logging = false
- }
-}
diff --git a/fast/stages/02-networking-nva/outputs.tf b/fast/stages/02-networking-nva/outputs.tf
index 072cb9a76f..df324570dd 100644
--- a/fast/stages/02-networking-nva/outputs.tf
+++ b/fast/stages/02-networking-nva/outputs.tf
@@ -38,15 +38,21 @@ locals {
}
}
-# optionally generate tfvars file for subsequent stages
+# generate tfvars file for subsequent stages
resource "local_file" "tfvars" {
for_each = var.outputs_location == null ? {} : { 1 = 1 }
file_permission = "0644"
- filename = "${pathexpand(var.outputs_location)}/tfvars/02-networking.auto.tfvars.json"
+ filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/02-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"
+ content = jsonencode(local.tfvars)
+}
+
# outputs
output "host_project_ids" {
@@ -64,6 +70,12 @@ output "shared_vpc_self_links" {
value = local.vpc_self_links
}
+output "tfvars" {
+ description = "Terraform variables file for the following stages."
+ sensitive = true
+ value = local.tfvars
+}
+
output "vpn_gateway_endpoints" {
description = "External IP Addresses for the GCP VPN gateways."
value = local.enable_onprem_vpn == false ? null : {
@@ -77,9 +89,3 @@ output "vpn_gateway_endpoints" {
}
}
}
-
-output "tfvars" {
- description = "Terraform variables file for the following stages."
- sensitive = true
- value = local.tfvars
-}
diff --git a/fast/stages/02-networking-nva/spoke-dev.tf b/fast/stages/02-networking-nva/spoke-dev.tf
index 52c37d81b9..fb88384c49 100644
--- a/fast/stages/02-networking-nva/spoke-dev.tf
+++ b/fast/stages/02-networking-nva/spoke-dev.tf
@@ -22,10 +22,6 @@ module "dev-spoke-project" {
name = "dev-net-spoke-0"
parent = var.folder_ids.networking-dev
prefix = var.prefix
- service_config = {
- disable_on_destroy = false
- disable_dependent_services = false
- }
services = [
"compute.googleapis.com",
"dns.googleapis.com",
@@ -35,12 +31,14 @@ module "dev-spoke-project" {
"stackdriver.googleapis.com",
]
shared_vpc_host_config = {
- enabled = true
- service_projects = []
+ enabled = true
}
metric_scopes = [module.landing-project.project_id]
iam = {
- "roles/dns.admin" = [local.service_accounts.project-factory-dev]
+ "roles/dns.admin" = compact([
+ try(local.service_accounts.gke-dev, null),
+ try(local.service_accounts.project-factory-dev, null),
+ ])
}
}
@@ -52,20 +50,18 @@ module "dev-spoke-vpc" {
data_folder = "${var.data_dir}/subnets/dev"
delete_default_routes_on_create = true
psa_config = try(var.psa_ranges.dev, null)
- subnets_l7ilb = local.l7ilb_subnets.dev
+ subnets_proxy_only = local.l7ilb_subnets.dev
# Set explicit routes for googleapis; send everything else to NVAs
routes = {
private-googleapis = {
dest_range = "199.36.153.8/30"
priority = 999
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
restricted-googleapis = {
dest_range = "199.36.153.4/30"
priority = 999
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
@@ -74,42 +70,43 @@ module "dev-spoke-vpc" {
priority = 1000
tags = ["ew1"]
next_hop_type = "ilb"
- next_hop = module.ilb-nva-trusted-ew1.forwarding_rule_address
+ next_hop = module.ilb-nva-trusted["europe-west1"].forwarding_rule_address
}
nva-ew4-to-ew4 = {
dest_range = "0.0.0.0/0"
priority = 1000
tags = ["ew4"]
next_hop_type = "ilb"
- next_hop = module.ilb-nva-trusted-ew4.forwarding_rule_address
+ next_hop = module.ilb-nva-trusted["europe-west4"].forwarding_rule_address
}
nva-ew1-to-ew4 = {
dest_range = "0.0.0.0/0"
priority = 1001
tags = ["ew1"]
next_hop_type = "ilb"
- next_hop = module.ilb-nva-trusted-ew4.forwarding_rule_address
+ next_hop = module.ilb-nva-trusted["europe-west4"].forwarding_rule_address
}
nva-ew4-to-ew1 = {
dest_range = "0.0.0.0/0"
priority = 1001
tags = ["ew4"]
next_hop_type = "ilb"
- next_hop = module.ilb-nva-trusted-ew1.forwarding_rule_address
+ next_hop = module.ilb-nva-trusted["europe-west1"].forwarding_rule_address
}
}
}
module "dev-spoke-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.dev-spoke-project.project_id
- network = module.dev-spoke-vpc.name
- admin_ranges = []
- http_source_ranges = []
- https_source_ranges = []
- ssh_source_ranges = []
- data_folder = "${var.data_dir}/firewall-rules/dev"
- cidr_template_file = "${var.data_dir}/cidrs.yaml"
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.dev-spoke-project.project_id
+ network = module.dev-spoke-vpc.name
+ default_rules_config = {
+ disabled = true
+ }
+ factories_config = {
+ cidr_tpl_file = "${var.data_dir}/cidrs.yaml"
+ rules_folder = "${var.data_dir}/firewall-rules/dev"
+ }
}
module "peering-dev" {
@@ -123,10 +120,11 @@ module "peering-dev" {
resource "google_project_iam_binding" "dev_spoke_project_iam_delegated" {
project = module.dev-spoke-project.project_id
role = "roles/resourcemanager.projectIamAdmin"
- members = [
- local.service_accounts.data-platform-dev,
- local.service_accounts.project-factory-dev,
- ]
+ members = compact([
+ try(local.service_accounts.data-platform-dev, null),
+ try(local.service_accounts.project-factory-dev, null),
+ try(local.service_accounts.gke-dev, null),
+ ])
condition {
title = "dev_stage3_sa_delegated_grants"
description = "Development host project delegated grants."
diff --git a/fast/stages/02-networking-nva/spoke-prod.tf b/fast/stages/02-networking-nva/spoke-prod.tf
index 9996bb762e..484550acac 100644
--- a/fast/stages/02-networking-nva/spoke-prod.tf
+++ b/fast/stages/02-networking-nva/spoke-prod.tf
@@ -22,10 +22,6 @@ module "prod-spoke-project" {
name = "prod-net-spoke-0"
parent = var.folder_ids.networking-prod
prefix = var.prefix
- service_config = {
- disable_on_destroy = false
- disable_dependent_services = false
- }
services = [
"compute.googleapis.com",
"dns.googleapis.com",
@@ -35,12 +31,14 @@ module "prod-spoke-project" {
"stackdriver.googleapis.com",
]
shared_vpc_host_config = {
- enabled = true
- service_projects = []
+ enabled = true
}
metric_scopes = [module.landing-project.project_id]
iam = {
- "roles/dns.admin" = [local.service_accounts.project-factory-prod]
+ "roles/dns.admin" = compact([
+ try(local.service_accounts.gke-prod, null),
+ try(local.service_accounts.project-factory-prod, null),
+ ])
}
}
@@ -52,20 +50,18 @@ module "prod-spoke-vpc" {
data_folder = "${var.data_dir}/subnets/prod"
delete_default_routes_on_create = true
psa_config = try(var.psa_ranges.prod, null)
- subnets_l7ilb = local.l7ilb_subnets.prod
+ subnets_proxy_only = local.l7ilb_subnets.prod
# Set explicit routes for googleapis; send everything else to NVAs
routes = {
private-googleapis = {
dest_range = "199.36.153.8/30"
priority = 999
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
restricted-googleapis = {
dest_range = "199.36.153.4/30"
priority = 999
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
@@ -74,42 +70,43 @@ module "prod-spoke-vpc" {
priority = 1000
tags = ["ew1"]
next_hop_type = "ilb"
- next_hop = module.ilb-nva-trusted-ew1.forwarding_rule_address
+ next_hop = module.ilb-nva-trusted["europe-west1"].forwarding_rule_address
}
nva-ew4-to-ew4 = {
dest_range = "0.0.0.0/0"
priority = 1000
tags = ["ew4"]
next_hop_type = "ilb"
- next_hop = module.ilb-nva-trusted-ew4.forwarding_rule_address
+ next_hop = module.ilb-nva-trusted["europe-west4"].forwarding_rule_address
}
nva-ew1-to-ew4 = {
dest_range = "0.0.0.0/0"
priority = 1001
tags = ["ew1"]
next_hop_type = "ilb"
- next_hop = module.ilb-nva-trusted-ew4.forwarding_rule_address
+ next_hop = module.ilb-nva-trusted["europe-west4"].forwarding_rule_address
}
nva-ew4-to-ew1 = {
dest_range = "0.0.0.0/0"
priority = 1001
tags = ["ew4"]
next_hop_type = "ilb"
- next_hop = module.ilb-nva-trusted-ew1.forwarding_rule_address
+ next_hop = module.ilb-nva-trusted["europe-west1"].forwarding_rule_address
}
}
}
module "prod-spoke-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.prod-spoke-project.project_id
- network = module.prod-spoke-vpc.name
- admin_ranges = []
- http_source_ranges = []
- https_source_ranges = []
- ssh_source_ranges = []
- data_folder = "${var.data_dir}/firewall-rules/prod"
- cidr_template_file = "${var.data_dir}/cidrs.yaml"
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.prod-spoke-project.project_id
+ network = module.prod-spoke-vpc.name
+ default_rules_config = {
+ disabled = true
+ }
+ factories_config = {
+ cidr_tpl_file = "${var.data_dir}/cidrs.yaml"
+ rules_folder = "${var.data_dir}/firewall-rules/prod"
+ }
}
module "peering-prod" {
@@ -123,10 +120,11 @@ module "peering-prod" {
resource "google_project_iam_binding" "prod_spoke_project_iam_delegated" {
project = module.prod-spoke-project.project_id
role = "roles/resourcemanager.projectIamAdmin"
- members = [
- local.service_accounts.data-platform-prod,
- local.service_accounts.project-factory-prod,
- ]
+ members = compact([
+ try(local.service_accounts.data-platform-prod, null),
+ try(local.service_accounts.project-factory-prod, null),
+ try(local.service_accounts.gke-prod, null),
+ ])
condition {
title = "prod_stage3_sa_delegated_grants"
description = "Production host project delegated grants."
diff --git a/fast/stages/02-networking-nva/test-resources.tf b/fast/stages/02-networking-nva/test-resources.tf
index 9c259c7f4f..d3b6109e3c 100644
--- a/fast/stages/02-networking-nva/test-resources.tf
+++ b/fast/stages/02-networking-nva/test-resources.tf
@@ -16,7 +16,7 @@
# tfdoc:file:description temporary instances for testing
-# Untrusted (Landing)
+# # Untrusted (Landing)
# module "test-vm-landing-untrusted-ew1-0" {
# source = "../../../modules/compute-vm"
@@ -26,16 +26,15 @@
# network_interfaces = [{
# network = module.landing-untrusted-vpc.self_link
# subnetwork = module.landing-untrusted-vpc.subnet_self_links["europe-west1/landing-untrusted-default-ew1"]
-# alias_ips = {}
-# nat = false
-# addresses = null
# }]
# tags = ["ew1", "ssh"]
# service_account_create = true
# boot_disk = {
# image = "projects/debian-cloud/global/images/family/debian-10"
-# type = "pd-balanced"
-# size = 10
+# }
+# options = {
+# spot = true
+# termination_action = "STOP"
# }
# metadata = {
# startup-script = <net-cloudnat
· net-vpc
· net-vpc-firewall
· project
| |
| [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. | | local_file
|
+| [outputs.tf](./outputs.tf) | Module outputs. | | google_storage_bucket_object
· local_file
|
| [peerings.tf](./peerings.tf) | None | net-vpc-peering
| |
| [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
|
@@ -287,31 +295,33 @@ DNS configurations are centralised in the `dns.tf` file. Spokes delegate DNS res
| name | description | type | required | default | producer |
|---|---|:---:|:---:|:---:|:---:|
-| [billing_account](variables.tf#L17) | Billing account id and organization id ('nnnnnnnn' or null). | object({…})
| ✓ | | 00-bootstrap
|
-| [folder_ids](variables.tf#L66) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…})
| ✓ | | 01-resman
|
-| [organization](variables.tf#L94) | Organization details. | object({…})
| ✓ | | 00-bootstrap
|
-| [prefix](variables.tf#L110) | Prefix used for resources that need unique names. Use 9 characters or less. | string
| ✓ | | 00-bootstrap
|
-| [custom_adv](variables.tf#L26) | Custom advertisement definitions in name => range format. | map(string)
| | {…}
| |
-| [custom_roles](variables.tf#L43) | Custom roles defined at the org level, in key => id format. | object({…})
| | null
| 00-bootstrap
|
-| [data_dir](variables.tf#L52) | Relative path for the folder storing configuration data for network resources. | string
| | "data"
| |
-| [dns](variables.tf#L58) | Onprem DNS resolvers. | map(list(string))
| | {…}
| |
-| [l7ilb_subnets](variables.tf#L76) | Subnets used for L7 ILBs. | map(list(object({…})))
| | {…}
| |
-| [outputs_location](variables.tf#L104) | 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({…})
| ✓ | | 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
| |
| [peering_configs](variables-peerings.tf#L19) | Peering configurations. | map(object({…}))
| | {…}
| |
-| [psa_ranges](variables.tf#L121) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…})
| | null
| |
-| [router_onprem_configs](variables.tf#L158) | Configurations for routers used for onprem connectivity. | map(object({…}))
| | {…}
| |
-| [service_accounts](variables.tf#L176) | Automation service accounts in name => email format. | object({…})
| | null
| 01-resman
|
-| [vpn_onprem_configs](variables.tf#L188) | VPN gateway configuration for onprem interconnection. | 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({…}))
| | {…}
| |
## Outputs
| name | description | sensitive | consumers |
|---|---|:---:|---|
-| [cloud_dns_inbound_policy](outputs.tf#L57) | IP Addresses for Cloud DNS inbound policy. | | |
-| [host_project_ids](outputs.tf#L62) | Network project ids. | | |
-| [host_project_numbers](outputs.tf#L67) | Network project numbers. | | |
-| [shared_vpc_self_links](outputs.tf#L72) | Shared VPC host projects. | | |
-| [tfvars](outputs.tf#L87) | Terraform variables file for the following stages. | ✓ | |
-| [vpn_gateway_endpoints](outputs.tf#L77) | External IP Addresses for the GCP VPN gateways. | | |
+| [cloud_dns_inbound_policy](outputs.tf#L63) | IP Addresses for Cloud DNS inbound policy. | | |
+| [host_project_ids](outputs.tf#L68) | Network project ids. | | |
+| [host_project_numbers](outputs.tf#L73) | Network project numbers. | | |
+| [shared_vpc_self_links](outputs.tf#L78) | Shared VPC host projects. | | |
+| [tfvars](outputs.tf#L83) | Terraform variables file for the following stages. | ✓ | |
+| [vpn_gateway_endpoints](outputs.tf#L89) | External IP Addresses for the GCP VPN gateways. | | |
diff --git a/fast/stages/02-networking-peering/data/firewall-rules/dev/rules.yaml b/fast/stages/02-networking-peering/data/firewall-rules/dev/rules.yaml
index d4df8cdc31..cab42edc94 100644
--- a/fast/stages/02-networking-peering/data/firewall-rules/dev/rules.yaml
+++ b/fast/stages/02-networking-peering/data/firewall-rules/dev/rules.yaml
@@ -1,27 +1,21 @@
# skip boilerplate check
-ingress-allow-composer-nodes:
- description: "Allow traffic to Composer nodes."
- direction: INGRESS
- action: allow
- sources: []
- ranges: ["0.0.0.0/0"]
- targets:
- - composer-worker
- use_service_accounts: false
- rules:
- - protocol: tcp
- ports: [80, 443, 3306, 3307]
-
-ingress-allow-dataflow-load:
- description: "Allow traffic to Dataflow nodes."
- direction: INGRESS
- action: allow
- sources: []
- ranges: ["0.0.0.0/0"]
- targets:
- - dataflow
- use_service_accounts: false
- rules:
- - protocol: tcp
- ports: [12345, 12346]
+ingress:
+ ingress-allow-composer-nodes:
+ description: "Allow traffic to Composer nodes."
+ sources:
+ - composer-worker
+ targets:
+ - composer-worker
+ rules:
+ - protocol: tcp
+ ports: [80, 443, 3306, 3307]
+ ingress-allow-dataflow-load:
+ description: "Allow traffic to Dataflow nodes."
+ sources:
+ - dataflow
+ targets:
+ - dataflow
+ rules:
+ - protocol: tcp
+ ports: [12345, 12346]
diff --git a/fast/stages/02-networking-peering/data/firewall-rules/landing/rules.yaml b/fast/stages/02-networking-peering/data/firewall-rules/landing/rules.yaml
index e72b7c9c7d..3c1425a7c0 100644
--- a/fast/stages/02-networking-peering/data/firewall-rules/landing/rules.yaml
+++ b/fast/stages/02-networking-peering/data/firewall-rules/landing/rules.yaml
@@ -1,15 +1,11 @@
# skip boilerplate check
-allow-onprem-probes-example:
- description: "Allow traffic from onprem probes"
- direction: INGRESS
- action: allow
- sources: []
- ranges:
- - $onprem_probes
- targets: []
- use_service_accounts: false
- rules:
- - protocol: tcp
- ports:
- - 12345
+ingress:
+ allow-onprem-probes-example:
+ description: "Allow traffic from onprem probes"
+ source_ranges:
+ - onprem_probes
+ rules:
+ - protocol: tcp
+ ports:
+ - 12345
diff --git a/fast/stages/02-networking-peering/data/subnets/dev/dev-gke-nodes-ew1.yaml b/fast/stages/02-networking-peering/data/subnets/dev/dev-gke-nodes-ew1.yaml
new file mode 100644
index 0000000000..c2b5cbe712
--- /dev/null
+++ b/fast/stages/02-networking-peering/data/subnets/dev/dev-gke-nodes-ew1.yaml
@@ -0,0 +1,8 @@
+# skip boilerplate check
+
+region: europe-west1
+description: Default subnet for prod gke nodes
+ip_cidr_range: 10.64.0.0/24
+secondary_ip_range:
+ pods: 100.64.0.0/16
+ services: 192.168.1.0/24
diff --git a/fast/stages/02-networking-peering/dns-dev.tf b/fast/stages/02-networking-peering/dns-dev.tf
index aad50afc3f..03ae01221c 100644
--- a/fast/stages/02-networking-peering/dns-dev.tf
+++ b/fast/stages/02-networking-peering/dns-dev.tf
@@ -26,7 +26,7 @@ module "dev-dns-private-zone" {
domain = "dev.gcp.example.com."
client_networks = [module.landing-vpc.self_link]
recordsets = {
- "A localhost" = { type = "A", ttl = 300, records = ["127.0.0.1"] }
+ "A localhost" = { records = ["127.0.0.1"] }
}
}
diff --git a/fast/stages/02-networking-peering/dns-landing.tf b/fast/stages/02-networking-peering/dns-landing.tf
index b1d766ab24..7b97a8cfd5 100644
--- a/fast/stages/02-networking-peering/dns-landing.tf
+++ b/fast/stages/02-networking-peering/dns-landing.tf
@@ -46,11 +46,11 @@ module "gcp-example-dns-private-zone" {
domain = "gcp.example.com."
client_networks = [module.landing-vpc.self_link]
recordsets = {
- "A localhost" = { type = "A", ttl = 300, records = ["127.0.0.1"] }
+ "A localhost" = { records = ["127.0.0.1"] }
}
}
-# Google API zone to trigger Private Access
+# Google APIs
module "googleapis-private-zone" {
source = "../../../modules/dns"
@@ -60,12 +60,72 @@ module "googleapis-private-zone" {
domain = "googleapis.com."
client_networks = [module.landing-vpc.self_link]
recordsets = {
- "A private" = { type = "A", ttl = 300, records = [
+ "A private" = { records = [
"199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
] }
- "A restricted" = { type = "A", ttl = 300, records = [
+ "A restricted" = { records = [
"199.36.153.4", "199.36.153.5", "199.36.153.6", "199.36.153.7"
] }
- "CNAME *" = { type = "CNAME", ttl = 300, records = ["private.googleapis.com."] }
+ "CNAME *" = { records = ["private.googleapis.com."] }
+ }
+}
+
+module "gcrio-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.landing-project.project_id
+ type = "private"
+ name = "gcr-io"
+ domain = "gcr.io."
+ client_networks = [module.landing-vpc.self_link]
+ recordsets = {
+ "A gcr.io." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
+ }
+}
+
+module "packages-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.landing-project.project_id
+ type = "private"
+ name = "packages-cloud"
+ domain = "packages.cloud.google.com."
+ client_networks = [module.landing-vpc.self_link]
+ recordsets = {
+ "A packages.cloud.google.com." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
+ }
+}
+
+module "pkgdev-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.landing-project.project_id
+ type = "private"
+ name = "pkg-dev"
+ domain = "pkg.dev."
+ client_networks = [module.landing-vpc.self_link]
+ recordsets = {
+ "A pkg.dev." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
+ }
+}
+
+module "pkigoog-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.landing-project.project_id
+ type = "private"
+ name = "pki-goog"
+ domain = "pki.goog."
+ client_networks = [module.landing-vpc.self_link]
+ recordsets = {
+ "A pki.goog." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
}
}
diff --git a/fast/stages/02-networking-peering/dns-prod.tf b/fast/stages/02-networking-peering/dns-prod.tf
index a4a916b468..5bb695fdcb 100644
--- a/fast/stages/02-networking-peering/dns-prod.tf
+++ b/fast/stages/02-networking-peering/dns-prod.tf
@@ -26,7 +26,7 @@ module "prod-dns-private-zone" {
domain = "prod.gcp.example.com."
client_networks = [module.landing-vpc.self_link]
recordsets = {
- "A localhost" = { type = "A", ttl = 300, records = ["127.0.0.1"] }
+ "A localhost" = { records = ["127.0.0.1"] }
}
}
diff --git a/fast/stages/02-networking-peering/landing.tf b/fast/stages/02-networking-peering/landing.tf
index fae959570f..83a0d509af 100644
--- a/fast/stages/02-networking-peering/landing.tf
+++ b/fast/stages/02-networking-peering/landing.tf
@@ -22,10 +22,6 @@ module "landing-project" {
name = "prod-net-landing-0"
parent = var.folder_ids.networking-prod
prefix = var.prefix
- service_config = {
- disable_on_destroy = false
- disable_dependent_services = false
- }
services = [
"compute.googleapis.com",
"dns.googleapis.com",
@@ -34,14 +30,15 @@ module "landing-project" {
"stackdriver.googleapis.com"
]
shared_vpc_host_config = {
- enabled = true
- service_projects = []
+ enabled = true
}
iam = {
- "roles/dns.admin" = [local.service_accounts.project-factory-prod]
- (local.custom_roles.service_project_network_admin) = [
- local.service_accounts.project-factory-prod
- ]
+ "roles/dns.admin" = compact([
+ try(local.service_accounts.project-factory-prod, null)
+ ])
+ (local.custom_roles.service_project_network_admin) = compact([
+ try(local.service_accounts.project-factory-prod, null)
+ ])
}
}
@@ -51,23 +48,17 @@ module "landing-vpc" {
name = "prod-landing-0"
mtu = 1500
dns_policy = {
- inbound = true
- logging = false
- outbound = null
+ inbound = true
}
# set explicit routes for googleapis in case the default route is deleted
routes = {
private-googleapis = {
dest_range = "199.36.153.8/30"
- priority = 1000
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
restricted-googleapis = {
dest_range = "199.36.153.4/30"
- priority = 1000
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
@@ -76,15 +67,16 @@ module "landing-vpc" {
}
module "landing-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.landing-project.project_id
- network = module.landing-vpc.name
- admin_ranges = []
- http_source_ranges = []
- https_source_ranges = []
- ssh_source_ranges = []
- data_folder = "${var.data_dir}/firewall-rules/landing"
- cidr_template_file = "${var.data_dir}/cidrs.yaml"
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.landing-project.project_id
+ network = module.landing-vpc.name
+ default_rules_config = {
+ disabled = true
+ }
+ factories_config = {
+ cidr_tpl_file = "${var.data_dir}/cidrs.yaml"
+ rules_folder = "${var.data_dir}/firewall-rules/landing"
+ }
}
module "landing-nat-ew1" {
diff --git a/fast/stages/02-networking-peering/main.tf b/fast/stages/02-networking-peering/main.tf
index 5df6d604e1..f68d39eb85 100644
--- a/fast/stages/02-networking-peering/main.tf
+++ b/fast/stages/02-networking-peering/main.tf
@@ -25,18 +25,17 @@ locals {
name = "${env}-l7ilb-${s.region}"
})]
}
- region_trigram = {
- europe-west1 = "ew1"
- europe-west3 = "ew3"
- }
stage3_sas_delegated_grants = [
"roles/composer.sharedVpcAgent",
"roles/compute.networkUser",
+ "roles/compute.networkViewer",
"roles/container.hostServiceAgentUser",
+ "roles/multiclusterservicediscovery.serviceAgent",
"roles/vpcaccess.user",
]
service_accounts = {
- for k, v in coalesce(var.service_accounts, {}) : k => "serviceAccount:${v}"
+ for k, v in coalesce(var.service_accounts, {}) :
+ k => "serviceAccount:${v}" if v != null
}
}
diff --git a/fast/stages/02-networking-peering/outputs.tf b/fast/stages/02-networking-peering/outputs.tf
index 3fe18d6575..3b97b7f254 100644
--- a/fast/stages/02-networking-peering/outputs.tf
+++ b/fast/stages/02-networking-peering/outputs.tf
@@ -43,15 +43,21 @@ locals {
}
}
-# optionally generate tfvars file for subsequent stages
+# generate tfvars file for subsequent stages
resource "local_file" "tfvars" {
for_each = var.outputs_location == null ? {} : { 1 = 1 }
file_permission = "0644"
- filename = "${pathexpand(var.outputs_location)}/tfvars/02-networking.auto.tfvars.json"
+ filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/02-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"
+ content = jsonencode(local.tfvars)
+}
+
# outputs
output "cloud_dns_inbound_policy" {
@@ -74,6 +80,12 @@ output "shared_vpc_self_links" {
value = local.vpc_self_links
}
+output "tfvars" {
+ description = "Terraform variables file for the following stages."
+ sensitive = true
+ value = local.tfvars
+}
+
output "vpn_gateway_endpoints" {
description = "External IP Addresses for the GCP VPN gateways."
value = local.enable_onprem_vpn == false ? null : {
@@ -83,9 +95,3 @@ output "vpn_gateway_endpoints" {
}
}
}
-
-output "tfvars" {
- description = "Terraform variables file for the following stages."
- sensitive = true
- value = local.tfvars
-}
diff --git a/fast/stages/02-networking-peering/spoke-dev.tf b/fast/stages/02-networking-peering/spoke-dev.tf
index 268f5b703b..e67cfb70db 100644
--- a/fast/stages/02-networking-peering/spoke-dev.tf
+++ b/fast/stages/02-networking-peering/spoke-dev.tf
@@ -22,10 +22,6 @@ module "dev-spoke-project" {
name = "dev-net-spoke-0"
parent = var.folder_ids.networking-dev
prefix = var.prefix
- service_config = {
- disable_on_destroy = false
- disable_dependent_services = false
- }
services = [
"container.googleapis.com",
"compute.googleapis.com",
@@ -36,36 +32,34 @@ module "dev-spoke-project" {
"stackdriver.googleapis.com",
]
shared_vpc_host_config = {
- enabled = true
- service_projects = []
+ enabled = true
}
metric_scopes = [module.landing-project.project_id]
iam = {
- "roles/dns.admin" = [local.service_accounts.project-factory-dev]
+ "roles/dns.admin" = compact([
+ try(local.service_accounts.gke-dev, null),
+ try(local.service_accounts.project-factory-dev, null),
+ ])
}
}
module "dev-spoke-vpc" {
- source = "../../../modules/net-vpc"
- project_id = module.dev-spoke-project.project_id
- name = "dev-spoke-0"
- mtu = 1500
- data_folder = "${var.data_dir}/subnets/dev"
- psa_config = try(var.psa_ranges.dev, null)
- subnets_l7ilb = local.l7ilb_subnets.dev
+ source = "../../../modules/net-vpc"
+ project_id = module.dev-spoke-project.project_id
+ name = "dev-spoke-0"
+ mtu = 1500
+ data_folder = "${var.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
routes = {
private-googleapis = {
dest_range = "199.36.153.8/30"
- priority = 1000
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
restricted-googleapis = {
dest_range = "199.36.153.4/30"
- priority = 1000
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
@@ -73,15 +67,16 @@ module "dev-spoke-vpc" {
}
module "dev-spoke-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.dev-spoke-project.project_id
- network = module.dev-spoke-vpc.name
- admin_ranges = []
- http_source_ranges = []
- https_source_ranges = []
- ssh_source_ranges = []
- data_folder = "${var.data_dir}/firewall-rules/dev"
- cidr_template_file = "${var.data_dir}/cidrs.yaml"
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.dev-spoke-project.project_id
+ network = module.dev-spoke-vpc.name
+ default_rules_config = {
+ disabled = true
+ }
+ factories_config = {
+ cidr_tpl_file = "${var.data_dir}/cidrs.yaml"
+ rules_folder = "${var.data_dir}/firewall-rules/dev"
+ }
}
module "dev-spoke-cloudnat" {
@@ -89,7 +84,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-${var.region_trigram[each.value]}"
router_create = true
router_network = module.dev-spoke-vpc.name
router_asn = 4200001024
@@ -100,10 +95,11 @@ module "dev-spoke-cloudnat" {
resource "google_project_iam_binding" "dev_spoke_project_iam_delegated" {
project = module.dev-spoke-project.project_id
role = "roles/resourcemanager.projectIamAdmin"
- members = [
- local.service_accounts.data-platform-dev,
- local.service_accounts.project-factory-dev,
- ]
+ members = compact([
+ try(local.service_accounts.data-platform-dev, null),
+ try(local.service_accounts.project-factory-dev, null),
+ try(local.service_accounts.gke-dev, null),
+ ])
condition {
title = "dev_stage3_sa_delegated_grants"
description = "Development host project delegated grants."
diff --git a/fast/stages/02-networking-peering/spoke-prod.tf b/fast/stages/02-networking-peering/spoke-prod.tf
index cf83d2e1e7..cf49152fa1 100644
--- a/fast/stages/02-networking-peering/spoke-prod.tf
+++ b/fast/stages/02-networking-peering/spoke-prod.tf
@@ -22,10 +22,6 @@ module "prod-spoke-project" {
name = "prod-net-spoke-0"
parent = var.folder_ids.networking-prod
prefix = var.prefix
- service_config = {
- disable_on_destroy = false
- disable_dependent_services = false
- }
services = [
"container.googleapis.com",
"compute.googleapis.com",
@@ -36,36 +32,34 @@ module "prod-spoke-project" {
"stackdriver.googleapis.com",
]
shared_vpc_host_config = {
- enabled = true
- service_projects = []
+ enabled = true
}
metric_scopes = [module.landing-project.project_id]
iam = {
- "roles/dns.admin" = [local.service_accounts.project-factory-prod]
+ "roles/dns.admin" = compact([
+ try(local.service_accounts.gke-prod, null),
+ try(local.service_accounts.project-factory-prod, null),
+ ])
}
}
module "prod-spoke-vpc" {
- source = "../../../modules/net-vpc"
- project_id = module.prod-spoke-project.project_id
- name = "prod-spoke-0"
- mtu = 1500
- data_folder = "${var.data_dir}/subnets/prod"
- psa_config = try(var.psa_ranges.prod, null)
- subnets_l7ilb = local.l7ilb_subnets.prod
+ source = "../../../modules/net-vpc"
+ project_id = module.prod-spoke-project.project_id
+ name = "prod-spoke-0"
+ mtu = 1500
+ data_folder = "${var.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
routes = {
private-googleapis = {
dest_range = "199.36.153.8/30"
- priority = 1000
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
restricted-googleapis = {
dest_range = "199.36.153.4/30"
- priority = 1000
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
@@ -73,15 +67,16 @@ module "prod-spoke-vpc" {
}
module "prod-spoke-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.prod-spoke-project.project_id
- network = module.prod-spoke-vpc.name
- admin_ranges = []
- http_source_ranges = []
- https_source_ranges = []
- ssh_source_ranges = []
- data_folder = "${var.data_dir}/firewall-rules/prod"
- cidr_template_file = "${var.data_dir}/cidrs.yaml"
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.prod-spoke-project.project_id
+ network = module.prod-spoke-vpc.name
+ default_rules_config = {
+ disabled = true
+ }
+ factories_config = {
+ cidr_tpl_file = "${var.data_dir}/cidrs.yaml"
+ rules_folder = "${var.data_dir}/firewall-rules/prod"
+ }
}
module "prod-spoke-cloudnat" {
@@ -89,7 +84,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-${var.region_trigram[each.value]}"
router_create = true
router_network = module.prod-spoke-vpc.name
router_asn = 4200001024
@@ -100,10 +95,11 @@ module "prod-spoke-cloudnat" {
resource "google_project_iam_binding" "prod_spoke_project_iam_delegated" {
project = module.prod-spoke-project.project_id
role = "roles/resourcemanager.projectIamAdmin"
- members = [
- local.service_accounts.data-platform-prod,
- local.service_accounts.project-factory-prod,
- ]
+ members = compact([
+ try(local.service_accounts.data-platform-prod, null),
+ try(local.service_accounts.project-factory-prod, null),
+ try(local.service_accounts.gke-prod, null),
+ ])
condition {
title = "prod_stage3_sa_delegated_grants"
description = "Production host project delegated grants."
diff --git a/fast/stages/02-networking-peering/test-resources.tf b/fast/stages/02-networking-peering/test-resources.tf
index 8139e75514..204971fec8 100644
--- a/fast/stages/02-networking-peering/test-resources.tf
+++ b/fast/stages/02-networking-peering/test-resources.tf
@@ -24,16 +24,15 @@
# network_interfaces = [{
# network = module.landing-vpc.self_link
# subnetwork = module.landing-vpc.subnet_self_links["europe-west1/landing-default-ew1"]
-# alias_ips = {}
-# nat = false
-# addresses = null
# }]
# tags = ["ssh"]
# service_account_create = true
# boot_disk = {
# image = "projects/debian-cloud/global/images/family/debian-10"
-# type = "pd-balanced"
-# size = 10
+# }
+# options = {
+# spot = true
+# termination_action = "STOP"
# }
# metadata = {
# startup-script = <+
additive, •
conditional.
+
+## Project dev-net-spoke-0
+
+| members | roles |
+|---|---|
+|dev-resman-pf-0•
•
+ +
+ +## Design overview and choices + +### VPC design + +This architecture creates one VPC for each environment, each in its respective project. Each VPC hosts external connectivity and shared services solely serving its own environment. + +As each environment is fully independent, this design trivialises the creation of new environments. + +### External connectivity + +External connectivity to on-prem is implemented here via HA VPN (two tunnels per region), as this is the minimum common denominator often used directly, or as a stop-gap solution to validate routing and transfer data, while waiting for [interconnects](https://cloud.google.com/network-connectivity/docs/interconnect) to be provisioned. + +Connectivity to additional on-prem sites or other cloud providers should be implemented in a similar fashion, via VPN tunnels or interconnects on each of the environment VPCs, sharing the same regional router. + +### IP ranges, subnetting, routing + +Minimizing the number of routes (and subnets) in use on the cloud environment is an important consideration, as it simplifies management and avoids hitting [Cloud Router](https://cloud.google.com/network-connectivity/docs/router/quotas) and [VPC](https://cloud.google.com/vpc/docs/quota) quotas and limits. For this reason, we recommend careful planning of the IP space used in your cloud environment, to be able to use large IP CIDR blocks in routes whenever possible. + +This stage uses a dedicated /16 block (which should of course be sized to your needs) shared by all regions and environments, and subnets created in each VPC derive their ranges from their relevant block. + +Each VPC also defines and reserves two "special" CIDR ranges dedicated to [PSA (Private Service Access)](https://cloud.google.com/vpc/docs/private-services-access) and [Internal HTTPs Load Balancers (L7ILB)](https://cloud.google.com/load-balancing/docs/l7-internal). + +Routes in GCP are either automatically created for VPC subnets, manually created via static routes, or dynamically programmed by [Cloud Routers](https://cloud.google.com/network-connectivity/docs/router#docs) via BGP sessions, which can be configured to advertise VPC ranges, and/or custom ranges via custom advertisements. + +In this setup: + +- routes between multiple subnets within the same VPC are automatically programmed by GCP +- on-premises is connected to each environment VPC and dynamically exchanges BGP routes with GCP using HA VPN + +### Internet egress + +The path of least resistance for Internet egress is using Cloud NAT, and that is what's implemented in this setup, with a NAT gateway configured for each VPC. + +Several other scenarios are possible of course, with varying degrees of complexity: + +- a forward proxy, with optional URL filters +- a default route to on-prem to leverage existing egress infrastructure +- a full-fledged perimeter firewall to control egress and implement additional security features like IPS + +Future pluggable modules will allow to easily experiment, or deploy the above scenarios. + +### VPC and Hierarchical Firewall + +The GCP Firewall is a stateful, distributed feature that allows the creation of L4 policies, either via VPC-level rules or more recently via hierarchical policies applied on the resource hierarchy (organization, folders). + +The current setup adopts both firewall types, and uses hierarchical rules on the Networking folder for common ingress rules (egress is open by default), e.g. from health check or IAP forwarders ranges, and VPC rules for the environment or workload-level ingress. + +Rules and policies are defined in simple YAML files, described below. + +### DNS + +DNS often goes hand in hand with networking, especially on GCP where Cloud DNS zones and policies are associated at the VPC level. This setup implements both DNS flows: + +- on-prem to cloud via private zones for cloud-managed domains, and an [inbound policy](https://cloud.google.com/dns/docs/server-policies-overview#dns-server-policy-in) used as forwarding target or via delegation (requires some extra configuration) from on-prem DNS resolvers +- cloud to on-prem via forwarding zones for the on-prem managed domains +- Private Google Access is enabled for a selection of the [supported domains](https://cloud.google.com/vpc/docs/configure-private-google-access#domain-options), namely + - `private.googleapis.com` + - `restricted.googleapis.com` + - `gcr.io` + - `packages.cloud.google.com` + - `pkg.dev` + - `pki.goog` + +To complete the configuration, the 35.199.192.0/19 range should be routed on the VPN tunnels from on-prem, and the following names configured for DNS forwarding to cloud: + +- `private.googleapis.com` +- `restricted.googleapis.com` +- `gcp.example.com` (used as a placeholder) + +From cloud, the `example.com` domain (used as a placeholder) is forwarded to on-prem. + +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. + +### VPCs + +VPCs are defined in separate files, one for each of `prod` and `dev`. +Each file contains the same resources, described in the following paragraphs. + +The **project** ([`project`](../../../modules/project)) contains the VPC, and enables the required APIs and sets itself as a "[host project](https://cloud.google.com/vpc/docs/shared-vpc)". + +The **VPC** ([`net-vpc`](../../../modules/net-vpc)) manages the DNS inbound policy, explicit routes for `{private,restricted}.googleapis.com`, and its **subnets**. Subnets are created leveraging a "resource factory" paradigm, where the configuration is separated from the module that implements it, and stored in a well-structured file. To add a new subnet, simply create a new file in the `data_folder` directory defined in the module, following the examples found in the [Fabric `net-vpc` documentation](../../../modules/net-vpc#subnet-factory). Sample subnets are shipped in [data/subnets](./data/subnets), and can be easily customised to fit your needs. + +Subnets for [L7 ILBs](https://cloud.google.com/load-balancing/docs/l7-internal/proxy-only-subnets) are handled differently, and defined in variable `l7ilb_subnets`, while ranges for [PSA](https://cloud.google.com/vpc/docs/configure-private-services-access#allocating-range) are configured by variable `psa_ranges` - such variables are consumed by spoke VPCs. + +**Cloud NAT** ([`net-cloudnat`](../../../modules/net-cloudnat)) manages the networking infrastructure required to enable internet egress. + +### VPNs + +Connectivity to on-prem is implemented with HA VPN ([`net-vpn`](../../../modules/net-vpn-ha)) and defined in `vpn-onprem-{dev,prod}.tf`. The files provisionally implement each a single logical connection between onprem and environment at `europe-west1`, and the relevant parameters for its configuration are found in variable `vpn_onprem_configs`. + +### Routing and BGP + +Each VPC network ([`net-vpc`](../../../modules/net-vpc)) manages a separate routing table, which can define static routes (e.g. to private.googleapis.com) and receives dynamic routes from BGP sessions established with neighbor networks (e.g. from onprem). + +Static routes are defined in `net-*.tf` files, in the `routes` section of each `net-vpc` module. + +### Firewall + +**VPC firewall rules** ([`net-vpc-firewall`](../../../modules/net-vpc-firewall)) are defined per-vpc on each `net-*.tf` file and leverage a resource factory to massively create rules. +To add a new firewall rule, create a new file or edit an existing one in the `data_folder` directory defined in the module `net-vpc-firewall`, following the examples of the "[Rules factory](../../../modules/net-vpc-firewall#rules-factory)" section of the module documentation. Sample firewall rules are shipped in [data/firewall-rules/dev](./data/firewall-rules/dev) and can be easily customised. + +**Hierarchical firewall policies** ([`folder`](../../../modules/folder)) are defined in `main.tf`, and managed through a policy factory implemented by the `folder` module, which applies the defined hierarchical to the `Networking` folder, which contains all the core networking infrastructure. Policies are defined in the `rules_file` file - to define a new one simply use the instructions found on "[Firewall policy factory](../../../modules/organization#firewall-policy-factory)". Sample hierarchical firewall policies are shipped in [data/hierarchical-policy-rules.yaml](./data/hierarchical-policy-rules.yaml) and can be easily customised. + +### DNS architecture + +The DNS ([`dns`](../../../modules/dns)) infrastructure is defined in the respective `dns-xxx.tf` files. + +Cloud DNS manages onprem forwarding and environment-specific zones (i.e. `dev.gcp.example.com` and `prod.gcp.example.com`). + +#### Cloud to on-prem + +Leveraging the forwarding zones defined on each environment, the cloud environment can resolve `in-addr.arpa.` and `onprem.example.com.` using the on-premises DNS infrastructure. Onprem resolvers IPs are set in variable `dns`. + +DNS queries sent to the on-premises infrastructure come from the `35.199.192.0/19` source range, which is only accessible from within a VPC or networks connected to one. + +When implementing this architecture, make sure you'll be able to route packets coming from the /19 range to the right environment (route to prod requests coming from prod and to dev for requests coming from dev). As an alternative, consider leveraging self-managed DNS resolvers (e.g. CoreDNS forwarders) on each environment. + +#### On-prem to cloud + +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 + +[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 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` + +### 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. + + + + +## Files + +| name | description | modules | resources | +|---|---|---|---| +| [dns-dev.tf](./dns-dev.tf) | Development spoke DNS zones and peerings setup. |dns
| |
+| [dns-prod.tf](./dns-prod.tf) | Production spoke DNS zones and peerings setup. | 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
|
+| [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
| |
+| [variables.tf](./variables.tf) | Module variables. | | |
+| [vpn-onprem-dev.tf](./vpn-onprem-dev.tf) | VPN between dev and onprem. | net-vpn-ha
| |
+| [vpn-onprem-prod.tf](./vpn-onprem-prod.tf) | VPN between prod and onprem. | 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#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({…}))
| | {…}
| |
+
+## Outputs
+
+| name | description | sensitive | consumers |
+|---|---|:---:|---|
+| [dev_cloud_dns_inbound_policy](outputs.tf#L59) | IP Addresses for Cloud DNS inbound policy for the dev environment. | | |
+| [host_project_ids](outputs.tf#L64) | Network project ids. | | |
+| [host_project_numbers](outputs.tf#L69) | Network project numbers. | | |
+| [prod_cloud_dns_inbound_policy](outputs.tf#L74) | IP Addresses for Cloud DNS inbound policy for the prod environment. | | |
+| [shared_vpc_self_links](outputs.tf#L79) | Shared VPC host projects. | | |
+| [tfvars](outputs.tf#L84) | Terraform variables file for the following stages. | ✓ | |
+| [vpn_gateway_endpoints](outputs.tf#L90) | External IP Addresses for the GCP VPN gateways. | | |
+
+
diff --git a/fast/stages/02-networking-separate-envs/data/cidrs.yaml b/fast/stages/02-networking-separate-envs/data/cidrs.yaml
new file mode 100644
index 0000000000..b6c25e21ab
--- /dev/null
+++ b/fast/stages/02-networking-separate-envs/data/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
diff --git a/fast/stages/02-networking-separate-envs/data/dashboards/firewall_insights.json b/fast/stages/02-networking-separate-envs/data/dashboards/firewall_insights.json
new file mode 100644
index 0000000000..e829091cfe
--- /dev/null
+++ b/fast/stages/02-networking-separate-envs/data/dashboards/firewall_insights.json
@@ -0,0 +1,68 @@
+{
+ "displayName": "Firewall Insights Monitoring",
+ "gridLayout": {
+ "columns": "2",
+ "widgets": [
+ {
+ "title": "Subnet Firewall Hit Counts",
+ "xyChart": {
+ "chartOptions": {
+ "mode": "COLOR"
+ },
+ "dataSets": [
+ {
+ "minAlignmentPeriod": "60s",
+ "plotType": "LINE",
+ "targetAxis": "Y1",
+ "timeSeriesQuery": {
+ "timeSeriesFilter": {
+ "aggregation": {
+ "perSeriesAligner": "ALIGN_RATE"
+ },
+ "filter": "metric.type=\"firewallinsights.googleapis.com/subnet/firewall_hit_count\" resource.type=\"gce_subnetwork\"",
+ "secondaryAggregation": {}
+ },
+ "unitOverride": "1"
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ {
+ "title": "VM Firewall Hit Counts",
+ "xyChart": {
+ "chartOptions": {
+ "mode": "COLOR"
+ },
+ "dataSets": [
+ {
+ "minAlignmentPeriod": "60s",
+ "plotType": "LINE",
+ "targetAxis": "Y1",
+ "timeSeriesQuery": {
+ "timeSeriesFilter": {
+ "aggregation": {
+ "perSeriesAligner": "ALIGN_RATE"
+ },
+ "filter": "metric.type=\"firewallinsights.googleapis.com/vm/firewall_hit_count\" resource.type=\"gce_instance\"",
+ "secondaryAggregation": {}
+ },
+ "unitOverride": "1"
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/fast/stages/02-networking-separate-envs/data/dashboards/vpn.json b/fast/stages/02-networking-separate-envs/data/dashboards/vpn.json
new file mode 100644
index 0000000000..4396cc00b6
--- /dev/null
+++ b/fast/stages/02-networking-separate-envs/data/dashboards/vpn.json
@@ -0,0 +1,248 @@
+{
+ "displayName": "VPN Monitoring",
+ "gridLayout": {
+ "columns": "2",
+ "widgets": [
+ {
+ "title": "Number of connections",
+ "xyChart": {
+ "chartOptions": {
+ "mode": "COLOR"
+ },
+ "dataSets": [
+ {
+ "minAlignmentPeriod": "60s",
+ "plotType": "LINE",
+ "targetAxis": "Y1",
+ "timeSeriesQuery": {
+ "timeSeriesFilter": {
+ "aggregation": {
+ "perSeriesAligner": "ALIGN_MEAN"
+ },
+ "filter": "metric.type=\"vpn.googleapis.com/gateway/connections\" resource.type=\"vpn_gateway\"",
+ "secondaryAggregation": {}
+ },
+ "unitOverride": "1"
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ {
+ "title": "Tunnel established",
+ "xyChart": {
+ "chartOptions": {
+ "mode": "COLOR"
+ },
+ "dataSets": [
+ {
+ "minAlignmentPeriod": "60s",
+ "plotType": "LINE",
+ "targetAxis": "Y1",
+ "timeSeriesQuery": {
+ "timeSeriesFilter": {
+ "aggregation": {
+ "perSeriesAligner": "ALIGN_MEAN"
+ },
+ "filter": "metric.type=\"vpn.googleapis.com/tunnel_established\" resource.type=\"vpn_gateway\"",
+ "secondaryAggregation": {}
+ },
+ "unitOverride": "1"
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ {
+ "title": "Cloud VPN Gateway - Received bytes",
+ "xyChart": {
+ "chartOptions": {
+ "mode": "COLOR"
+ },
+ "dataSets": [
+ {
+ "minAlignmentPeriod": "60s",
+ "plotType": "LINE",
+ "targetAxis": "Y1",
+ "timeSeriesQuery": {
+ "timeSeriesFilter": {
+ "aggregation": {
+ "perSeriesAligner": "ALIGN_RATE"
+ },
+ "filter": "metric.type=\"vpn.googleapis.com/network/received_bytes_count\" resource.type=\"vpn_gateway\"",
+ "secondaryAggregation": {}
+ },
+ "unitOverride": "By"
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ {
+ "title": "Cloud VPN Gateway - Sent bytes",
+ "xyChart": {
+ "chartOptions": {
+ "mode": "COLOR"
+ },
+ "dataSets": [
+ {
+ "minAlignmentPeriod": "60s",
+ "plotType": "LINE",
+ "targetAxis": "Y1",
+ "timeSeriesQuery": {
+ "timeSeriesFilter": {
+ "aggregation": {
+ "perSeriesAligner": "ALIGN_RATE"
+ },
+ "filter": "metric.type=\"vpn.googleapis.com/network/sent_bytes_count\" resource.type=\"vpn_gateway\"",
+ "secondaryAggregation": {}
+ },
+ "unitOverride": "By"
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ {
+ "title": "Cloud VPN Gateway - Received packets",
+ "xyChart": {
+ "chartOptions": {
+ "mode": "COLOR"
+ },
+ "dataSets": [
+ {
+ "minAlignmentPeriod": "60s",
+ "plotType": "LINE",
+ "targetAxis": "Y1",
+ "timeSeriesQuery": {
+ "timeSeriesFilter": {
+ "aggregation": {
+ "perSeriesAligner": "ALIGN_RATE"
+ },
+ "filter": "metric.type=\"vpn.googleapis.com/network/received_packets_count\" resource.type=\"vpn_gateway\"",
+ "secondaryAggregation": {}
+ },
+ "unitOverride": "{packets}"
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ {
+ "title": "Cloud VPN Gateway - Sent packets",
+ "xyChart": {
+ "chartOptions": {
+ "mode": "COLOR"
+ },
+ "dataSets": [
+ {
+ "minAlignmentPeriod": "60s",
+ "plotType": "LINE",
+ "targetAxis": "Y1",
+ "timeSeriesQuery": {
+ "timeSeriesFilter": {
+ "aggregation": {
+ "perSeriesAligner": "ALIGN_RATE"
+ },
+ "filter": "metric.type=\"vpn.googleapis.com/network/sent_packets_count\" resource.type=\"vpn_gateway\"",
+ "secondaryAggregation": {}
+ },
+ "unitOverride": "{packets}"
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ {
+ "title": "Incoming packets dropped",
+ "xyChart": {
+ "chartOptions": {
+ "mode": "COLOR"
+ },
+ "dataSets": [
+ {
+ "minAlignmentPeriod": "60s",
+ "plotType": "LINE",
+ "targetAxis": "Y1",
+ "timeSeriesQuery": {
+ "timeSeriesFilter": {
+ "aggregation": {
+ "perSeriesAligner": "ALIGN_RATE"
+ },
+ "filter": "metric.type=\"vpn.googleapis.com/network/dropped_received_packets_count\" resource.type=\"vpn_gateway\"",
+ "secondaryAggregation": {}
+ },
+ "unitOverride": "1"
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ },
+ {
+ "title": "Outgoing packets dropped",
+ "xyChart": {
+ "chartOptions": {
+ "mode": "COLOR"
+ },
+ "dataSets": [
+ {
+ "minAlignmentPeriod": "60s",
+ "plotType": "LINE",
+ "targetAxis": "Y1",
+ "timeSeriesQuery": {
+ "timeSeriesFilter": {
+ "aggregation": {
+ "perSeriesAligner": "ALIGN_RATE"
+ },
+ "filter": "metric.type=\"vpn.googleapis.com/network/dropped_sent_packets_count\" resource.type=\"vpn_gateway\"",
+ "secondaryAggregation": {}
+ },
+ "unitOverride": "1"
+ }
+ }
+ ],
+ "timeshiftDuration": "0s",
+ "yAxis": {
+ "label": "y1Axis",
+ "scale": "LINEAR"
+ }
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/fast/stages/02-networking-separate-envs/data/firewall-rules/dev/rules.yaml b/fast/stages/02-networking-separate-envs/data/firewall-rules/dev/rules.yaml
new file mode 100644
index 0000000000..67386c4467
--- /dev/null
+++ b/fast/stages/02-networking-separate-envs/data/firewall-rules/dev/rules.yaml
@@ -0,0 +1,17 @@
+# skip boilerplate check
+
+ingress:
+ ingress-allow-composer-nodes:
+ description: "Allow traffic to Composer nodes."
+ targets:
+ - composer-worker
+ rules:
+ - protocol: tcp
+ ports: [80, 443, 3306, 3307]
+ ingress-allow-dataflow-load:
+ description: "Allow traffic to Dataflow nodes."
+ targets:
+ - dataflow
+ rules:
+ - protocol: tcp
+ ports: [12345, 12346]
diff --git a/fast/stages/02-networking-separate-envs/data/hierarchical-policy-rules.yaml b/fast/stages/02-networking-separate-envs/data/hierarchical-policy-rules.yaml
new file mode 100644
index 0000000000..0172a3091e
--- /dev/null
+++ b/fast/stages/02-networking-separate-envs/data/hierarchical-policy-rules.yaml
@@ -0,0 +1,49 @@
+# 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
diff --git a/fast/stages/02-networking-separate-envs/data/subnets/dev/dev-dataplatform-ew1.yaml b/fast/stages/02-networking-separate-envs/data/subnets/dev/dev-dataplatform-ew1.yaml
new file mode 100644
index 0000000000..92994826dc
--- /dev/null
+++ b/fast/stages/02-networking-separate-envs/data/subnets/dev/dev-dataplatform-ew1.yaml
@@ -0,0 +1,8 @@
+# skip boilerplate check
+
+region: europe-west1
+description: Default subnet for dev Data Platform
+ip_cidr_range: 10.128.48.0/24
+secondary_ip_range:
+ pods: 100.128.48.0/20
+ services: 100.255.48.0/24
diff --git a/fast/stages/02-networking-separate-envs/data/subnets/dev/dev-default-ew1.yaml b/fast/stages/02-networking-separate-envs/data/subnets/dev/dev-default-ew1.yaml
new file mode 100644
index 0000000000..8b066ba706
--- /dev/null
+++ b/fast/stages/02-networking-separate-envs/data/subnets/dev/dev-default-ew1.yaml
@@ -0,0 +1,5 @@
+# skip boilerplate check
+
+region: europe-west1
+ip_cidr_range: 10.128.32.0/24
+description: Default subnet for dev
diff --git a/fast/stages/02-networking-separate-envs/data/subnets/prod/prod-default-ew1.yaml b/fast/stages/02-networking-separate-envs/data/subnets/prod/prod-default-ew1.yaml
new file mode 100644
index 0000000000..0052eff95d
--- /dev/null
+++ b/fast/stages/02-networking-separate-envs/data/subnets/prod/prod-default-ew1.yaml
@@ -0,0 +1,5 @@
+# skip boilerplate check
+
+region: europe-west1
+ip_cidr_range: 10.128.64.0/24
+description: Default subnet for prod
diff --git a/fast/stages/02-networking-separate-envs/diagram.png b/fast/stages/02-networking-separate-envs/diagram.png
new file mode 100644
index 0000000000..740b84429a
Binary files /dev/null and b/fast/stages/02-networking-separate-envs/diagram.png differ
diff --git a/fast/stages/02-networking-separate-envs/diagram.svg b/fast/stages/02-networking-separate-envs/diagram.svg
new file mode 100644
index 0000000000..ef569b90e5
--- /dev/null
+++ b/fast/stages/02-networking-separate-envs/diagram.svg
@@ -0,0 +1,2059 @@
+
+
diff --git a/fast/stages/02-networking-separate-envs/dns-dev.tf b/fast/stages/02-networking-separate-envs/dns-dev.tf
new file mode 100644
index 0000000000..25adab5e22
--- /dev/null
+++ b/fast/stages/02-networking-separate-envs/dns-dev.tf
@@ -0,0 +1,131 @@
+/**
+ * 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:file:description Development spoke DNS zones and peerings setup.
+
+# GCP-specific environment zone
+
+module "dev-dns-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.dev-spoke-project.project_id
+ type = "private"
+ name = "dev-gcp-example-com"
+ domain = "dev.gcp.example.com."
+ client_networks = [module.dev-spoke-vpc.self_link]
+ recordsets = {
+ "A localhost" = { records = ["127.0.0.1"] }
+ }
+}
+
+module "dev-onprem-example-dns-forwarding" {
+ source = "../../../modules/dns"
+ project_id = module.dev-spoke-project.project_id
+ type = "forwarding"
+ name = "example-com"
+ domain = "onprem.example.com."
+ client_networks = [module.dev-spoke-vpc.self_link]
+ forwarders = { for ip in var.dns.dev : ip => null }
+}
+
+module "dev-reverse-10-dns-forwarding" {
+ source = "../../../modules/dns"
+ project_id = module.dev-spoke-project.project_id
+ type = "forwarding"
+ name = "root-reverse-10"
+ domain = "10.in-addr.arpa."
+ client_networks = [module.dev-spoke-vpc.self_link]
+ forwarders = { for ip in var.dns.dev : ip => null }
+}
+
+# Google APIs
+
+module "dev-googleapis-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.dev-spoke-project.project_id
+ type = "private"
+ name = "googleapis-com"
+ domain = "googleapis.com."
+ client_networks = [module.dev-spoke-vpc.self_link]
+ recordsets = {
+ "A private" = { records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "A restricted" = { records = [
+ "199.36.153.4", "199.36.153.5", "199.36.153.6", "199.36.153.7"
+ ] }
+ "CNAME *" = { records = ["private.googleapis.com."] }
+ }
+}
+
+module "dev-gcrio-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.dev-spoke-project.project_id
+ type = "private"
+ name = "gcr-io"
+ domain = "gcr.io."
+ client_networks = [module.dev-spoke-vpc.self_link]
+ recordsets = {
+ "A gcr.io." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
+ }
+}
+
+module "dev-packages-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.dev-spoke-project.project_id
+ type = "private"
+ name = "packages-cloud"
+ domain = "packages.cloud.google.com."
+ client_networks = [module.dev-spoke-vpc.self_link]
+ recordsets = {
+ "A packages.cloud.google.com." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
+ }
+}
+
+module "dev-pkgdev-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.dev-spoke-project.project_id
+ type = "private"
+ name = "pkg-dev"
+ domain = "pkg.dev."
+ client_networks = [module.dev-spoke-vpc.self_link]
+ recordsets = {
+ "A pkg.dev." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
+ }
+}
+
+module "dev-pkigoog-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.dev-spoke-project.project_id
+ type = "private"
+ name = "pki-goog"
+ domain = "pki.goog."
+ client_networks = [module.dev-spoke-vpc.self_link]
+ recordsets = {
+ "A pki.goog." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
+ }
+}
diff --git a/fast/stages/02-networking-separate-envs/dns-prod.tf b/fast/stages/02-networking-separate-envs/dns-prod.tf
new file mode 100644
index 0000000000..47c8cdca48
--- /dev/null
+++ b/fast/stages/02-networking-separate-envs/dns-prod.tf
@@ -0,0 +1,131 @@
+/**
+ * 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:file:description Production spoke DNS zones and peerings setup.
+
+# GCP-specific environment zone
+
+module "prod-dns-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.prod-spoke-project.project_id
+ type = "private"
+ name = "prod-gcp-example-com"
+ domain = "prod.gcp.example.com."
+ client_networks = [module.prod-spoke-vpc.self_link]
+ recordsets = {
+ "A localhost" = { records = ["127.0.0.1"] }
+ }
+}
+
+module "prod-onprem-example-dns-forwarding" {
+ source = "../../../modules/dns"
+ project_id = module.prod-spoke-project.project_id
+ type = "forwarding"
+ name = "example-com"
+ domain = "onprem.example.com."
+ client_networks = [module.prod-spoke-vpc.self_link]
+ forwarders = { for ip in var.dns.prod : ip => null }
+}
+
+module "prod-reverse-10-dns-forwarding" {
+ source = "../../../modules/dns"
+ project_id = module.prod-spoke-project.project_id
+ type = "forwarding"
+ name = "root-reverse-10"
+ domain = "10.in-addr.arpa."
+ client_networks = [module.prod-spoke-vpc.self_link]
+ forwarders = { for ip in var.dns.prod : ip => null }
+}
+
+# Google APIs
+
+module "prod-googleapis-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.prod-spoke-project.project_id
+ type = "private"
+ name = "googleapis-com"
+ domain = "googleapis.com."
+ client_networks = [module.prod-spoke-vpc.self_link]
+ recordsets = {
+ "A private" = { records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "A restricted" = { records = [
+ "199.36.153.4", "199.36.153.5", "199.36.153.6", "199.36.153.7"
+ ] }
+ "CNAME *" = { records = ["private.googleapis.com."] }
+ }
+}
+
+module "prod-gcrio-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.prod-spoke-project.project_id
+ type = "private"
+ name = "gcr-io"
+ domain = "gcr.io."
+ client_networks = [module.prod-spoke-vpc.self_link]
+ recordsets = {
+ "A gcr.io." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
+ }
+}
+
+module "prod-packages-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.prod-spoke-project.project_id
+ type = "private"
+ name = "packages-cloud"
+ domain = "packages.cloud.google.com."
+ client_networks = [module.prod-spoke-vpc.self_link]
+ recordsets = {
+ "A packages.cloud.google.com." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
+ }
+}
+
+module "prod-pkgdev-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.prod-spoke-project.project_id
+ type = "private"
+ name = "pkg-dev"
+ domain = "pkg.dev."
+ client_networks = [module.prod-spoke-vpc.self_link]
+ recordsets = {
+ "A pkg.dev." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
+ }
+}
+
+module "prod-pkigoog-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.prod-spoke-project.project_id
+ type = "private"
+ name = "pki-goog"
+ domain = "pki.goog."
+ client_networks = [module.prod-spoke-vpc.self_link]
+ recordsets = {
+ "A pki.goog." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
+ }
+}
diff --git a/fast/stages/02-networking-separate-envs/main.tf b/fast/stages/02-networking-separate-envs/main.tf
new file mode 100644
index 0000000000..0b901e3307
--- /dev/null
+++ b/fast/stages/02-networking-separate-envs/main.tf
@@ -0,0 +1,58 @@
+/**
+ * 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:file:description Networking folder and hierarchical policy.
+
+locals {
+ 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}"
+ })]
+ }
+ region_trigram = {
+ europe-west1 = "ew1"
+ europe-west3 = "ew3"
+ }
+ stage3_sas_delegated_grants = [
+ "roles/composer.sharedVpcAgent",
+ "roles/compute.networkUser",
+ "roles/container.hostServiceAgentUser",
+ "roles/vpcaccess.user",
+ ]
+ service_accounts = {
+ for k, v in coalesce(var.service_accounts, {}) : k => "serviceAccount:${v}" if v != null
+ }
+}
+
+module "folder" {
+ source = "../../../modules/folder"
+ parent = "organizations/${var.organization.id}"
+ name = "Networking"
+ 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"
+ }
+ firewall_policy_association = {
+ factory-policy = "factory"
+ }
+}
+
diff --git a/fast/stages/02-networking-separate-envs/monitoring.tf b/fast/stages/02-networking-separate-envs/monitoring.tf
new file mode 100644
index 0000000000..463c6a083d
--- /dev/null
+++ b/fast/stages/02-networking-separate-envs/monitoring.tf
@@ -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.
+ */
+
+# tfdoc:file:description Network monitoring dashboards.
+
+locals {
+ dashboard_path = "${var.data_dir}/dashboards"
+ dashboard_files = fileset(local.dashboard_path, "*.json")
+ dashboards = {
+ for filename in local.dashboard_files :
+ filename => "${local.dashboard_path}/${filename}"
+ }
+}
+
+resource "google_monitoring_dashboard" "dev-dashboard" {
+ for_each = local.dashboards
+ project = module.dev-spoke-project.project_id
+ dashboard_json = file(each.value)
+}
+
+resource "google_monitoring_dashboard" "prod-dashboard" {
+ for_each = local.dashboards
+ project = module.prod-spoke-project.project_id
+ dashboard_json = file(each.value)
+}
diff --git a/fast/stages/02-networking-separate-envs/outputs.tf b/fast/stages/02-networking-separate-envs/outputs.tf
new file mode 100644
index 0000000000..d06d499d63
--- /dev/null
+++ b/fast/stages/02-networking-separate-envs/outputs.tf
@@ -0,0 +1,102 @@
+/**
+ * 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 {
+ host_project_ids = {
+ dev-spoke-0 = module.dev-spoke-project.project_id
+ prod-spoke-0 = module.prod-spoke-project.project_id
+ }
+ host_project_numbers = {
+ dev-spoke-0 = module.dev-spoke-project.number
+ prod-spoke-0 = module.prod-spoke-project.number
+ }
+ subnet_self_links = {
+ dev-spoke-0 = module.dev-spoke-vpc.subnet_self_links
+ prod-spoke-0 = module.prod-spoke-vpc.subnet_self_links
+ }
+ tfvars = {
+ host_project_ids = local.host_project_ids
+ host_project_numbers = local.host_project_numbers
+ subnet_self_links = local.subnet_self_links
+ vpc_self_links = local.vpc_self_links
+ }
+ vpc_self_links = {
+ dev-spoke-0 = module.dev-spoke-vpc.self_link
+ prod-spoke-0 = module.prod-spoke-vpc.self_link
+ }
+}
+
+# generate tfvars file for subsequent stages
+
+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"
+ content = jsonencode(local.tfvars)
+}
+
+resource "google_storage_bucket_object" "tfvars" {
+ bucket = var.automation.outputs_bucket
+ name = "tfvars/02-networking.auto.tfvars.json"
+ content = jsonencode(local.tfvars)
+}
+
+# outputs
+
+output "dev_cloud_dns_inbound_policy" {
+ description = "IP Addresses for Cloud DNS inbound policy for the dev environment."
+ value = [for s in module.dev-spoke-vpc.subnets : cidrhost(s.ip_cidr_range, 2)]
+}
+
+output "host_project_ids" {
+ description = "Network project ids."
+ value = local.host_project_ids
+}
+
+output "host_project_numbers" {
+ description = "Network project numbers."
+ value = local.host_project_numbers
+}
+
+output "prod_cloud_dns_inbound_policy" {
+ description = "IP Addresses for Cloud DNS inbound policy for the prod environment."
+ value = [for s in module.prod-spoke-vpc.subnets : cidrhost(s.ip_cidr_range, 2)]
+}
+
+output "shared_vpc_self_links" {
+ description = "Shared VPC host projects."
+ value = local.vpc_self_links
+}
+
+output "tfvars" {
+ description = "Terraform variables file for the following stages."
+ sensitive = true
+ value = local.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 :
+ v.id => v.ip_address
+ }
+ prod-onprem-ew1 = {
+ for v in module.prod-to-onprem-ew1-vpn[0].gateway.vpn_interfaces :
+ v.id => v.ip_address
+ }
+ }
+}
diff --git a/fast/stages/02-networking-separate-envs/spoke-dev.tf b/fast/stages/02-networking-separate-envs/spoke-dev.tf
new file mode 100644
index 0000000000..ca7d8d4686
--- /dev/null
+++ b/fast/stages/02-networking-separate-envs/spoke-dev.tf
@@ -0,0 +1,109 @@
+/**
+ * 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:file:description Dev spoke VPC and related resources.
+
+module "dev-spoke-project" {
+ source = "../../../modules/project"
+ billing_account = var.billing_account.id
+ name = "dev-net-spoke-0"
+ parent = var.folder_ids.networking-dev
+ prefix = var.prefix
+ services = [
+ "container.googleapis.com",
+ "compute.googleapis.com",
+ "dns.googleapis.com",
+ "iap.googleapis.com",
+ "networkmanagement.googleapis.com",
+ "servicenetworking.googleapis.com",
+ "stackdriver.googleapis.com",
+ ]
+ shared_vpc_host_config = {
+ enabled = true
+ service_projects = []
+ }
+ iam = {
+ "roles/dns.admin" = compact([
+ try(local.service_accounts.project-factory-dev, null)
+ ])
+ }
+}
+
+module "dev-spoke-vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = module.dev-spoke-project.project_id
+ name = "dev-spoke-0"
+ mtu = 1500
+ data_folder = "${var.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
+ routes = {
+ private-googleapis = {
+ dest_range = "199.36.153.8/30"
+ next_hop_type = "gateway"
+ next_hop = "default-internet-gateway"
+ }
+ restricted-googleapis = {
+ dest_range = "199.36.153.4/30"
+ next_hop_type = "gateway"
+ next_hop = "default-internet-gateway"
+ }
+ }
+}
+
+module "dev-spoke-firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.dev-spoke-project.project_id
+ network = module.dev-spoke-vpc.name
+ default_rules_config = {
+ disabled = true
+ }
+ factories_config = {
+ cidr_tpl_file = "${var.data_dir}/cidrs.yaml"
+ rules_folder = "${var.data_dir}/firewall-rules/dev"
+ }
+}
+
+module "dev-spoke-cloudnat" {
+ for_each = toset(values(module.dev-spoke-vpc.subnet_regions))
+ source = "../../../modules/net-cloudnat"
+ project_id = module.dev-spoke-project.project_id
+ region = each.value
+ name = "dev-nat-${local.region_trigram[each.value]}"
+ router_create = true
+ router_network = module.dev-spoke-vpc.name
+ router_asn = 4200001024
+ logging_filter = "ERRORS_ONLY"
+}
+
+# Create delegated grants for stage3 service accounts
+resource "google_project_iam_binding" "dev_spoke_project_iam_delegated" {
+ project = module.dev-spoke-project.project_id
+ role = "roles/resourcemanager.projectIamAdmin"
+ members = compact([
+ try(local.service_accounts.data-platform-dev, null),
+ try(local.service_accounts.project-factory-dev, null),
+ ])
+ condition {
+ title = "dev_stage3_sa_delegated_grants"
+ description = "Development host project delegated grants."
+ expression = format(
+ "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])",
+ join(",", formatlist("'%s'", local.stage3_sas_delegated_grants))
+ )
+ }
+}
diff --git a/fast/stages/02-networking-separate-envs/spoke-prod.tf b/fast/stages/02-networking-separate-envs/spoke-prod.tf
new file mode 100644
index 0000000000..eba530a6c4
--- /dev/null
+++ b/fast/stages/02-networking-separate-envs/spoke-prod.tf
@@ -0,0 +1,109 @@
+/**
+ * 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:file:description Production spoke VPC and related resources.
+
+module "prod-spoke-project" {
+ source = "../../../modules/project"
+ billing_account = var.billing_account.id
+ name = "prod-net-spoke-0"
+ parent = var.folder_ids.networking-prod
+ prefix = var.prefix
+ services = [
+ "container.googleapis.com",
+ "compute.googleapis.com",
+ "dns.googleapis.com",
+ "iap.googleapis.com",
+ "networkmanagement.googleapis.com",
+ "servicenetworking.googleapis.com",
+ "stackdriver.googleapis.com",
+ ]
+ shared_vpc_host_config = {
+ enabled = true
+ service_projects = []
+ }
+ iam = {
+ "roles/dns.admin" = compact([
+ try(local.service_accounts.project-factory-prod, null)
+ ])
+ }
+}
+
+module "prod-spoke-vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = module.prod-spoke-project.project_id
+ name = "prod-spoke-0"
+ mtu = 1500
+ data_folder = "${var.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
+ routes = {
+ private-googleapis = {
+ dest_range = "199.36.153.8/30"
+ next_hop_type = "gateway"
+ next_hop = "default-internet-gateway"
+ }
+ restricted-googleapis = {
+ dest_range = "199.36.153.4/30"
+ next_hop_type = "gateway"
+ next_hop = "default-internet-gateway"
+ }
+ }
+}
+
+module "prod-spoke-firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.prod-spoke-project.project_id
+ network = module.prod-spoke-vpc.name
+ default_rules_config = {
+ disabled = true
+ }
+ factories_config = {
+ cidr_tpl_file = "${var.data_dir}/cidrs.yaml"
+ rules_folder = "${var.data_dir}/firewall-rules/prod"
+ }
+}
+
+module "prod-spoke-cloudnat" {
+ for_each = toset(values(module.prod-spoke-vpc.subnet_regions))
+ source = "../../../modules/net-cloudnat"
+ project_id = module.prod-spoke-project.project_id
+ region = each.value
+ name = "prod-nat-${local.region_trigram[each.value]}"
+ router_create = true
+ router_network = module.prod-spoke-vpc.name
+ router_asn = 4200001024
+ logging_filter = "ERRORS_ONLY"
+}
+
+# Create delegated grants for stage3 service accounts
+resource "google_project_iam_binding" "prod_spoke_project_iam_delegated" {
+ project = module.prod-spoke-project.project_id
+ role = "roles/resourcemanager.projectIamAdmin"
+ members = compact([
+ try(local.service_accounts.data-platform-prod, null),
+ try(local.service_accounts.project-factory-prod, null),
+ ])
+ condition {
+ title = "prod_stage3_sa_delegated_grants"
+ description = "Production host project delegated grants."
+ expression = format(
+ "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])",
+ join(",", formatlist("'%s'", local.stage3_sas_delegated_grants))
+ )
+ }
+}
diff --git a/fast/stages/02-networking-separate-envs/test-resources.tf b/fast/stages/02-networking-separate-envs/test-resources.tf
new file mode 100644
index 0000000000..ae3ba90a8d
--- /dev/null
+++ b/fast/stages/02-networking-separate-envs/test-resources.tf
@@ -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.
+ */
+
+# tfdoc:file:description Temporary instances for testing
+
+# module "test-vm-dev-0" {
+# source = "../../../modules/compute-vm"
+# project_id = module.dev-spoke-project.project_id
+# zone = "europe-west1-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"]
+# alias_ips = {}
+# nat = false
+# addresses = null
+# }]
+# tags = ["ssh"]
+# service_account_create = true
+# boot_disk = {
+# image = "projects/debian-cloud/global/images/family/debian-10"
+# type = "pd-balanced"
+# size = 10
+# }
+# options = {
+# allow_stopping_for_update = true
+# deletion_protection = false
+# spot = true
+# }
+# metadata = {
+# startup-script = <net-cloudnat
· net-vpc
· net-vpc-firewall
· project
| |
| [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. | | local_file
|
+| [outputs.tf](./outputs.tf) | Module outputs. | | google_storage_bucket_object
· local_file
|
| [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
| |
@@ -305,38 +313,41 @@ DNS configurations are centralised in the `dns.tf` file. Spokes delegate DNS res
| [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.tf](./vpn-spoke-prod.tf) | VPN between landing and production 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
| |
## Variables
| name | description | type | required | default | producer |
|---|---|:---:|:---:|:---:|:---:|
-| [billing_account](variables.tf#L17) | Billing account id and organization id ('nnnnnnnn' or null). | object({…})
| ✓ | | 00-bootstrap
|
-| [folder_ids](variables.tf#L66) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…})
| ✓ | | 01-resman
|
-| [organization](variables.tf#L94) | Organization details. | object({…})
| ✓ | | 00-bootstrap
|
-| [prefix](variables.tf#L110) | Prefix used for resources that need unique names. Use 9 characters or less. | string
| ✓ | | 00-bootstrap
|
-| [custom_adv](variables.tf#L26) | Custom advertisement definitions in name => range format. | map(string)
| | {…}
| |
-| [custom_roles](variables.tf#L43) | Custom roles defined at the org level, in key => id format. | object({…})
| | null
| 00-bootstrap
|
-| [data_dir](variables.tf#L52) | Relative path for the folder storing configuration data for network resources. | string
| | "data"
| |
-| [dns](variables.tf#L58) | Onprem DNS resolvers. | map(list(string))
| | {…}
| |
-| [l7ilb_subnets](variables.tf#L76) | Subnets used for L7 ILBs. | map(list(object({…})))
| | {…}
| |
-| [outputs_location](variables.tf#L104) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string
| | null
| |
-| [psa_ranges](variables.tf#L121) | IP ranges used for Private Service Access (e.g. CloudSQL). | object({…})
| | null
| |
-| [router_onprem_configs](variables.tf#L158) | Configurations for routers used for onprem connectivity. | map(object({…}))
| | {…}
| |
+| [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#L176) | Automation service accounts in name => email format. | object({…})
| | null
| 01-resman
|
-| [vpn_onprem_configs](variables.tf#L188) | VPN gateway configuration for onprem interconnection. | map(object({…}))
| | {…}
| |
-| [vpn_spoke_configs](variables-vpn.tf#L37) | VPN gateway configuration for spokes. | 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({…}))
| | {…}
| |
## Outputs
| name | description | sensitive | consumers |
|---|---|:---:|---|
-| [cloud_dns_inbound_policy](outputs.tf#L57) | IP Addresses for Cloud DNS inbound policy. | | |
-| [host_project_ids](outputs.tf#L62) | Network project ids. | | |
-| [host_project_numbers](outputs.tf#L67) | Network project numbers. | | |
-| [shared_vpc_self_links](outputs.tf#L72) | Shared VPC host projects. | | |
-| [tfvars](outputs.tf#L87) | Terraform variables file for the following stages. | ✓ | |
-| [vpn_gateway_endpoints](outputs.tf#L77) | External IP Addresses for the GCP VPN gateways. | | |
+| [cloud_dns_inbound_policy](outputs.tf#L63) | IP Addresses for Cloud DNS inbound policy. | | |
+| [host_project_ids](outputs.tf#L68) | Network project ids. | | |
+| [host_project_numbers](outputs.tf#L73) | Network project numbers. | | |
+| [shared_vpc_self_links](outputs.tf#L78) | Shared VPC host projects. | | |
+| [tfvars](outputs.tf#L83) | Terraform variables file for the following stages. | ✓ | |
+| [vpn_gateway_endpoints](outputs.tf#L89) | External IP Addresses for the GCP VPN gateways. | | |
diff --git a/fast/stages/02-networking-vpn/data/firewall-rules/dev/rules.yaml b/fast/stages/02-networking-vpn/data/firewall-rules/dev/rules.yaml
index d4df8cdc31..cab42edc94 100644
--- a/fast/stages/02-networking-vpn/data/firewall-rules/dev/rules.yaml
+++ b/fast/stages/02-networking-vpn/data/firewall-rules/dev/rules.yaml
@@ -1,27 +1,21 @@
# skip boilerplate check
-ingress-allow-composer-nodes:
- description: "Allow traffic to Composer nodes."
- direction: INGRESS
- action: allow
- sources: []
- ranges: ["0.0.0.0/0"]
- targets:
- - composer-worker
- use_service_accounts: false
- rules:
- - protocol: tcp
- ports: [80, 443, 3306, 3307]
-
-ingress-allow-dataflow-load:
- description: "Allow traffic to Dataflow nodes."
- direction: INGRESS
- action: allow
- sources: []
- ranges: ["0.0.0.0/0"]
- targets:
- - dataflow
- use_service_accounts: false
- rules:
- - protocol: tcp
- ports: [12345, 12346]
+ingress:
+ ingress-allow-composer-nodes:
+ description: "Allow traffic to Composer nodes."
+ sources:
+ - composer-worker
+ targets:
+ - composer-worker
+ rules:
+ - protocol: tcp
+ ports: [80, 443, 3306, 3307]
+ ingress-allow-dataflow-load:
+ description: "Allow traffic to Dataflow nodes."
+ sources:
+ - dataflow
+ targets:
+ - dataflow
+ rules:
+ - protocol: tcp
+ ports: [12345, 12346]
diff --git a/fast/stages/02-networking-vpn/data/firewall-rules/landing/rules.yaml b/fast/stages/02-networking-vpn/data/firewall-rules/landing/rules.yaml
index e72b7c9c7d..3c1425a7c0 100644
--- a/fast/stages/02-networking-vpn/data/firewall-rules/landing/rules.yaml
+++ b/fast/stages/02-networking-vpn/data/firewall-rules/landing/rules.yaml
@@ -1,15 +1,11 @@
# skip boilerplate check
-allow-onprem-probes-example:
- description: "Allow traffic from onprem probes"
- direction: INGRESS
- action: allow
- sources: []
- ranges:
- - $onprem_probes
- targets: []
- use_service_accounts: false
- rules:
- - protocol: tcp
- ports:
- - 12345
+ingress:
+ allow-onprem-probes-example:
+ description: "Allow traffic from onprem probes"
+ source_ranges:
+ - onprem_probes
+ rules:
+ - protocol: tcp
+ ports:
+ - 12345
diff --git a/fast/stages/02-networking-vpn/data/subnets/dev/dev-gke-nodes-ew1.yaml b/fast/stages/02-networking-vpn/data/subnets/dev/dev-gke-nodes-ew1.yaml
new file mode 100644
index 0000000000..c2b5cbe712
--- /dev/null
+++ b/fast/stages/02-networking-vpn/data/subnets/dev/dev-gke-nodes-ew1.yaml
@@ -0,0 +1,8 @@
+# skip boilerplate check
+
+region: europe-west1
+description: Default subnet for prod gke nodes
+ip_cidr_range: 10.64.0.0/24
+secondary_ip_range:
+ pods: 100.64.0.0/16
+ services: 192.168.1.0/24
diff --git a/fast/stages/02-networking-vpn/dns-dev.tf b/fast/stages/02-networking-vpn/dns-dev.tf
index aad50afc3f..03ae01221c 100644
--- a/fast/stages/02-networking-vpn/dns-dev.tf
+++ b/fast/stages/02-networking-vpn/dns-dev.tf
@@ -26,7 +26,7 @@ module "dev-dns-private-zone" {
domain = "dev.gcp.example.com."
client_networks = [module.landing-vpc.self_link]
recordsets = {
- "A localhost" = { type = "A", ttl = 300, records = ["127.0.0.1"] }
+ "A localhost" = { records = ["127.0.0.1"] }
}
}
diff --git a/fast/stages/02-networking-vpn/dns-landing.tf b/fast/stages/02-networking-vpn/dns-landing.tf
index b1d766ab24..7b97a8cfd5 100644
--- a/fast/stages/02-networking-vpn/dns-landing.tf
+++ b/fast/stages/02-networking-vpn/dns-landing.tf
@@ -46,11 +46,11 @@ module "gcp-example-dns-private-zone" {
domain = "gcp.example.com."
client_networks = [module.landing-vpc.self_link]
recordsets = {
- "A localhost" = { type = "A", ttl = 300, records = ["127.0.0.1"] }
+ "A localhost" = { records = ["127.0.0.1"] }
}
}
-# Google API zone to trigger Private Access
+# Google APIs
module "googleapis-private-zone" {
source = "../../../modules/dns"
@@ -60,12 +60,72 @@ module "googleapis-private-zone" {
domain = "googleapis.com."
client_networks = [module.landing-vpc.self_link]
recordsets = {
- "A private" = { type = "A", ttl = 300, records = [
+ "A private" = { records = [
"199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
] }
- "A restricted" = { type = "A", ttl = 300, records = [
+ "A restricted" = { records = [
"199.36.153.4", "199.36.153.5", "199.36.153.6", "199.36.153.7"
] }
- "CNAME *" = { type = "CNAME", ttl = 300, records = ["private.googleapis.com."] }
+ "CNAME *" = { records = ["private.googleapis.com."] }
+ }
+}
+
+module "gcrio-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.landing-project.project_id
+ type = "private"
+ name = "gcr-io"
+ domain = "gcr.io."
+ client_networks = [module.landing-vpc.self_link]
+ recordsets = {
+ "A gcr.io." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
+ }
+}
+
+module "packages-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.landing-project.project_id
+ type = "private"
+ name = "packages-cloud"
+ domain = "packages.cloud.google.com."
+ client_networks = [module.landing-vpc.self_link]
+ recordsets = {
+ "A packages.cloud.google.com." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
+ }
+}
+
+module "pkgdev-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.landing-project.project_id
+ type = "private"
+ name = "pkg-dev"
+ domain = "pkg.dev."
+ client_networks = [module.landing-vpc.self_link]
+ recordsets = {
+ "A pkg.dev." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
+ }
+}
+
+module "pkigoog-private-zone" {
+ source = "../../../modules/dns"
+ project_id = module.landing-project.project_id
+ type = "private"
+ name = "pki-goog"
+ domain = "pki.goog."
+ client_networks = [module.landing-vpc.self_link]
+ recordsets = {
+ "A pki.goog." = { ttl = 300, records = [
+ "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11"
+ ] }
+ "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] }
}
}
diff --git a/fast/stages/02-networking-vpn/dns-prod.tf b/fast/stages/02-networking-vpn/dns-prod.tf
index a4a916b468..5bb695fdcb 100644
--- a/fast/stages/02-networking-vpn/dns-prod.tf
+++ b/fast/stages/02-networking-vpn/dns-prod.tf
@@ -26,7 +26,7 @@ module "prod-dns-private-zone" {
domain = "prod.gcp.example.com."
client_networks = [module.landing-vpc.self_link]
recordsets = {
- "A localhost" = { type = "A", ttl = 300, records = ["127.0.0.1"] }
+ "A localhost" = { records = ["127.0.0.1"] }
}
}
diff --git a/fast/stages/02-networking-vpn/landing.tf b/fast/stages/02-networking-vpn/landing.tf
index fae959570f..83a0d509af 100644
--- a/fast/stages/02-networking-vpn/landing.tf
+++ b/fast/stages/02-networking-vpn/landing.tf
@@ -22,10 +22,6 @@ module "landing-project" {
name = "prod-net-landing-0"
parent = var.folder_ids.networking-prod
prefix = var.prefix
- service_config = {
- disable_on_destroy = false
- disable_dependent_services = false
- }
services = [
"compute.googleapis.com",
"dns.googleapis.com",
@@ -34,14 +30,15 @@ module "landing-project" {
"stackdriver.googleapis.com"
]
shared_vpc_host_config = {
- enabled = true
- service_projects = []
+ enabled = true
}
iam = {
- "roles/dns.admin" = [local.service_accounts.project-factory-prod]
- (local.custom_roles.service_project_network_admin) = [
- local.service_accounts.project-factory-prod
- ]
+ "roles/dns.admin" = compact([
+ try(local.service_accounts.project-factory-prod, null)
+ ])
+ (local.custom_roles.service_project_network_admin) = compact([
+ try(local.service_accounts.project-factory-prod, null)
+ ])
}
}
@@ -51,23 +48,17 @@ module "landing-vpc" {
name = "prod-landing-0"
mtu = 1500
dns_policy = {
- inbound = true
- logging = false
- outbound = null
+ inbound = true
}
# set explicit routes for googleapis in case the default route is deleted
routes = {
private-googleapis = {
dest_range = "199.36.153.8/30"
- priority = 1000
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
restricted-googleapis = {
dest_range = "199.36.153.4/30"
- priority = 1000
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
@@ -76,15 +67,16 @@ module "landing-vpc" {
}
module "landing-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.landing-project.project_id
- network = module.landing-vpc.name
- admin_ranges = []
- http_source_ranges = []
- https_source_ranges = []
- ssh_source_ranges = []
- data_folder = "${var.data_dir}/firewall-rules/landing"
- cidr_template_file = "${var.data_dir}/cidrs.yaml"
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.landing-project.project_id
+ network = module.landing-vpc.name
+ default_rules_config = {
+ disabled = true
+ }
+ factories_config = {
+ cidr_tpl_file = "${var.data_dir}/cidrs.yaml"
+ rules_folder = "${var.data_dir}/firewall-rules/landing"
+ }
}
module "landing-nat-ew1" {
diff --git a/fast/stages/02-networking-vpn/main.tf b/fast/stages/02-networking-vpn/main.tf
index 5df6d604e1..f68d39eb85 100644
--- a/fast/stages/02-networking-vpn/main.tf
+++ b/fast/stages/02-networking-vpn/main.tf
@@ -25,18 +25,17 @@ locals {
name = "${env}-l7ilb-${s.region}"
})]
}
- region_trigram = {
- europe-west1 = "ew1"
- europe-west3 = "ew3"
- }
stage3_sas_delegated_grants = [
"roles/composer.sharedVpcAgent",
"roles/compute.networkUser",
+ "roles/compute.networkViewer",
"roles/container.hostServiceAgentUser",
+ "roles/multiclusterservicediscovery.serviceAgent",
"roles/vpcaccess.user",
]
service_accounts = {
- for k, v in coalesce(var.service_accounts, {}) : k => "serviceAccount:${v}"
+ for k, v in coalesce(var.service_accounts, {}) :
+ k => "serviceAccount:${v}" if v != null
}
}
diff --git a/fast/stages/02-networking-vpn/outputs.tf b/fast/stages/02-networking-vpn/outputs.tf
index 3fe18d6575..3b97b7f254 100644
--- a/fast/stages/02-networking-vpn/outputs.tf
+++ b/fast/stages/02-networking-vpn/outputs.tf
@@ -43,15 +43,21 @@ locals {
}
}
-# optionally generate tfvars file for subsequent stages
+# generate tfvars file for subsequent stages
resource "local_file" "tfvars" {
for_each = var.outputs_location == null ? {} : { 1 = 1 }
file_permission = "0644"
- filename = "${pathexpand(var.outputs_location)}/tfvars/02-networking.auto.tfvars.json"
+ filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/02-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"
+ content = jsonencode(local.tfvars)
+}
+
# outputs
output "cloud_dns_inbound_policy" {
@@ -74,6 +80,12 @@ output "shared_vpc_self_links" {
value = local.vpc_self_links
}
+output "tfvars" {
+ description = "Terraform variables file for the following stages."
+ sensitive = true
+ value = local.tfvars
+}
+
output "vpn_gateway_endpoints" {
description = "External IP Addresses for the GCP VPN gateways."
value = local.enable_onprem_vpn == false ? null : {
@@ -83,9 +95,3 @@ output "vpn_gateway_endpoints" {
}
}
}
-
-output "tfvars" {
- description = "Terraform variables file for the following stages."
- sensitive = true
- value = local.tfvars
-}
diff --git a/fast/stages/02-networking-vpn/spoke-dev.tf b/fast/stages/02-networking-vpn/spoke-dev.tf
index 268f5b703b..e67cfb70db 100644
--- a/fast/stages/02-networking-vpn/spoke-dev.tf
+++ b/fast/stages/02-networking-vpn/spoke-dev.tf
@@ -22,10 +22,6 @@ module "dev-spoke-project" {
name = "dev-net-spoke-0"
parent = var.folder_ids.networking-dev
prefix = var.prefix
- service_config = {
- disable_on_destroy = false
- disable_dependent_services = false
- }
services = [
"container.googleapis.com",
"compute.googleapis.com",
@@ -36,36 +32,34 @@ module "dev-spoke-project" {
"stackdriver.googleapis.com",
]
shared_vpc_host_config = {
- enabled = true
- service_projects = []
+ enabled = true
}
metric_scopes = [module.landing-project.project_id]
iam = {
- "roles/dns.admin" = [local.service_accounts.project-factory-dev]
+ "roles/dns.admin" = compact([
+ try(local.service_accounts.gke-dev, null),
+ try(local.service_accounts.project-factory-dev, null),
+ ])
}
}
module "dev-spoke-vpc" {
- source = "../../../modules/net-vpc"
- project_id = module.dev-spoke-project.project_id
- name = "dev-spoke-0"
- mtu = 1500
- data_folder = "${var.data_dir}/subnets/dev"
- psa_config = try(var.psa_ranges.dev, null)
- subnets_l7ilb = local.l7ilb_subnets.dev
+ source = "../../../modules/net-vpc"
+ project_id = module.dev-spoke-project.project_id
+ name = "dev-spoke-0"
+ mtu = 1500
+ data_folder = "${var.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
routes = {
private-googleapis = {
dest_range = "199.36.153.8/30"
- priority = 1000
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
restricted-googleapis = {
dest_range = "199.36.153.4/30"
- priority = 1000
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
@@ -73,15 +67,16 @@ module "dev-spoke-vpc" {
}
module "dev-spoke-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.dev-spoke-project.project_id
- network = module.dev-spoke-vpc.name
- admin_ranges = []
- http_source_ranges = []
- https_source_ranges = []
- ssh_source_ranges = []
- data_folder = "${var.data_dir}/firewall-rules/dev"
- cidr_template_file = "${var.data_dir}/cidrs.yaml"
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.dev-spoke-project.project_id
+ network = module.dev-spoke-vpc.name
+ default_rules_config = {
+ disabled = true
+ }
+ factories_config = {
+ cidr_tpl_file = "${var.data_dir}/cidrs.yaml"
+ rules_folder = "${var.data_dir}/firewall-rules/dev"
+ }
}
module "dev-spoke-cloudnat" {
@@ -89,7 +84,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-${var.region_trigram[each.value]}"
router_create = true
router_network = module.dev-spoke-vpc.name
router_asn = 4200001024
@@ -100,10 +95,11 @@ module "dev-spoke-cloudnat" {
resource "google_project_iam_binding" "dev_spoke_project_iam_delegated" {
project = module.dev-spoke-project.project_id
role = "roles/resourcemanager.projectIamAdmin"
- members = [
- local.service_accounts.data-platform-dev,
- local.service_accounts.project-factory-dev,
- ]
+ members = compact([
+ try(local.service_accounts.data-platform-dev, null),
+ try(local.service_accounts.project-factory-dev, null),
+ try(local.service_accounts.gke-dev, null),
+ ])
condition {
title = "dev_stage3_sa_delegated_grants"
description = "Development host project delegated grants."
diff --git a/fast/stages/02-networking-vpn/spoke-prod.tf b/fast/stages/02-networking-vpn/spoke-prod.tf
index cf83d2e1e7..cf49152fa1 100644
--- a/fast/stages/02-networking-vpn/spoke-prod.tf
+++ b/fast/stages/02-networking-vpn/spoke-prod.tf
@@ -22,10 +22,6 @@ module "prod-spoke-project" {
name = "prod-net-spoke-0"
parent = var.folder_ids.networking-prod
prefix = var.prefix
- service_config = {
- disable_on_destroy = false
- disable_dependent_services = false
- }
services = [
"container.googleapis.com",
"compute.googleapis.com",
@@ -36,36 +32,34 @@ module "prod-spoke-project" {
"stackdriver.googleapis.com",
]
shared_vpc_host_config = {
- enabled = true
- service_projects = []
+ enabled = true
}
metric_scopes = [module.landing-project.project_id]
iam = {
- "roles/dns.admin" = [local.service_accounts.project-factory-prod]
+ "roles/dns.admin" = compact([
+ try(local.service_accounts.gke-prod, null),
+ try(local.service_accounts.project-factory-prod, null),
+ ])
}
}
module "prod-spoke-vpc" {
- source = "../../../modules/net-vpc"
- project_id = module.prod-spoke-project.project_id
- name = "prod-spoke-0"
- mtu = 1500
- data_folder = "${var.data_dir}/subnets/prod"
- psa_config = try(var.psa_ranges.prod, null)
- subnets_l7ilb = local.l7ilb_subnets.prod
+ source = "../../../modules/net-vpc"
+ project_id = module.prod-spoke-project.project_id
+ name = "prod-spoke-0"
+ mtu = 1500
+ data_folder = "${var.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
routes = {
private-googleapis = {
dest_range = "199.36.153.8/30"
- priority = 1000
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
restricted-googleapis = {
dest_range = "199.36.153.4/30"
- priority = 1000
- tags = []
next_hop_type = "gateway"
next_hop = "default-internet-gateway"
}
@@ -73,15 +67,16 @@ module "prod-spoke-vpc" {
}
module "prod-spoke-firewall" {
- source = "../../../modules/net-vpc-firewall"
- project_id = module.prod-spoke-project.project_id
- network = module.prod-spoke-vpc.name
- admin_ranges = []
- http_source_ranges = []
- https_source_ranges = []
- ssh_source_ranges = []
- data_folder = "${var.data_dir}/firewall-rules/prod"
- cidr_template_file = "${var.data_dir}/cidrs.yaml"
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.prod-spoke-project.project_id
+ network = module.prod-spoke-vpc.name
+ default_rules_config = {
+ disabled = true
+ }
+ factories_config = {
+ cidr_tpl_file = "${var.data_dir}/cidrs.yaml"
+ rules_folder = "${var.data_dir}/firewall-rules/prod"
+ }
}
module "prod-spoke-cloudnat" {
@@ -89,7 +84,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-${var.region_trigram[each.value]}"
router_create = true
router_network = module.prod-spoke-vpc.name
router_asn = 4200001024
@@ -100,10 +95,11 @@ module "prod-spoke-cloudnat" {
resource "google_project_iam_binding" "prod_spoke_project_iam_delegated" {
project = module.prod-spoke-project.project_id
role = "roles/resourcemanager.projectIamAdmin"
- members = [
- local.service_accounts.data-platform-prod,
- local.service_accounts.project-factory-prod,
- ]
+ members = compact([
+ try(local.service_accounts.data-platform-prod, null),
+ try(local.service_accounts.project-factory-prod, null),
+ try(local.service_accounts.gke-prod, null),
+ ])
condition {
title = "prod_stage3_sa_delegated_grants"
description = "Production host project delegated grants."
diff --git a/fast/stages/02-networking-vpn/test-resources.tf b/fast/stages/02-networking-vpn/test-resources.tf
index 8139e75514..204971fec8 100644
--- a/fast/stages/02-networking-vpn/test-resources.tf
+++ b/fast/stages/02-networking-vpn/test-resources.tf
@@ -24,16 +24,15 @@
# network_interfaces = [{
# network = module.landing-vpc.self_link
# subnetwork = module.landing-vpc.subnet_self_links["europe-west1/landing-default-ew1"]
-# alias_ips = {}
-# nat = false
-# addresses = null
# }]
# tags = ["ssh"]
# service_account_create = true
# boot_disk = {
# image = "projects/debian-cloud/global/images/family/debian-10"
-# type = "pd-balanced"
-# size = 10
+# }
+# options = {
+# spot = true
+# termination_action = "STOP"
# }
# metadata = {
# startup-script = <kms
· project
| google_project_iam_member
|
| [core-prod.tf](./core-prod.tf) | None | kms
· project
| google_project_iam_member
|
| [main.tf](./main.tf) | Module-level locals and resources. | | |
-| [outputs.tf](./outputs.tf) | Module outputs. | | local_file
|
+| [outputs.tf](./outputs.tf) | Module outputs. | | google_storage_bucket_object
· local_file
|
| [variables.tf](./variables.tf) | Module variables. | | |
| [vpc-sc.tf](./vpc-sc.tf) | None | vpc-sc
| |
@@ -285,29 +262,27 @@ Some references that might be useful in setting up this stage:
| name | description | type | required | default | producer |
|---|---|:---:|:---:|:---:|:---:|
-| [billing_account](variables.tf#L17) | Billing account id and organization id ('nnnnnnnn' or null). | object({…})
| ✓ | | 00-bootstrap
|
-| [folder_ids](variables.tf#L26) | Folder name => id mappings, the 'security' folder name must exist. | object({…})
| ✓ | | 01-resman
|
-| [organization](variables.tf#L81) | Organization details. | object({…})
| ✓ | | 00-bootstrap
|
-| [prefix](variables.tf#L97) | Prefix used for resources that need unique names. Use 9 characters or less. | string
| ✓ | | 00-bootstrap
|
-| [service_accounts](variables.tf#L72) | Automation service accounts that can assign the encrypt/decrypt roles on keys. | object({…})
| ✓ | | 01-resman
|
-| [groups](variables.tf#L34) | Group names to grant organization-level permissions. | map(string)
| | {…}
| 00-bootstrap
|
-| [kms_defaults](variables.tf#L49) | Defaults used for KMS keys. | object({…})
| | {…}
| |
-| [kms_keys](variables.tf#L61) | KMS keys to create, keyed by name. Null attributes will be interpolated with defaults. | map(object({…}))
| | {}
| |
-| [outputs_location](variables.tf#L91) | 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#L108) | VPC SC access level definitions. | map(object({…}))
| | {}
| |
-| [vpc_sc_egress_policies](variables.tf#L123) | VPC SC egress policy defnitions. | map(object({…}))
| | {}
| |
-| [vpc_sc_ingress_policies](variables.tf#L141) | VPC SC ingress policy defnitions. | map(object({…}))
| | {}
| |
-| [vpc_sc_perimeter_access_levels](variables.tf#L161) | VPC SC perimeter access_levels. | object({…})
| | null
| |
-| [vpc_sc_perimeter_egress_policies](variables.tf#L171) | VPC SC egress policies per perimeter, values reference keys defined in the `vpc_sc_ingress_policies` variable. | object({…})
| | null
| |
-| [vpc_sc_perimeter_ingress_policies](variables.tf#L181) | VPC SC ingress policies per perimeter, values reference keys defined in the `vpc_sc_ingress_policies` variable. | object({…})
| | null
| |
-| [vpc_sc_perimeter_projects](variables.tf#L191) | VPC SC perimeter resources. | object({…})
| | null
| |
+| [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({…})
| | {}
| |
## Outputs
| name | description | sensitive | consumers |
|---|---|:---:|---|
-| [kms_keys](outputs.tf#L53) | KMS key ids. | | |
-| [stage_perimeter_projects](outputs.tf#L58) | Security project numbers. They can be added to perimeter resources. | | |
-| [tfvars](outputs.tf#L68) | Terraform variable files for the following stages. | ✓ | |
+| [kms_keys](outputs.tf#L59) | KMS key ids. | | |
+| [stage_perimeter_projects](outputs.tf#L64) | Security project numbers. They can be added to perimeter resources. | | |
+| [tfvars](outputs.tf#L74) | Terraform variable files for the following stages. | ✓ | |
diff --git a/fast/stages/02-security/core-dev.tf b/fast/stages/02-security/core-dev.tf
index 92fcaec0df..c6b9ba01bf 100644
--- a/fast/stages/02-security/core-dev.tf
+++ b/fast/stages/02-security/core-dev.tf
@@ -16,7 +16,10 @@
locals {
dev_kms_restricted_admins = [
- "serviceAccount:${var.service_accounts.project-factory-dev}"
+ for sa in compact([
+ var.service_accounts.project-factory-dev,
+ var.service_accounts.data-platform-dev
+ ]) : "serviceAccount:${sa}"
]
}
@@ -49,7 +52,6 @@ module "dev-sec-kms" {
}
# TODO(ludo): add support for conditions to Fabric modules
-# TODO(ludo): grant delegated role at key instead of project level
resource "google_project_iam_member" "dev_key_admin_delegated" {
for_each = toset(local.dev_kms_restricted_admins)
@@ -60,7 +62,7 @@ resource "google_project_iam_member" "dev_key_admin_delegated" {
title = "kms_sa_delegated_grants"
description = "Automation service account delegated grants."
expression = format(
- "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])",
+ "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s]) && resource.type == 'cloudkms.googleapis.com/CryptoKey'",
join(",", formatlist("'%s'", [
"roles/cloudkms.cryptoKeyEncrypterDecrypter",
"roles/cloudkms.cryptoKeyEncrypterDecrypterViaDelegation"
diff --git a/fast/stages/02-security/core-prod.tf b/fast/stages/02-security/core-prod.tf
index d00c724daa..b21547607a 100644
--- a/fast/stages/02-security/core-prod.tf
+++ b/fast/stages/02-security/core-prod.tf
@@ -16,7 +16,10 @@
locals {
prod_kms_restricted_admins = [
- "serviceAccount:${var.service_accounts.project-factory-prod}"
+ for sa in compact([
+ var.service_accounts.project-factory-prod,
+ var.service_accounts.data-platform-prod
+ ]) : "serviceAccount:${sa}"
]
}
@@ -59,7 +62,7 @@ resource "google_project_iam_member" "prod_key_admin_delegated" {
title = "kms_sa_delegated_grants"
description = "Automation service account delegated grants."
expression = format(
- "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])",
+ "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s]) && resource.type == 'cloudkms.googleapis.com/CryptoKey'",
join(",", formatlist("'%s'", [
"roles/cloudkms.cryptoKeyEncrypterDecrypter",
"roles/cloudkms.cryptoKeyEncrypterDecrypterViaDelegation"
diff --git a/fast/stages/02-security/outputs.tf b/fast/stages/02-security/outputs.tf
index ee2ac15e6c..b7e42e4923 100644
--- a/fast/stages/02-security/outputs.tf
+++ b/fast/stages/02-security/outputs.tf
@@ -39,7 +39,7 @@ locals {
}
}
-# optionally generate files for subsequent stages
+# generate files for subsequent stages
resource "local_file" "tfvars" {
for_each = var.outputs_location == null ? {} : { 1 = 1 }
@@ -48,6 +48,12 @@ resource "local_file" "tfvars" {
content = jsonencode(local.tfvars)
}
+resource "google_storage_bucket_object" "tfvars" {
+ bucket = var.automation.outputs_bucket
+ name = "tfvars/02-security.auto.tfvars.json"
+ content = jsonencode(local.tfvars)
+}
+
# outputs
output "kms_keys" {
diff --git a/fast/stages/02-security/variables.tf b/fast/stages/02-security/variables.tf
index 8ff52ffdaf..349589c964 100644
--- a/fast/stages/02-security/variables.tf
+++ b/fast/stages/02-security/variables.tf
@@ -14,6 +14,14 @@
* limitations under the License.
*/
+variable "automation" {
+ # tfdoc:variable:source 00-bootstrap
+ description = "Automation resources created by the bootstrap stage."
+ type = object({
+ outputs_bucket = string
+ })
+}
+
variable "billing_account" {
# tfdoc:variable:source 00-bootstrap
description = "Billing account id and organization id ('nnnnnnnn' or null)."
@@ -69,15 +77,6 @@ variable "kms_keys" {
default = {}
}
-variable "service_accounts" {
- # tfdoc:variable:source 01-resman
- description = "Automation service accounts that can assign the encrypt/decrypt roles on keys."
- type = object({
- project-factory-dev = string
- project-factory-prod = string
- })
-}
-
variable "organization" {
# tfdoc:variable:source 00-bootstrap
description = "Organization details."
@@ -105,95 +104,109 @@ variable "prefix" {
}
}
+variable "service_accounts" {
+ # tfdoc:variable:source 01-resman
+ description = "Automation service accounts that can assign the encrypt/decrypt roles on keys."
+ type = object({
+ data-platform-dev = string
+ data-platform-prod = string
+ project-factory-dev = string
+ project-factory-prod = string
+ })
+}
+
variable "vpc_sc_access_levels" {
description = "VPC SC access level definitions."
type = map(object({
- combining_function = string
- conditions = list(object({
- ip_subnetworks = list(string)
- members = list(string)
- negate = bool
- regions = list(string)
- required_access_levels = list(string)
- }))
+ 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 = {}
+ default = {}
+ nullable = false
}
variable "vpc_sc_egress_policies" {
description = "VPC SC egress policy defnitions."
type = map(object({
- egress_from = object({
- identity_type = string
- identities = list(string)
+ from = object({
+ identity_type = optional(string, "ANY_IDENTITY")
+ identities = optional(list(string))
})
- egress_to = object({
- operations = list(object({
- method_selectors = list(string)
+ to = object({
+ operations = optional(list(object({
+ method_selectors = optional(list(string))
service_name = string
- }))
- resources = list(string)
+ })), [])
+ resources = optional(list(string))
+ resource_type_external = optional(bool, false)
})
}))
- default = {}
+ default = {}
+ nullable = false
}
variable "vpc_sc_ingress_policies" {
description = "VPC SC ingress policy defnitions."
type = map(object({
- ingress_from = object({
- identity_type = string
- identities = list(string)
- source_access_levels = list(string)
- source_resources = list(string)
+ from = object({
+ access_levels = optional(list(string), [])
+ identity_type = optional(string)
+ identities = optional(list(string))
+ resources = optional(list(string), [])
})
- ingress_to = object({
- operations = list(object({
- method_selectors = list(string)
+ to = object({
+ operations = optional(list(object({
+ method_selectors = optional(list(string))
service_name = string
- }))
- resources = list(string)
+ })), [])
+ resources = optional(list(string))
})
}))
- default = {}
-}
-
-variable "vpc_sc_perimeter_access_levels" {
- description = "VPC SC perimeter access_levels."
- type = object({
- dev = list(string)
- landing = list(string)
- prod = list(string)
- })
- default = null
-}
-
-variable "vpc_sc_perimeter_egress_policies" {
- description = "VPC SC egress policies per perimeter, values reference keys defined in the `vpc_sc_ingress_policies` variable."
- type = object({
- dev = list(string)
- landing = list(string)
- prod = list(string)
- })
- default = null
-}
-
-variable "vpc_sc_perimeter_ingress_policies" {
- description = "VPC SC ingress policies per perimeter, values reference keys defined in the `vpc_sc_ingress_policies` variable."
- type = object({
- dev = list(string)
- landing = list(string)
- prod = list(string)
- })
- default = null
+ default = {}
+ nullable = false
}
-variable "vpc_sc_perimeter_projects" {
- description = "VPC SC perimeter resources."
+variable "vpc_sc_perimeters" {
+ description = "VPC SC regular perimeter definitions."
type = object({
- dev = list(string)
- landing = list(string)
- prod = list(string)
+ dev = optional(object({
+ access_levels = optional(list(string), [])
+ egress_policies = optional(list(string), [])
+ ingress_policies = optional(list(string), [])
+ resources = optional(list(string), [])
+ }), {})
+ landing = optional(object({
+ access_levels = optional(list(string), [])
+ egress_policies = optional(list(string), [])
+ ingress_policies = optional(list(string), [])
+ resources = optional(list(string), [])
+ }), {})
+ prod = optional(object({
+ access_levels = optional(list(string), [])
+ egress_policies = optional(list(string), [])
+ ingress_policies = optional(list(string), [])
+ resources = optional(list(string), [])
+ }), {})
})
- default = null
+ default = {}
+ nullable = false
}
diff --git a/fast/stages/02-security/vpc-sc.tf b/fast/stages/02-security/vpc-sc.tf
index e9887d2053..953badf15a 100644
--- a/fast/stages/02-security/vpc-sc.tf
+++ b/fast/stages/02-security/vpc-sc.tf
@@ -15,118 +15,42 @@
*/
locals {
- _perimeter_names = ["dev", "landing", "prod"]
- # dereference perimeter egress policy names to the actual objects
- _vpc_sc_perimeter_egress_policies = {
- for k, v in coalesce(var.vpc_sc_perimeter_egress_policies, {}) :
- k => [
- for i in coalesce(v, []) : var.vpc_sc_egress_policies[i]
- if lookup(var.vpc_sc_egress_policies, i, null) != null
- ]
- }
- # dereference perimeter ingress policy names to the actual objects
- _vpc_sc_perimeter_ingress_policies = {
- for k, v in coalesce(var.vpc_sc_perimeter_ingress_policies, {}) :
- k => [
- for i in coalesce(v, []) : var.vpc_sc_ingress_policies[i]
- if lookup(var.vpc_sc_ingress_policies, i, null) != null
- ]
- }
+ _vpc_sc_vpc_accessible_services = null
+ _vpc_sc_restricted_services = yamldecode(
+ file("${path.module}/vpc-sc-restricted-services.yaml")
+ )
# compute the number of projects in each perimeter to detect which to create
vpc_sc_counts = {
- for k in local._perimeter_names : k => length(
- local.vpc_sc_perimeter_projects[k]
- )
+ for k, v in var.vpc_sc_perimeters : k => length(v.resources)
}
# define dry run spec at file level for convenience
vpc_sc_explicit_dry_run_spec = true
# compute perimeter bridge resources (projects)
- vpc_sc_p_bridge_resources = {
+ vpc_sc_bridge_resources = {
landing_to_dev = concat(
- local.vpc_sc_perimeter_projects.landing,
- local.vpc_sc_perimeter_projects.dev
+ var.vpc_sc_perimeters.landing.resources,
+ var.vpc_sc_perimeters.dev.resources
)
landing_to_prod = concat(
- local.vpc_sc_perimeter_projects.landing,
- local.vpc_sc_perimeter_projects.prod
+ var.vpc_sc_perimeters.landing.resources,
+ var.vpc_sc_perimeters.prod.resources
)
}
- # computer perimeter regular specs / status
- vpc_sc_p_regular_specs = {
- dev = {
- access_levels = coalesce(
- try(var.vpc_sc_perimeter_access_levels.dev, null), []
- )
- resources = local.vpc_sc_perimeter_projects.dev
- restricted_services = local.vpc_sc_restricted_services
- egress_policies = try(
- local._vpc_sc_perimeter_egress_policies.dev, null
- )
- ingress_policies = try(
- local._vpc_sc_perimeter_ingress_policies.dev, null
- )
- vpc_accessible_services = null
- # vpc_accessible_services = {
- # allowed_services = ["RESTRICTED-SERVICES"]
- # enable_restriction = true
- # }
- }
- landing = {
- access_levels = coalesce(
- try(var.vpc_sc_perimeter_access_levels.landing, null), []
- )
- resources = local.vpc_sc_perimeter_projects.landing
- restricted_services = local.vpc_sc_restricted_services
- egress_policies = try(
- local._vpc_sc_perimeter_egress_policies.landing, null
- )
- ingress_policies = try(
- local._vpc_sc_perimeter_ingress_policies.landing, null
- )
- vpc_accessible_services = null
- # vpc_accessible_services = {
- # allowed_services = ["RESTRICTED-SERVICES"]
- # enable_restriction = true
- # }
- }
- prod = {
- access_levels = coalesce(
- try(var.vpc_sc_perimeter_access_levels.prod, null), []
- )
- # combine the security project, and any specified in the variable
- resources = local.vpc_sc_perimeter_projects.prod
- restricted_services = local.vpc_sc_restricted_services
- egress_policies = try(
- local._vpc_sc_perimeter_egress_policies.prod, null
- )
- ingress_policies = try(
- local._vpc_sc_perimeter_ingress_policies.prod, null
- )
- vpc_accessible_services = null
- # vpc_accessible_services = {
- # allowed_services = ["RESTRICTED-SERVICES"]
- # enable_restriction = true
- # }
- }
+ # compute spec/status for each perimeter
+ 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
+ })
}
- # account for null values in variable
- vpc_sc_perimeter_projects = (
- var.vpc_sc_perimeter_projects == null ?
- {
- for k in local._perimeter_names : k => []
- }
- : {
- for k in local._perimeter_names : k => (
- var.vpc_sc_perimeter_projects[k] == null
- ? []
- : var.vpc_sc_perimeter_projects[k]
- )
- }
- )
- # get the list of restricted services from the yaml file
- vpc_sc_restricted_services = yamldecode(
- file("${path.module}/vpc-sc-restricted-services.yaml")
- )
}
module "vpc-sc" {
@@ -138,22 +62,39 @@ module "vpc-sc" {
parent = "organizations/${var.organization.id}"
title = "default"
}
- access_levels = coalesce(try(var.vpc_sc_access_levels, null), {})
- # bridge type perimeters
+ access_levels = var.vpc_sc_access_levels
+ egress_policies = var.vpc_sc_egress_policies
+ ingress_policies = var.vpc_sc_ingress_policies
service_perimeters_bridge = merge(
# landing to dev, only we have projects in landing and dev perimeters
local.vpc_sc_counts.landing * local.vpc_sc_counts.dev == 0 ? {} : {
landing_to_dev = {
- spec_resources = local.vpc_sc_p_bridge_resources.landing_to_dev
- status_resources = null
+ spec_resources = (
+ local.vpc_sc_explicit_dry_run_spec
+ ? local.vpc_sc_bridge_resources.landing_to_dev
+ : null
+ )
+ status_resources = (
+ local.vpc_sc_explicit_dry_run_spec
+ ? null
+ : local.vpc_sc_bridge_resources.landing_to_dev
+ )
use_explicit_dry_run_spec = local.vpc_sc_explicit_dry_run_spec
}
},
# landing to prod, only we have projects in landing and prod perimeters
local.vpc_sc_counts.landing * local.vpc_sc_counts.prod == 0 ? {} : {
landing_to_prod = {
- spec_resources = local.vpc_sc_p_bridge_resources.landing_to_prod
- status_resources = null
+ spec_resources = (
+ local.vpc_sc_explicit_dry_run_spec
+ ? local.vpc_sc_bridge_resources.landing_to_prod
+ : null
+ )
+ status_resources = (
+ local.vpc_sc_explicit_dry_run_spec
+ ? null
+ : local.vpc_sc_bridge_resources.landing_to_prod
+ )
use_explicit_dry_run_spec = local.vpc_sc_explicit_dry_run_spec
}
}
@@ -163,24 +104,48 @@ module "vpc-sc" {
# dev if we have projects in var.vpc_sc_perimeter_projects.dev
local.vpc_sc_counts.dev == 0 ? {} : {
dev = {
- spec = local.vpc_sc_p_regular_specs.dev
- status = null
+ spec = (
+ local.vpc_sc_explicit_dry_run_spec
+ ? local.vpc_sc_perimeters_spec_status.dev
+ : null
+ )
+ status = (
+ local.vpc_sc_explicit_dry_run_spec
+ ? null
+ : local.vpc_sc_perimeters_spec_status.dev
+ )
use_explicit_dry_run_spec = local.vpc_sc_explicit_dry_run_spec
}
},
# landing if we have projects in var.vpc_sc_perimeter_projects.landing
local.vpc_sc_counts.landing == 0 ? {} : {
landing = {
- spec = local.vpc_sc_p_regular_specs.landing
- status = null
+ spec = (
+ local.vpc_sc_explicit_dry_run_spec
+ ? local.vpc_sc_perimeters_spec_status.landing
+ : null
+ )
+ status = (
+ local.vpc_sc_explicit_dry_run_spec
+ ? null
+ : local.vpc_sc_perimeters_spec_status.landing
+ )
use_explicit_dry_run_spec = local.vpc_sc_explicit_dry_run_spec
}
},
# prod if we have projects in var.vpc_sc_perimeter_projects.prod
local.vpc_sc_counts.prod == 0 ? {} : {
prod = {
- spec = local.vpc_sc_p_regular_specs.prod
- status = null
+ spec = (
+ local.vpc_sc_explicit_dry_run_spec
+ ? local.vpc_sc_perimeters_spec_status.prod
+ : null
+ )
+ status = (
+ local.vpc_sc_explicit_dry_run_spec
+ ? null
+ : 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/dev/IAM.md b/fast/stages/03-data-platform/dev/IAM.md
index 2fa6fbd94e..70622c2e03 100644
--- a/fast/stages/03-data-platform/dev/IAM.md
+++ b/fast/stages/03-data-platform/dev/IAM.md
@@ -6,45 +6,53 @@ Legend: +
additive, •
conditional.
| members | roles |
|---|---|
+|gcp-data-analysts+
|
+|dev-data-load-df-0+
|
+|dev-data-load-df-0+
|
+|dev-data-load-df-0+
|
## Project dev-data-lnd-0
@@ -62,37 +70,40 @@ Legend: +
additive, •
conditional.
| members | roles |
|---|---|
|gcp-data-engineers+
|
|dev-data-load-df-0+
|
|dev-data-load-df-0+
|
|dev-data-orc-cmp-0+
|
+|PROJECT_CLOUD_SERVICES+
|
+|SERVICE_IDENTITY_cloudcomposer-accounts+
|
+|SERVICE_IDENTITY_container-engine-robot+
+
|
+|SERVICE_IDENTITY_dataflow-service-producer-prod+
+
+
+
|
|dev-data-load-df-0+
|
|dev-data-trf-df-0+
|
-|service-36960036774+
+
+
+
+
|
-|service-426128559612+
|
-|service-883871192228+
|
diff --git a/fast/stages/03-data-platform/dev/README.md b/fast/stages/03-data-platform/dev/README.md
index 19adb06821..12db8d2923 100644
--- a/fast/stages/03-data-platform/dev/README.md
+++ b/fast/stages/03-data-platform/dev/README.md
@@ -8,7 +8,7 @@ The Data Platform builds on top of your foundations to create and set up project
## Design overview and choices
-> A more comprehensive description of the Data Platform architecture and approach can be found in the [Data Platform module README](../../../../examples/data-solutions/data-platform-foundations/). The module is wrapped and configured here to leverage the FAST flow.
+> 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.
@@ -28,13 +28,13 @@ The Data Platform manages:
### 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](../../../../examples/data-solutions/data-platform-foundations/#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 lake L2 project, and BigQuery READ/WRITE access to the playground project.
+- *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 Lake L0|Data Lake L1|Data Lake L2|Data Lake Playground|Orchestration|Common|
+|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`|-|-|
@@ -50,12 +50,31 @@ Cloud KMS crypto keys can be configured wither from the [FAST security stage](..
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.
+
++ +
+ ## 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)). @@ -89,6 +108,14 @@ If you're running this on top of Fast, you should run the following commands to 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: @@ -105,6 +132,8 @@ If you configured a valid path for `outputs_location` in the bootstrap security 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. @@ -116,47 +145,56 @@ 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 | -|---|---|---| -| [main.tf](./main.tf) | Data Platformy. |data-platform-foundations
|
-| [outputs.tf](./outputs.tf) | Output variables. | |
-| [variables.tf](./variables.tf) | Terraform Variables. | |
+| 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 |
|---|---|:---:|:---:|:---:|:---:|
-| [billing_account](variables.tf#L17) | Billing account id and organization id ('nnnnnnnn' or null). | object({…})
| ✓ | | 00-globals
|
-| [folder_ids](variables.tf#L45) | Folder to be used for the networking resources in folders/nnnn format. | object({…})
| ✓ | | 01-resman
|
-| [host_project_ids](variables.tf#L63) | Shared VPC project ids. | object({…})
| ✓ | | 02-networking
|
-| [organization](variables.tf#L89) | Organization details. | object({…})
| ✓ | | 00-globals
|
-| [prefix](variables.tf#L105) | Unique prefix used for resource names. Not used for projects if 'project_create' is null. | string
| ✓ | | 00-globals
|
-| [composer_config](variables.tf#L26) | | object({…})
| | {…}
| |
-| [data_force_destroy](variables.tf#L39) | Flag to set 'force_destroy' on data services like BigQery or Cloud Storage. | bool
| | false
| |
-| [groups](variables.tf#L53) | Groups. | map(string)
| | {…}
| |
-| [network_config_composer](variables.tf#L71) | Network configurations to use for Composer. | object({…})
| | {…}
| |
-| [outputs_location](variables.tf#L99) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string
| | null
| |
-| [project_services](variables.tf#L111) | List of core services enabled on all projects. | list(string)
| | […]
| |
-| [region](variables.tf#L122) | Region used for regional resources. | string
| | "europe-west1"
| |
-| [service_encryption_keys](variables.tf#L128) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…})
| | null
| |
-| [subnet_self_links](variables.tf#L140) | Shared VPC subnet self links. | object({…})
| | null
| 02-networking
|
-| [vpc_self_links](variables.tf#L149) | Shared VPC self links. | object({…})
| | null
| 02-networking
|
+| [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#L17) | BigQuery datasets. | | |
+| [bigquery_datasets](outputs.tf#L42) | BigQuery datasets. | | |
| [demo_commands](outputs.tf#L47) | Demo commands. | | |
-| [gcs_buckets](outputs.tf#L22) | GCS buckets. | | |
-| [kms_keys](outputs.tf#L27) | Cloud MKS keys. | | |
-| [projects](outputs.tf#L32) | GCP Projects informations. | | |
-| [vpc_network](outputs.tf#L37) | VPC network. | | |
-| [vpc_subnet](outputs.tf#L42) | VPC subnetworks. | | |
+| [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/03-data-platform/dev/demo
new file mode 120000
index 0000000000..7a0e7c1e35
--- /dev/null
+++ b/fast/stages/03-data-platform/dev/demo
@@ -0,0 +1 @@
+../../../../blueprints/data-solutions/data-platform-foundations/demo/
\ No newline at end of file
diff --git a/fast/stages/03-data-platform/dev/diagram.png b/fast/stages/03-data-platform/dev/diagram.png
index 001c6f2ab0..79b46e179e 100644
Binary files a/fast/stages/03-data-platform/dev/diagram.png and b/fast/stages/03-data-platform/dev/diagram.png differ
diff --git a/fast/stages/03-data-platform/dev/diagram_vpcsc.png b/fast/stages/03-data-platform/dev/diagram_vpcsc.png
new file mode 100644
index 0000000000..2bbaad0b21
Binary files /dev/null and b/fast/stages/03-data-platform/dev/diagram_vpcsc.png differ
diff --git a/fast/stages/03-data-platform/dev/main.tf b/fast/stages/03-data-platform/dev/main.tf
index c10380da74..24abb58d98 100644
--- a/fast/stages/03-data-platform/dev/main.tf
+++ b/fast/stages/03-data-platform/dev/main.tf
@@ -14,15 +14,17 @@
* limitations under the License.
*/
-# tfdoc:file:description Data Platformy.
+# tfdoc:file:description Data Platform.
module "data-platform" {
- source = "../../../../examples/data-solutions/data-platform-foundations"
+ source = "../../../../blueprints/data-solutions/data-platform-foundations"
billing_account_id = var.billing_account.id
composer_config = var.composer_config
data_force_destroy = var.data_force_destroy
- folder_id = var.folder_ids.data-platform
+ data_catalog_tags = var.data_catalog_tags
+ folder_id = var.folder_ids.data-platform-dev
groups = var.groups
+ location = var.location
network_config = {
host_project = var.host_project_ids.dev-spoke-0
network_self_link = var.vpc_self_links.dev-spoke-0
diff --git a/fast/stages/03-data-platform/dev/outputs.tf b/fast/stages/03-data-platform/dev/outputs.tf
index 0820b64f64..d0f79358cb 100644
--- a/fast/stages/03-data-platform/dev/outputs.tf
+++ b/fast/stages/03-data-platform/dev/outputs.tf
@@ -14,11 +14,41 @@
# tfdoc:file:description Output variables.
+locals {
+ tfvars = {
+ bigquery_dataset = module.data-platform.bigquery-datasets
+ gcs_buckets = module.data-platform.gcs-buckets
+ projects = module.data-platform.projects
+ }
+}
+
+# generate tfvars file for subsequent stages
+
+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"
+ content = jsonencode(local.tfvars)
+}
+
+resource "google_storage_bucket_object" "tfvars" {
+ bucket = var.automation.outputs_bucket
+ name = "tfvars/03-data-platform-dev.auto.tfvars.json"
+ content = jsonencode(local.tfvars)
+}
+
+# outputs
+
output "bigquery_datasets" {
description = "BigQuery datasets."
value = module.data-platform.bigquery-datasets
}
+output "demo_commands" {
+ description = "Demo commands."
+ value = module.data-platform.demo_commands
+}
+
output "gcs_buckets" {
description = "GCS buckets."
value = module.data-platform.gcs-buckets
@@ -43,8 +73,3 @@ output "vpc_subnet" {
description = "VPC subnetworks."
value = module.data-platform.vpc_subnet
}
-
-output "demo_commands" {
- description = "Demo commands."
- value = module.data-platform.demo_commands
-}
diff --git a/fast/stages/03-data-platform/dev/variables.tf b/fast/stages/03-data-platform/dev/variables.tf
index 1f65cf7737..9495316a91 100644
--- a/fast/stages/03-data-platform/dev/variables.tf
+++ b/fast/stages/03-data-platform/dev/variables.tf
@@ -14,6 +14,14 @@
# tfdoc:file:description Terraform Variables.
+variable "automation" {
+ # tfdoc:variable:source 00-bootstrap
+ description = "Automation resources created by the bootstrap stage."
+ type = object({
+ outputs_bucket = string
+ })
+}
+
variable "billing_account" {
# tfdoc:variable:source 00-globals
description = "Billing account id and organization id ('nnnnnnnn' or null)."
@@ -24,6 +32,7 @@ variable "billing_account" {
}
variable "composer_config" {
+ description = "Cloud Composer configuration options."
type = object({
node_count = number
airflow_version = string
@@ -36,6 +45,17 @@ variable "composer_config" {
}
}
+variable "data_catalog_tags" {
+ description = "List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format."
+ type = map(map(list(string)))
+ nullable = false
+ default = {
+ "3_Confidential" = null
+ "2_Private" = null
+ "1_Sensitive" = null
+ }
+}
+
variable "data_force_destroy" {
description = "Flag to set 'force_destroy' on data services like BigQery or Cloud Storage."
type = bool
@@ -46,7 +66,7 @@ variable "folder_ids" {
# tfdoc:variable:source 01-resman
description = "Folder to be used for the networking resources in folders/nnnn format."
type = object({
- data-platform = string
+ data-platform-dev = string
})
}
@@ -68,6 +88,12 @@ variable "host_project_ids" {
})
}
+variable "location" {
+ description = "Location used for multi-regional resources."
+ type = string
+ default = "eu"
+}
+
variable "network_config_composer" {
description = "Network configurations to use for Composer."
type = object({
diff --git a/fast/stages/03-gke-multitenant/README.md b/fast/stages/03-gke-multitenant/README.md
new file mode 100644
index 0000000000..f08910c834
--- /dev/null
+++ b/fast/stages/03-gke-multitenant/README.md
@@ -0,0 +1,9 @@
+# GKE Multitenant stage
+
+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 [`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
+
+Refer to [the `dev` documentation](./dev/README.md) configuration details, and to [the `gke-serverless` documentation](../../../blueprints/gke/multitenant-fleet) for the architectural design and decisions taken.
diff --git a/fast/stages/03-gke-multitenant/dev/README.md b/fast/stages/03-gke-multitenant/dev/README.md
new file mode 100644
index 0000000000..c446fbcb4a
--- /dev/null
+++ b/fast/stages/03-gke-multitenant/dev/README.md
@@ -0,0 +1,169 @@
+# GKE Multitenant
+
+This stage allows creation and management of a fleet of GKE multitenant clusters, optionally leveraging GKE Hub to configure additional features. It's designed to be replicated once for every homogeneous set of clusters, either per environment or with more granularity as needed (e.g. teams or sets of teams sharing similar requirements).
+
+The following diagram illustrates the high-level design of created resources, which can be adapted to specific requirements via variables:
+
++ +
+ +## Design overview and choices + +> The detailed architecture of the underlying resources is explained in the documentation of [GKE multitenant module](../../../../blueprints/gke/multitenant-fleet/README.md). + +This stage creates a project containing and as many clusters and node pools as requested by the user through the [variables](#variables) explained below. The GKE clusters are created with the with the following setup: + +- All clusters are assumed to be [private](https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters), therefore only [VPC-native clusters](https://cloud.google.com/kubernetes-engine/docs/concepts/alias-ips) are supported. +- Logging and monitoring configured to use Cloud Operations for system components and user workloads. +- [GKE metering](https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-usage-metering) enabled by default and stored in a bigquery dataset created within the project. +- Optional [GKE Fleet](https://cloud.google.com/kubernetes-engine/docs/fleets-overview) support with the possibility to enable any of the following features: + - [Fleet workload identity](https://cloud.google.com/anthos/fleet-management/docs/use-workload-identity) + - [Anthos Config Management](https://cloud.google.com/anthos-config-management/docs/overview) + - [Anthos Service Mesh](https://cloud.google.com/service-mesh/docs/overview) + - [Anthos Identity Service](https://cloud.google.com/anthos/identity/setup/fleet) + - [Multi-cluster services](https://cloud.google.com/kubernetes-engine/docs/concepts/multi-cluster-services) + - [Multi-cluster ingress](https://cloud.google.com/kubernetes-engine/docs/concepts/multi-cluster-ingress). +- Support for [Config Sync](https://cloud.google.com/anthos-config-management/docs/config-sync-overview), [Hierarchy Controller](https://cloud.google.com/anthos-config-management/docs/concepts/hierarchy-controller), and [Policy Controller](https://cloud.google.com/anthos-config-management/docs/concepts/policy-controller) when using Anthos Config Management. +- [Groups for GKE](https://cloud.google.com/kubernetes-engine/docs/how-to/google-groups-rbac) can be enabled to facilitate the creation of flexible RBAC policies referencing group principals. +- Support for [application layer secret encryption](https://cloud.google.com/kubernetes-engine/docs/how-to/encrypting-secrets). +- Support to customize peering configuration of the control plane VPC (e.g. to import/export routes to the peered network) +- Some features are enabled by default in all clusters: + - [Intranode visibility](https://cloud.google.com/kubernetes-engine/docs/how-to/intranode-visibility) + - [Dataplane v2](https://cloud.google.com/kubernetes-engine/docs/concepts/dataplane-v2) + - [Shielded GKE nodes](https://cloud.google.com/kubernetes-engine/docs/how-to/shielded-gke-nodes) + - [Workload identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) + - [Node local DNS cache](https://cloud.google.com/kubernetes-engine/docs/how-to/nodelocal-dns-cache) + - [Use of the GCE persistent disk CSI driver](https://cloud.google.com/kubernetes-engine/docs/how-to/persistent-volumes/gce-pd-csi-driver) + - Node [auto-upgrade](https://cloud.google.com/kubernetes-engine/docs/how-to/node-auto-upgrades) and [auto-repair](https://cloud.google.com/kubernetes-engine/docs/how-to/node-auto-repair) for all node pools + +## 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. + +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: + +- 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` + - `roles/xpnAdmin` +- 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 +$ 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 + +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: + +- the `cluster_default` variable allows defining common defaults for all clusters +- the `clusters` variable is used to declare the actual GKE clusters and allows overriding defaults on a per-cluster basis +- the `nodepool_defaults` variable allows definining common defaults for all node pools +- the `nodepools` variable is used to declare cluster node pools and allows overriding defaults on a per-cluster basis + +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 is entirely optional, and uses three separate variables: + +- `fleet_features`: specifies the [GKE fleet](https://cloud.google.com/anthos/fleet-management/docs/fleet-concepts#fleet-enabled-components) features you want activate +- `fleet_configmanagement_templates`: defines configuration templates for specific sets of features ([Config Management](https://cloud.google.com/anthos-config-management/docs/how-to/install-anthos-config-management) currently) +- `fleet_configmanagement_clusters`: specifies which clusters are managed by fleet features, and the optional Config Management template for each cluster +- `fleet_workload_identity`: to enables optional centralized [Workload Identity](https://cloud.google.com/anthos/fleet-management/docs/use-workload-identity) + +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 +``` + + + + +## Files + +| name | description | modules | resources | +|---|---|---|---| +| [main.tf](./main.tf) | GKE multitenant for development environment. |multitenant-fleet
| |
+| [outputs.tf](./outputs.tf) | Output variables. | | google_storage_bucket_object
· local_file
|
+| [variables.tf](./variables.tf) | Module variables. | | |
+
+## Variables
+
+| 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)
| | []
| |
+
+## Outputs
+
+| name | description | sensitive | consumers |
+|---|---|:---:|---|
+| [cluster_ids](outputs.tf#L57) | Cluster ids. | | |
+| [clusters](outputs.tf#L62) | Cluster resources. | ✓ | |
+| [project_id](outputs.tf#L68) | GKE project id. | | |
+
+
diff --git a/fast/stages/03-gke-multitenant/dev/diagram.png b/fast/stages/03-gke-multitenant/dev/diagram.png
new file mode 100644
index 0000000000..a282e7d5e6
Binary files /dev/null and b/fast/stages/03-gke-multitenant/dev/diagram.png differ
diff --git a/fast/stages/03-gke-multitenant/dev/main.tf b/fast/stages/03-gke-multitenant/dev/main.tf
new file mode 100644
index 0000000000..8d87b9076e
--- /dev/null
+++ b/fast/stages/03-gke-multitenant/dev/main.tf
@@ -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.
+ */
+
+# tfdoc:file:description GKE multitenant for development environment.
+
+module "gke-multitenant" {
+ source = "../../../../blueprints/gke/multitenant-fleet"
+ billing_account_id = var.billing_account.id
+ folder_id = var.folder_ids.gke-dev
+ project_id = "gke-clusters-0"
+ group_iam = var.group_iam
+ iam = var.iam
+ labels = merge(var.labels, { environment = "dev" })
+ prefix = "${var.prefix}-dev"
+ project_services = var.project_services
+ vpc_config = {
+ host_project_id = var.host_project_ids.dev-spoke-0
+ vpc_self_link = var.vpc_self_links.dev-spoke-0
+ }
+ clusters = var.clusters
+ nodepools = var.nodepools
+ fleet_configmanagement_clusters = var.fleet_configmanagement_clusters
+ fleet_configmanagement_templates = var.fleet_configmanagement_templates
+ fleet_features = var.fleet_features
+ fleet_workload_identity = var.fleet_workload_identity
+}
diff --git a/fast/stages/03-gke-multitenant/dev/outputs.tf b/fast/stages/03-gke-multitenant/dev/outputs.tf
new file mode 100644
index 0000000000..87b0ca737c
--- /dev/null
+++ b/fast/stages/03-gke-multitenant/dev/outputs.tf
@@ -0,0 +1,71 @@
+# 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.
+
+# 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.
+
+# tfdoc:file:description Output variables.
+
+locals {
+ tfvars = {
+ clusters = module.gke-multitenant.cluster_ids
+ project_ids = {
+ gke-dev = module.gke-multitenant.project_id
+ }
+ }
+}
+
+# generate tfvars file for subsequent stages
+
+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"
+ content = jsonencode(local.tfvars)
+}
+
+resource "google_storage_bucket_object" "tfvars" {
+ bucket = var.automation.outputs_bucket
+ name = "tfvars/03-gke-dev.auto.tfvars.json"
+ content = jsonencode(local.tfvars)
+}
+
+# outputs
+
+output "cluster_ids" {
+ description = "Cluster ids."
+ value = module.gke-multitenant.cluster_ids
+}
+
+output "clusters" {
+ description = "Cluster resources."
+ value = module.gke-multitenant.clusters
+ sensitive = true
+}
+
+output "project_id" {
+ description = "GKE project id."
+ value = module.gke-multitenant.project_id
+}
diff --git a/fast/stages/03-gke-multitenant/dev/variables.tf b/fast/stages/03-gke-multitenant/dev/variables.tf
new file mode 100644
index 0000000000..6be89126a5
--- /dev/null
+++ b/fast/stages/03-gke-multitenant/dev/variables.tf
@@ -0,0 +1,231 @@
+/**
+ * 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.
+ */
+
+# we deal with one env here
+# 1 project, m clusters
+# cloud dns for gke?
+
+variable "automation" {
+ # tfdoc:variable:source 00-bootstrap
+ description = "Automation resources created by the bootstrap stage."
+ type = object({
+ outputs_bucket = string
+ })
+}
+
+variable "billing_account" {
+ # tfdoc:variable:source 00-bootstrap
+ description = "Billing account id and organization id ('nnnnnnnn' or null)."
+ type = object({
+ id = string
+ organization_id = number
+ })
+}
+
+variable "clusters" {
+ description = "Clusters configuration. Refer to the gke-cluster module for type details."
+ type = map(object({
+ cluster_autoscaling = optional(any)
+ description = optional(string)
+ enable_addons = optional(any, {
+ horizontal_pod_autoscaling = true, http_load_balancing = true
+ })
+ enable_features = optional(any, {
+ workload_identity = true
+ })
+ issue_client_certificate = optional(bool, false)
+ labels = optional(map(string))
+ location = string
+ logging_config = optional(list(string), ["SYSTEM_COMPONENTS"])
+ maintenance_config = optional(any, {
+ daily_window_start_time = "03:00"
+ recurring_window = null
+ maintenance_exclusion = []
+ })
+ max_pods_per_node = optional(number, 110)
+ min_master_version = optional(string)
+ monitoring_config = optional(object({
+ enable_components = optional(list(string), ["SYSTEM_COMPONENTS"])
+ managed_prometheus = optional(bool)
+ }))
+ node_locations = optional(list(string))
+ private_cluster_config = optional(any)
+ release_channel = optional(string)
+ vpc_config = object({
+ subnetwork = string
+ network = optional(string)
+ secondary_range_blocks = optional(object({
+ pods = string
+ services = string
+ }))
+ secondary_range_names = optional(object({
+ pods = string
+ services = string
+ }), { pods = "pods", services = "services" })
+ master_authorized_ranges = optional(map(string))
+ master_ipv4_cidr_block = optional(string)
+ })
+ }))
+ default = {}
+ nullable = false
+}
+
+variable "fleet_configmanagement_clusters" {
+ description = "Config management features enabled on specific sets of member clusters, in config name => [cluster name] format."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+
+variable "fleet_configmanagement_templates" {
+ description = "Sets of config management configurations that can be applied to member clusters, in config name => {options} format."
+ type = map(object({
+ binauthz = bool
+ config_sync = object({
+ git = object({
+ gcp_service_account_email = string
+ https_proxy = string
+ policy_dir = string
+ secret_type = string
+ sync_branch = string
+ sync_repo = string
+ sync_rev = string
+ sync_wait_secs = number
+ })
+ prevent_drift = string
+ source_format = string
+ })
+ hierarchy_controller = object({
+ enable_hierarchical_resource_quota = bool
+ enable_pod_tree_labels = bool
+ })
+ policy_controller = object({
+ audit_interval_seconds = number
+ exemptable_namespaces = list(string)
+ log_denies_enabled = bool
+ referential_rules_enabled = bool
+ template_library_installed = bool
+ })
+ version = string
+ }))
+ default = {}
+ nullable = false
+}
+
+variable "fleet_features" {
+ description = "Enable and configue fleet features. Set to null to disable GKE Hub if fleet workload identity is not used."
+ type = object({
+ appdevexperience = bool
+ configmanagement = bool
+ identityservice = bool
+ multiclusteringress = string
+ multiclusterservicediscovery = bool
+ servicemesh = bool
+ })
+ default = null
+}
+
+variable "fleet_workload_identity" {
+ description = "Use Fleet Workload Identity for clusters. Enables GKE Hub if set to true."
+ type = bool
+ default = false
+ nullable = false
+}
+
+variable "folder_ids" {
+ # tfdoc:variable:source 01-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
+ })
+}
+
+variable "group_iam" {
+ description = "Project-level authoritative IAM bindings for groups in {GROUP_EMAIL => [ROLES]} format. Use group emails as keys, list of roles as values."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "host_project_ids" {
+ # tfdoc:variable:source 02-networking
+ description = "Host project for the shared VPC."
+ type = object({
+ dev-spoke-0 = string
+ })
+}
+
+variable "iam" {
+ description = "Project-level authoritative IAM bindings for users and service accounts in {ROLE => [MEMBERS]} format."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "labels" {
+ description = "Project-level labels."
+ type = map(string)
+ default = {}
+}
+
+variable "nodepools" {
+ description = "Nodepools configuration. Refer to the gke-nodepool module for type details."
+ type = map(map(object({
+ gke_version = optional(string)
+ labels = optional(map(string), {})
+ max_pods_per_node = optional(number)
+ name = optional(string)
+ node_config = optional(any, { disk_type = "pd-balanced" })
+ node_count = optional(map(number), { initial = 1 })
+ node_locations = optional(list(string))
+ nodepool_config = optional(any)
+ pod_range = optional(any)
+ reservation_affinity = optional(any)
+ service_account = optional(any)
+ sole_tenant_nodegroup = optional(string)
+ tags = optional(list(string))
+ taints = optional(list(any))
+ })))
+ default = {}
+ nullable = false
+}
+
+variable "outputs_location" {
+ description = "Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable."
+ type = string
+ default = null
+}
+
+variable "prefix" {
+ description = "Prefix used for resources that need unique names."
+ type = string
+}
+
+variable "project_services" {
+ description = "Additional project services to enable."
+ type = list(string)
+ default = []
+ nullable = false
+}
+
+variable "vpc_self_links" {
+ # tfdoc:variable:source 02-networking
+ description = "Self link for the shared VPC."
+ type = object({
+ dev-spoke-0 = string
+ })
+}
diff --git a/fast/stages/03-project-factory/dev/README.md b/fast/stages/03-project-factory/dev/README.md
index ba5056140e..8fe213cee9 100644
--- a/fast/stages/03-project-factory/dev/README.md
+++ b/fast/stages/03-project-factory/dev/README.md
@@ -53,7 +53,7 @@ It's of course possible to run this stage in isolation, by making sure the archi
### 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.
+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
@@ -84,7 +84,7 @@ If you're not using Fast, refer to the [Variables](#variables) table at the bott
Besides the values above, a project factory takes 2 additional inputs:
- `data/defaults.yaml`, manually configured by adapting the [`data/defaults.yaml`](./data/defaults.yaml), which defines per-environment default values e.g., for billing alerts and labels.
-- `data/projects/*.yaml`, one file per project (optionally grouped in folders), which configures each project. A [`data/projects/project.yaml`](./data/projects/project.yaml) is provided as reference and documentation for the schema. Projects will be named after the filename, e.g., `fast-dev-lab0.yaml` will create project `fast-dev-lab0`.
+- `data/projects/*.yaml`, one file per project (optionally grouped in folders), which configures each project. A [`data/projects/project.yaml`](./data/projects/project.yaml.sample) is provided as reference and documentation for the schema. Projects will be named after the filename, e.g., `fast-dev-lab0.yaml` will create project `fast-dev-lab0`.
Once the configuration is complete, run the project factory by running
@@ -109,12 +109,12 @@ 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#L47) | Prefix used for resources that need unique names. Use 9 characters or less. | string
| ✓ | | 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#L41) | Relative path for the file storing the project factory configuration. | string
| | "data/defaults.yaml"
| |
-| [environment_dns_zone](variables.tf#L34) | DNS zone suffix for environment. | string
| | null
| 02-networking
|
-| [host_project_ids](variables.tf#L67) | Host project for the shared VPC. | object({…})
| | null
| 02-networking
|
-| [vpc_self_links](variables.tf#L58) | Self link for the shared VPC. | object({…})
| | null
| 02-networking
|
+| [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
|
## Outputs
diff --git a/fast/stages/03-project-factory/dev/data/projects/project.yaml.sample b/fast/stages/03-project-factory/dev/data/projects/project.yaml.sample
index 13a8f5f52e..88ba0bf50a 100644
--- a/fast/stages/03-project-factory/dev/data/projects/project.yaml.sample
+++ b/fast/stages/03-project-factory/dev/data/projects/project.yaml.sample
@@ -48,15 +48,15 @@ labels:
# [opt] Org policy overrides defined at project level
org_policies:
- policy_boolean:
- constraints/compute.disableGuestAttributesAccess: true
- policy_list:
- constraints/compute.trustedImageProjects:
- inherit_from_parent: null
- status: true
- suggested_value: null
+ constraints/compute.disableGuestAttributesAccess:
+ enforce: true
+ constraints/compute.trustedImageProjects:
+ allow:
values:
- projects/fast-dev-iac-core-0
+ constraints/compute.vmExternalIpAccess:
+ deny:
+ all: true
# [opt] Service account to create for the project and their roles on the project
# in name => [roles] format
diff --git a/fast/stages/03-project-factory/dev/main.tf b/fast/stages/03-project-factory/dev/main.tf
index 802bff9dba..e0deb24856 100644
--- a/fast/stages/03-project-factory/dev/main.tf
+++ b/fast/stages/03-project-factory/dev/main.tf
@@ -33,7 +33,7 @@ locals {
}
module "projects" {
- source = "../../../../examples/factories/project-factory"
+ source = "../../../../blueprints/factories/project-factory"
for_each = local.projects
defaults = local.defaults
project_id = each.key
@@ -41,7 +41,7 @@ module "projects" {
billing_alert = try(each.value.billing_alert, null)
dns_zones = try(each.value.dns_zones, [])
essential_contacts = try(each.value.essential_contacts, [])
- folder_id = each.value.folder_id
+ folder_id = try(each.value.folder_id, local.defaults.folder_id)
group_iam = try(each.value.group_iam, {})
iam = try(each.value.iam, {})
kms_service_agents = try(each.value.kms, {})
@@ -49,8 +49,9 @@ module "projects" {
org_policies = try(each.value.org_policies, null)
prefix = var.prefix
service_accounts = try(each.value.service_accounts, {})
+ service_accounts_iam = try(each.value.service_accounts_iam, {})
services = try(each.value.services, [])
- service_identities_iam = try(each.value.services_iam, {})
+ service_identities_iam = try(each.value.service_identities_iam, {})
vpc = try(each.value.vpc, null)
}
diff --git a/fast/stages/03-project-factory/dev/variables.tf b/fast/stages/03-project-factory/dev/variables.tf
index 0fe8c7dc18..2993bfba7b 100644
--- a/fast/stages/03-project-factory/dev/variables.tf
+++ b/fast/stages/03-project-factory/dev/variables.tf
@@ -31,6 +31,12 @@ variable "data_dir" {
default = "data/projects"
}
+variable "defaults_file" {
+ description = "Relative path for the file storing the project factory configuration."
+ type = string
+ default = "data/defaults.yaml"
+}
+
variable "environment_dns_zone" {
# tfdoc:variable:source 02-networking
description = "DNS zone suffix for environment."
@@ -38,10 +44,13 @@ variable "environment_dns_zone" {
default = null
}
-variable "defaults_file" {
- description = "Relative path for the file storing the project factory configuration."
- type = string
- default = "data/defaults.yaml"
+variable "host_project_ids" {
+ # tfdoc:variable:source 02-networking
+ description = "Host project for the shared VPC."
+ type = object({
+ dev-spoke-0 = string
+ })
+ default = null
}
variable "prefix" {
@@ -63,12 +72,3 @@ variable "vpc_self_links" {
})
default = null
}
-
-variable "host_project_ids" {
- # tfdoc:variable:source 02-networking
- description = "Host project for the shared VPC."
- type = object({
- dev-spoke-0 = string
- })
- default = null
-}
diff --git a/fast/stages/CLEANUP.md b/fast/stages/CLEANUP.md
new file mode 100644
index 0000000000..e5f1418f75
--- /dev/null
+++ b/fast/stages/CLEANUP.md
@@ -0,0 +1,114 @@
+# 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
+
+## Stage 3 (Project Factory)
+
+```bash
+cd $FAST_PWD/03-project-factory/prod/
+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/
+
+# remove BQ dataset manually
+for x in $(terraform state list | grep google_bigquery_dataset); do
+ terraform state rm "$x";
+done
+
+terraform destroy
+```
+
+
+## Stage 2 (Security)
+```bash
+cd $FAST_PWD/02-security/
+terraform destroy
+```
+
+## Stage 2 (Networking)
+```bash
+cd $FAST_PWD/02-networking-XXX/
+terraform destroy
+```
+
+A minor glitch can surface running `terraform destroy`, where the service project attachments to the Shared VPCs will not get destroyed even with the relevant API call succeeding. We are investigating the issue but in the meantime, manually remove the attachment in the Cloud console or via the ```gcloud beta compute shared-vpc associated-projects remove``` [command](https://cloud.google.com/sdk/gcloud/reference/beta/compute/shared-vpc/associated-projects/remove) when destroy fails, and then relaunch the command.
+
+## Stage 1 (Resource Management)
+
+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/
+
+# remove buckets from state since terraform refuses to delete them
+for x in $(terraform state list | grep google_storage_bucket.bucket); do
+ terraform state rm "$x"
+done
+
+terraform destroy
+```
+
+## Stage 0 (Bootstrap)
+
+**Warning: you should follow these steps carefully as we will modify our own permissions. Ensure you can grant yourself the Organization Admin role again. Otherwise, you will not be able to finish the destruction process and will, most likely, get locked out of your organization.**
+
+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/
+
+# remove provider config to execute without SA impersonation
+rm 00-bootstrap-providers.tf
+
+# migrate to local state
+terraform init -migrate-state
+
+# remove GCS buckets and BQ dataset manually
+for x in $(terraform state list | grep google_storage_bucket.bucket); do
+ terraform state rm "$x";
+done
+
+for x in $(terraform state list | grep google_bigquery_dataset); do
+ terraform state rm "$x";
+done
+
+terraform destroy
+```
+
+When the destroy fails, continue with the steps below. Again, make sure your user (the one you are using to execute this step) has the Organization Administrator role, as we will remove the permissions for the organization-admins group
+
+```bash
+# Add the Organization Admin role to $BU_USER in the GCP Console
+# then execute the command below to grant yourself the permissions needed
+# to finish the destruction
+export FAST_DESTROY_ROLES="roles/billing.admin roles/logging.admin \
+ roles/iam.organizationRoleAdmin roles/resourcemanager.projectDeleter \
+ roles/resourcemanager.folderAdmin roles/owner"
+
+export FAST_BU=$(gcloud config list --format 'value(core.account)')
+
+# find your org id
+gcloud organizations list --filter display_name:[part of your domain]
+
+# set your org id
+export FAST_ORG_ID=XXXX
+
+for role in $FAST_DESTROY_ROLES; do
+ gcloud organizations add-iam-policy-binding $FAST_ORG_ID \
+ --member user:$FAST_BU --role $role
+done
+
+terraform destroy
+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.
diff --git a/fast/stages/COMPANION.md b/fast/stages/COMPANION.md
new file mode 100644
index 0000000000..e0f6ec6215
--- /dev/null
+++ b/fast/stages/COMPANION.md
@@ -0,0 +1,222 @@
+# 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.
+
+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@`
+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"
+
+# set the initial user variable via gcloud
+export FAST_BU=$(gcloud config list --format 'value(core.account)')
+
+# find your org id. change "fast.example.com" with your own org domain
+gcloud organizations list --filter display_name:fast.example.com
+
+# set your org id
+export FAST_ORG_ID=1234567890
+
+# set needed roles (do not change this)
+export FAST_ROLES="roles/billing.admin roles/logging.admin \
+roles/iam.organizationRoleAdmin roles/resourcemanager.projectCreator"
+
+for role in $FAST_ROLES; do
+gcloud organizations add-iam-policy-binding $FAST_ORG_ID \
+--member user:$FAST_BU --role $role
+done
+```
+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!
+export FAST_BA_ID=XXXXXX-YYYYYY-ZZZZZZ
+# set needed roles (do not change this)
+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.
+
+## 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
+
+# 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={
+ id="XXXXXX-YYYYYY-ZZZZZZ"
+ organization_id="01234567890"
+}
+# get the required info by running `gcloud organizations list`
+organization={
+ id="01234567890"
+ domain="fast.example.com"
+ customer_id="Cxxxxxxx"
+}
+# create your own 4-letters prefix
+prefix="abcd"
+
+# path for automatic generation of configs
+outputs_location = "~/fast-config"
+```
+
+```bash
+# run init and apply
+terraform init
+terraform apply -var bootstrap_user=$FAST_BU
+
+# link the generated provider file
+ln -s ~/fast-config/providers/00-bootstrap* .
+
+# 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
+
+# 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 .
+ln -s ~/fast-config/tfvars/globals.auto.tfvars.json .
+
+# 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"
+
+# optional
+team_folders = {
+ team-1 = {
+ descriptive_name = "Team 1"
+ group_iam = {
+ "team-1-users@fast.example.com" = ["roles/viewer"]
+ }
+ impersonation_groups = [
+ "team-1-admins@fast.example.com"
+ ]
+ }
+}
+```
+```bash
+# run init and apply
+terraform init
+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
+
+# 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/tfvars/globals.auto.tfvars.json .
+
+# 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
+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/tfvars/globals.auto.tfvars.json .
+
+# 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)
+```bash
+# run init and apply
+terraform init
+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 .
+ln -s ~/fast-config/tfvars/globals.auto.tfvars.json .
+
+# 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
+cp data/projects/project.yaml.sample data/projects/YOUR_PROJECT_NAME.yaml
+edit data/projects/YOUR_PROJECT_NAME.yaml
+
+# run init and apply
+terraform init
+terraform apply
+```
diff --git a/fast/stages/FAQ.md b/fast/stages/FAQ.md
new file mode 100644
index 0000000000..bd9559d481
--- /dev/null
+++ b/fast/stages/FAQ.md
@@ -0,0 +1,29 @@
+
+## 00-bootstrap
+
+1. How to handle requests where automation, logging and/or billing export projects are not under organization but in different folders.
+ - Run bootstrap stage and let automation, logging and/or billing projects be created under organization.
+ - Run resource manager stage or any other custom stage which creates the folders where these projects will reside.
+ - Once folders are created add folder ids to varibale "project_parent_ids" in bootstrap stage and run bootstrap stage.
+ - This step will move the projects from organization to the parent folders specificed.
+
+## cicd
+
+1. Why do we need two seperate ServiceAccounts when configuring cicd pipelines (cicd SA and IaC SA)
+ - Having seperate service accounts helps shutdown the pipeline incase of any issues and still keep IaC SA and ability to run terraform plan/apply manually.
+ - A pipeline can only generate a token that can get access to an SA. It cannot directly call a provider file to impersonate IaC SA.
+ - Having providers file that allows impersonation to IaC SA allows flexibility to run terraform manually or from CICD Pipelines.
+ ![CICD SA and IaC SA](IaC_SA.png)
+
+## Authenciation
+
+1. If you are seeing "Permission Issues" when doing terraform apply and the identity with which you are running terraform has correct permissions;
+ run below command so that correct auth credentials are picked by ADC when terraform commands are executed
+
+ ````bash
+ gcloud auth application-default login
+ ````
+
+
+ Refer to [GCP Authentication](https://cloud.google.com/docs/authentication
+ ) and [Terraform Provider](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference) for more information
diff --git a/fast/stages/IaC_SA.png b/fast/stages/IaC_SA.png
new file mode 100644
index 0000000000..8247f429e6
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 ba01e6b219..9b41bf1cae 100644
--- a/fast/stages/README.md
+++ b/fast/stages/README.md
@@ -17,11 +17,13 @@ To achieve this, we rely on specific GCP functionality like [delegated role gran
Refer to each stage's documentation for a detailed description of its purpose, the architectural choices made in its design, and how it can be configured and wired together to terraform a whole GCP organization. The following is a brief overview of each stage.
+To destroy a previous FAST deployment follow the instructions detailed in [cleanup](CLEANUP.md).
+
## Organizational level (00-01)
- [Bootstrap](00-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 project id, organization-level custom roles
+ Exports: automation variables, organization-level custom roles
- [Resource Management](01-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
@@ -31,8 +33,8 @@ Refer to each stage's documentation for a detailed description of its purpose, t
- [Security](02-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-peering/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 three flavors: [spokes connected via VPN](02-networking-vpn/README.md), [and spokes connected via appliances](02-networking-nva/README.md), and [and spokes connected via VPC peering](02-networking-peering/README.md).\
+- 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).\
Exports: host project ids and numbers, vpc self links
## Environment-level resources (03)
@@ -40,5 +42,5 @@ Refer to each stage's documentation for a detailed description of its purpose, t
- [Project Factory](03-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 (in development)
+- [GKE Multitenant](03-gke-multitenant/dev/)
- GCE Migration (in development)
diff --git a/modules/README.md b/modules/README.md
index 6303f01556..f5ed3c9c91 100644
--- a/modules/README.md
+++ b/modules/README.md
@@ -13,12 +13,13 @@ These modules are not necessarily backward compatible. Changes breaking compatib
These modules are used in the examples included in this repository. If you are using any of those examples in your own Terraform configuration, make sure that you are using the same version for all the modules, and switch module sources to GitHub format using references. The recommended approach to working with Fabric modules is the following:
- Fork the repository and own the fork. This will allow you to:
- - Evolve the existing modules.
- - Create your own modules.
- - Sync from the upstream repository to get all the updates.
-
+ - Evolve the existing modules.
+ - Create your own modules.
+ - Sync from the upstream repository to get all the updates.
+
- Use GitHub sources with refs to reference the modules. See an example below:
- ```
+
+ ```terraform
module "project" {
source = "github.com/GoogleCloudPlatform/cloud-foundation-fabric//modules/project?ref=v13.0.0"
name = "my-project"
@@ -30,62 +31,63 @@ These modules are used in the examples included in this repository. If you are u
## Foundational modules
- [billing budget](./billing-budget)
+- [Cloud Identity group](./cloud-identity-group/)
- [folder](./folder)
+- [service accounts](./iam-service-account)
- [logging bucket](./logging-bucket)
-- [naming convention](./naming-convention)
- [organization](./organization)
- [project](./project)
- [projects-data-source](./projects-data-source)
-- [service account](./iam-service-account)
## Networking modules
-- [address reservation](./net-address)
-- [Cloud DNS](./dns)
-- [Cloud NAT](./net-cloudnat)
+- [DNS](./dns)
- [Cloud Endpoints](./endpoints)
-- [L4 Internal Load Balancer](./net-ilb)
-- [Service Directory](./service-directory)
+- [address reservation](./net-address)
+- [NAT](./net-cloudnat)
+- [Global Load Balancer (classic)](./net-glb/)
+- [L4 ILB](./net-ilb)
+- [L7 ILB](./net-ilb-l7)
- [VPC](./net-vpc)
- [VPC firewall](./net-vpc-firewall)
- [VPC peering](./net-vpc-peering)
-- [VPN static](./net-vpn-static)
- [VPN dynamic](./net-vpn-dynamic)
- [HA VPN](./net-vpn-ha)
-- [ ] TODO: xLB modules
+- [VPN static](./net-vpn-static)
+- [Service Directory](./service-directory)
## Compute/Container
-- [COS container](./cloud-config-container/onprem/) (coredns, mysql, onprem, squid)
+- [VM/VM group](./compute-vm)
+- [MIG](./compute-mig)
+- [COS container](./cloud-config-container/cos-generic-metadata/) (coredns/mysql/nva/onprem/squid)
- [GKE cluster](./gke-cluster)
-- [GKE nodepool](./gke-nodepool)
- [GKE hub](./gke-hub)
-- [Managed Instance Group](./compute-mig)
-- [VM/VM group](./compute-vm)
+- [GKE nodepool](./gke-nodepool)
## Data
- [BigQuery dataset](./bigquery-dataset)
-- [Datafusion](./datafusion)
-- [GCS](./gcs)
-- [Pub/Sub](./pubsub)
- [Bigtable instance](./bigtable-instance)
- [Cloud SQL instance](./cloudsql-instance)
- [Data Catalog Policy Tag](./data-catalog-policy-tag)
+- [Datafusion](./datafusion)
+- [GCS](./gcs)
+- [Pub/Sub](./pubsub)
## Development
+- [API Gateway](./api-gateway)
+- [Apigee](./apigee)
- [Artifact Registry](./artifact-registry)
- [Container Registry](./container-registry)
-- [Source Repository](./source-repository)
-- [Apigee Organization](./apigee-organization)
-- [Apigee X Instance](./apigee-x-instance)
-- [API Gateway](./api-gateway)
+- [Cloud Source Repository](./source-repository)
## Security
-- [Cloud KMS](./kms)
-- [Secret Manager](./secret-manager)
+- [Binauthz](./binauthz/)
+- [KMS](./kms)
+- [SecretManager](./secret-manager)
- [VPC Service Control](./vpc-sc)
## Serverless
diff --git a/modules/__experimental/net-dns-policy-address/README.md b/modules/__experimental/net-dns-policy-address/README.md
index 36f9e4e1ea..7044ef2ecc 100644
--- a/modules/__experimental/net-dns-policy-address/README.md
+++ b/modules/__experimental/net-dns-policy-address/README.md
@@ -8,11 +8,11 @@ Since it's currently impossible to fetch those addresses using a GCP data source
```hcl
module "dns-policy-addresses" {
- source = "./modules/__experimental/net-dns-policy-addresses"
+ source = "./fabric/modules/__experimental/net-dns-policy-addresses"
project_id = "myproject"
regions = ["europe-west1", "europe-west3"]
}
-# tftest skip
+# tftest skip (uses data sources)
```
The output is a map with lists of addresses of type `DNS_RESOLVER` for each region specified in variables.
diff --git a/modules/__experimental/net-neg/README.md b/modules/__experimental/net-neg/README.md
index ad90c14e96..cb271c5054 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 = "./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
```
@@ -29,19 +30,19 @@ module "neg" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [endpoints](variables.tf#L42) | List of (instance, port, address) of the NEG. | list(object({…}))
| ✓ | |
-| [name](variables.tf#L22) | NEG name. | string
| ✓ | |
-| [network](variables.tf#L27) | Name or self link of the VPC used for the NEG. Use the self link for Shared VPC. | string
| ✓ | |
-| [project_id](variables.tf#L17) | NEG project id. | string
| ✓ | |
-| [subnetwork](variables.tf#L32) | VPC subnetwork name or self link. | string
| ✓ | |
-| [zone](variables.tf#L37) | NEG zone. | string
| ✓ | |
+| [endpoints](variables.tf#L17) | List of (instance, port, address) of the NEG. | list(object({…}))
| ✓ | |
+| [name](variables.tf#L26) | NEG name. | string
| ✓ | |
+| [network](variables.tf#L31) | Name or self link of the VPC used for the NEG. Use the self link for Shared VPC. | string
| ✓ | |
+| [project_id](variables.tf#L36) | NEG project id. | string
| ✓ | |
+| [subnetwork](variables.tf#L41) | VPC subnetwork name or self link. | string
| ✓ | |
+| [zone](variables.tf#L46) | NEG zone. | string
| ✓ | |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [id](outputs.tf#L17) | Network endpoint group ID. | |
-| [self_lnk](outputs.tf#L27) | Network endpoint group self link. | |
-| [size](outputs.tf#L22) | Size of the network endpoint group. | |
+| [self_lnk](outputs.tf#L22) | Network endpoint group self link. | |
+| [size](outputs.tf#L27) | Size of the network endpoint group. | |
diff --git a/modules/__experimental/net-neg/outputs.tf b/modules/__experimental/net-neg/outputs.tf
index a8ccdbf003..cb496f5cf3 100644
--- a/modules/__experimental/net-neg/outputs.tf
+++ b/modules/__experimental/net-neg/outputs.tf
@@ -19,12 +19,12 @@ output "id" {
value = google_compute_network_endpoint_group.group.name
}
-output "size" {
- description = "Size of the network endpoint group."
- value = google_compute_network_endpoint_group.group.size
-}
-
output "self_lnk" {
description = "Network endpoint group self link."
value = google_compute_network_endpoint_group.group.self_link
}
+
+output "size" {
+ description = "Size of the network endpoint group."
+ value = google_compute_network_endpoint_group.group.size
+}
diff --git a/modules/__experimental/net-neg/variables.tf b/modules/__experimental/net-neg/variables.tf
index 0771def150..b4eb42ac62 100644
--- a/modules/__experimental/net-neg/variables.tf
+++ b/modules/__experimental/net-neg/variables.tf
@@ -14,9 +14,13 @@
* limitations under the License.
*/
-variable "project_id" {
- description = "NEG project id."
- type = string
+variable "endpoints" {
+ description = "List of (instance, port, address) of the NEG."
+ type = list(object({
+ instance = string
+ port = number
+ ip_address = string
+ }))
}
variable "name" {
@@ -29,6 +33,11 @@ variable "network" {
type = string
}
+variable "project_id" {
+ description = "NEG project id."
+ type = string
+}
+
variable "subnetwork" {
description = "VPC subnetwork name or self link."
type = string
@@ -38,12 +47,3 @@ variable "zone" {
description = "NEG zone."
type = string
}
-
-variable "endpoints" {
- description = "List of (instance, port, address) of the NEG."
- type = list(object({
- instance = string
- port = number
- ip_address = string
- }))
-}
diff --git a/modules/__experimental/net-neg/versions.tf b/modules/__experimental/net-neg/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/__experimental/net-neg/versions.tf
+++ b/modules/__experimental/net-neg/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/api-gateway/README.md b/modules/api-gateway/README.md
index b503f37e77..0b5fc9283d 100644
--- a/modules/api-gateway/README.md
+++ b/modules/api-gateway/README.md
@@ -6,11 +6,11 @@ This module allows creating an API with its associated API config and API gatewa
## Basic example
```hcl
module "gateway" {
- source = "./modules/api-gateway"
- project_id = "my-project"
- api_id = "api"
- region = "europe-west1"
- spec = <string
| ✓ | |
-| [region](variables.tf#L39) | Region | string
| ✓ | |
+| [region](variables.tf#L39) | Region. | string
| ✓ | |
| [spec](variables.tf#L56) | String with the contents of the OpenAPI spec. | string
| ✓ | |
| [iam](variables.tf#L22) | IAM bindings for the API in {ROLE => [MEMBERS]} format. | map(list(string))
| | null
|
| [labels](variables.tf#L28) | Map of labels. | map(string)
| | null
|
-| [service_account_create](variables.tf#L44) | Flag indicating whether a service account needs to be created | bool
| | false
|
-| [service_account_email](variables.tf#L50) | Service account for creating API configs | string
| | null
|
+| [service_account_create](variables.tf#L44) | Flag indicating whether a service account needs to be created. | bool
| | false
|
+| [service_account_email](variables.tf#L50) | Service account for creating API configs. | string
| | null
|
## Outputs
diff --git a/modules/api-gateway/variables.tf b/modules/api-gateway/variables.tf
index 9625919840..ef5bd41d48 100644
--- a/modules/api-gateway/variables.tf
+++ b/modules/api-gateway/variables.tf
@@ -37,18 +37,18 @@ variable "project_id" {
}
variable "region" {
- description = "Region"
+ description = "Region."
type = string
}
variable "service_account_create" {
- description = "Flag indicating whether a service account needs to be created"
+ description = "Flag indicating whether a service account needs to be created."
type = bool
default = false
}
variable "service_account_email" {
- description = "Service account for creating API configs"
+ description = "Service account for creating API configs."
type = string
default = null
}
diff --git a/modules/api-gateway/versions.tf b/modules/api-gateway/versions.tf
index 039f8199a6..90b632f6d4 100644
--- a/modules/api-gateway/versions.tf
+++ b/modules/api-gateway/versions.tf
@@ -1,29 +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
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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
+#
+# 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.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
-}
\ No newline at end of file
+}
+
+
diff --git a/modules/apigee-organization/README.md b/modules/apigee-organization/README.md
deleted file mode 100644
index 36878896ee..0000000000
--- a/modules/apigee-organization/README.md
+++ /dev/null
@@ -1,127 +0,0 @@
-# Google Apigee Organization Module
-
-This module allows managing a single Apigee organization and its environments and environmentgroups.
-
-## Examples
-
-### Apigee X Evaluation Organization
-
-```hcl
-module "apigee-organization" {
- source = "./modules/apigee-organization"
- project_id = "my-project"
- analytics_region = "us-central1"
- runtime_type = "CLOUD"
- authorized_network = "my-vpc"
- apigee_environments = [
- "eval1",
- "eval2"
- ]
- apigee_envgroups = {
- eval = {
- environments = [
- "eval1",
- "eval2"
- ]
- hostnames = [
- "eval.api.example.com"
- ]
- }
- }
-}
-# tftest modules=1 resources=6
-```
-
-### Apigee X Paid Organization
-
-```hcl
-module "apigee-organization" {
- source = "./modules/apigee-organization"
- project_id = "my-project"
- analytics_region = "us-central1"
- runtime_type = "CLOUD"
- authorized_network = "my-vpc"
- database_encryption_key = "my-data-key"
- apigee_environments = [
- "dev1",
- "dev2",
- "test1",
- "test2"
- ]
- apigee_envgroups = {
- dev = {
- environments = [
- "dev1",
- "dev2"
- ]
- hostnames = [
- "dev.api.example.com"
- ]
- }
- test = {
- environments = [
- "test1",
- "test2"
- ]
- hostnames = [
- "test.api.example.com"
- ]
- }
- }
-}
-# tftest modules=1 resources=11
-```
-
-### Apigee hybrid Organization
-
-```hcl
-module "apigee-organization" {
- source = "./modules/apigee-organization"
- project_id = "my-project"
- analytics_region = "us-central1"
- runtime_type = "HYBRID"
- apigee_environments = [
- "eval1",
- "eval2"
- ]
- apigee_envgroups = {
- eval = {
- environments = [
- "eval1",
- "eval2"
- ]
- hostnames = [
- "eval.api.example.com"
- ]
- }
- }
-}
-# tftest modules=1 resources=6
-```
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [analytics_region](variables.tf#L17) | Analytics Region for the Apigee Organization (immutable). See https://cloud.google.com/apigee/docs/api-platform/get-started/install-cli. | string
| ✓ | |
-| [project_id](variables.tf#L61) | Project ID to host this Apigee organization (will also become the Apigee Org name). | string
| ✓ | |
-| [runtime_type](variables.tf#L66) | Apigee runtime type. Must be `CLOUD` or `HYBRID`. | string
| ✓ | |
-| [apigee_envgroups](variables.tf#L22) | Apigee Environment Groups. | map(object({…}))
| | {}
|
-| [apigee_environments](variables.tf#L31) | Apigee Environment Names. | list(string)
| | []
|
-| [authorized_network](variables.tf#L37) | VPC network self link (requires service network peering enabled (Used in Apigee X only). | string
| | null
|
-| [database_encryption_key](variables.tf#L43) | Cloud KMS key self link (e.g. `projects/foo/locations/us/keyRings/bar/cryptoKeys/baz`) used for encrypting the data that is stored and replicated across runtime instances (immutable, used in Apigee X only). | string
| | null
|
-| [description](variables.tf#L49) | Description of the Apigee Organization. | string
| | "Apigee Organization created by tf module"
|
-| [display_name](variables.tf#L55) | Display Name of the Apigee Organization. | string
| | null
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [envs](outputs.tf#L17) | Apigee Environments. | |
-| [org](outputs.tf#L22) | Apigee Organization. | |
-| [org_ca_certificate](outputs.tf#L27) | Apigee organization CA certificate. | |
-| [org_id](outputs.tf#L32) | Apigee Organization ID. | |
-| [subscription_type](outputs.tf#L37) | Apigee subscription type. | |
-
-
diff --git a/modules/apigee-organization/main.tf b/modules/apigee-organization/main.tf
deleted file mode 100644
index fe798f929f..0000000000
--- a/modules/apigee-organization/main.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.
- */
-
-locals {
- env_envgroup_pairs = flatten([
- for eg_name, eg in var.apigee_envgroups : [
- for e in eg.environments : {
- envgroup = eg_name
- env = e
- }
- ]
- ])
-}
-
-resource "google_apigee_organization" "apigee_org" {
- project_id = var.project_id
- analytics_region = var.analytics_region
- display_name = var.display_name
- description = var.description
- runtime_type = var.runtime_type
- authorized_network = var.authorized_network
- runtime_database_encryption_key_name = var.database_encryption_key
-}
-
-resource "google_apigee_environment" "apigee_env" {
- for_each = toset(var.apigee_environments)
- org_id = google_apigee_organization.apigee_org.id
- name = each.key
-}
-
-resource "google_apigee_envgroup" "apigee_envgroup" {
- for_each = var.apigee_envgroups
- org_id = google_apigee_organization.apigee_org.id
- name = each.key
- hostnames = each.value.hostnames
-}
-
-resource "google_apigee_envgroup_attachment" "env_to_envgroup_attachment" {
- for_each = { for pair in local.env_envgroup_pairs : "${pair.envgroup}-${pair.env}" => pair }
- envgroup_id = google_apigee_envgroup.apigee_envgroup[each.value.envgroup].id
- environment = google_apigee_environment.apigee_env[each.value.env].name
-}
diff --git a/modules/apigee-organization/outputs.tf b/modules/apigee-organization/outputs.tf
deleted file mode 100644
index f53a469611..0000000000
--- a/modules/apigee-organization/outputs.tf
+++ /dev/null
@@ -1,40 +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 "envs" {
- description = "Apigee Environments."
- value = google_apigee_environment.apigee_env
-}
-
-output "org" {
- description = "Apigee Organization."
- value = google_apigee_organization.apigee_org
-}
-
-output "org_ca_certificate" {
- description = "Apigee organization CA certificate."
- value = google_apigee_organization.apigee_org.ca_certificate
-}
-
-output "org_id" {
- description = "Apigee Organization ID."
- value = google_apigee_organization.apigee_org.id
-}
-
-output "subscription_type" {
- description = "Apigee subscription type."
- value = google_apigee_organization.apigee_org.subscription_type
-}
diff --git a/modules/apigee-organization/variables.tf b/modules/apigee-organization/variables.tf
deleted file mode 100644
index d7ab70dacb..0000000000
--- a/modules/apigee-organization/variables.tf
+++ /dev/null
@@ -1,75 +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 "analytics_region" {
- description = "Analytics Region for the Apigee Organization (immutable). See https://cloud.google.com/apigee/docs/api-platform/get-started/install-cli."
- type = string
-}
-
-variable "apigee_envgroups" {
- description = "Apigee Environment Groups."
- type = map(object({
- environments = list(string)
- hostnames = list(string)
- }))
- default = {}
-}
-
-variable "apigee_environments" {
- description = "Apigee Environment Names."
- type = list(string)
- default = []
-}
-
-variable "authorized_network" {
- description = "VPC network self link (requires service network peering enabled (Used in Apigee X only)."
- type = string
- default = null
-}
-
-variable "database_encryption_key" {
- description = "Cloud KMS key self link (e.g. `projects/foo/locations/us/keyRings/bar/cryptoKeys/baz`) used for encrypting the data that is stored and replicated across runtime instances (immutable, used in Apigee X only)."
- type = string
- default = null
-}
-
-variable "description" {
- description = "Description of the Apigee Organization."
- type = string
- default = "Apigee Organization created by tf module"
-}
-
-variable "display_name" {
- description = "Display Name of the Apigee Organization."
- type = string
- default = null
-}
-
-variable "project_id" {
- description = "Project ID to host this Apigee organization (will also become the Apigee Org name)."
- type = string
-}
-
-variable "runtime_type" {
- description = "Apigee runtime type. Must be `CLOUD` or `HYBRID`."
- type = string
- validation {
- condition = contains(["CLOUD", "HYBRID"], var.runtime_type)
- error_message = "Allowed values for runtime_type \"CLOUD\" or \"HYBRID\"."
- }
-}
-
-
diff --git a/modules/apigee-organization/versions.tf b/modules/apigee-organization/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/modules/apigee-organization/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/modules/apigee-x-instance/README.md b/modules/apigee-x-instance/README.md
deleted file mode 100644
index bca575dc71..0000000000
--- a/modules/apigee-x-instance/README.md
+++ /dev/null
@@ -1,68 +0,0 @@
-# Google Apigee X Instance Module
-
-This module allows managing a single Apigee X instance and its environment attachments.
-
-## Examples
-
-### Apigee X Evaluation Instance
-
-```hcl
-module "apigee-x-instance" {
- source = "./modules/apigee-x-instance"
- name = "my-us-instance"
- region = "us-central1"
- ip_range = "10.0.0.0/22"
-
- apigee_org_id = "my-project"
- apigee_environments = [
- "eval1",
- "eval2"
- ]
-}
-# tftest modules=1 resources=3
-```
-
-### Apigee X Paid Instance
-
-```hcl
-module "apigee-x-instance" {
- source = "./modules/apigee-x-instance"
- name = "my-us-instance"
- region = "us-central1"
- ip_range = "10.0.0.0/22"
- disk_encryption_key = "my-disk-key"
-
- apigee_org_id = "my-project"
- apigee_environments = [
- "dev1",
- "dev2",
- "test1",
- "test2"
- ]
-}
-# tftest modules=1 resources=5
-```
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [apigee_org_id](variables.tf#L32) | Apigee Organization ID. | string
| ✓ | |
-| [ip_range](variables.tf#L37) | Customer-provided CIDR block of length 22 for the Apigee instance. | string
| ✓ | |
-| [name](variables.tf#L52) | Apigee instance name. | string
| ✓ | |
-| [region](variables.tf#L57) | Compute region. | string
| ✓ | |
-| [apigee_envgroups](variables.tf#L17) | Apigee Environment Groups. | map(object({…}))
| | {}
|
-| [apigee_environments](variables.tf#L26) | Apigee Environment Names. | list(string)
| | []
|
-| [disk_encryption_key](variables.tf#L46) | Customer Managed Encryption Key (CMEK) self link (e.g. `projects/foo/locations/us/keyRings/bar/cryptoKeys/baz`) used for disk and volume encryption (required for PAID Apigee Orgs only). | string
| | null
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [endpoint](outputs.tf#L17) | Internal endpoint of the Apigee instance. | |
-| [id](outputs.tf#L22) | Apigee instance ID. | |
-| [instance](outputs.tf#L27) | Apigee instance. | |
-| [port](outputs.tf#L32) | Port number of the internal endpoint of the Apigee instance. | |
-
-
diff --git a/modules/apigee-x-instance/main.tf b/modules/apigee-x-instance/main.tf
deleted file mode 100644
index 1ef2c66b1f..0000000000
--- a/modules/apigee-x-instance/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.
- */
-
-resource "google_apigee_instance" "apigee_instance" {
- org_id = var.apigee_org_id
- name = var.name
- location = var.region
- ip_range = var.ip_range
- disk_encryption_key_name = var.disk_encryption_key
-}
-
-resource "google_apigee_instance_attachment" "apigee_instance_attchment" {
- for_each = toset(var.apigee_environments)
- instance_id = google_apigee_instance.apigee_instance.id
- environment = each.key
-}
diff --git a/modules/apigee-x-instance/outputs.tf b/modules/apigee-x-instance/outputs.tf
deleted file mode 100644
index d21a422aaf..0000000000
--- a/modules/apigee-x-instance/outputs.tf
+++ /dev/null
@@ -1,35 +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.
- * limitations under the License.
- * See the License for the specific language governing permissions and
- */
-
-output "endpoint" {
- description = "Internal endpoint of the Apigee instance."
- value = google_apigee_instance.apigee_instance.host
-}
-
-output "id" {
- description = "Apigee instance ID."
- value = google_apigee_instance.apigee_instance.id
-}
-
-output "instance" {
- description = "Apigee instance."
- value = google_apigee_instance.apigee_instance
-}
-
-output "port" {
- description = "Port number of the internal endpoint of the Apigee instance."
- value = google_apigee_instance.apigee_instance.port
-}
diff --git a/modules/apigee-x-instance/variables.tf b/modules/apigee-x-instance/variables.tf
deleted file mode 100644
index 10122a2bea..0000000000
--- a/modules/apigee-x-instance/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 "apigee_envgroups" {
- description = "Apigee Environment Groups."
- type = map(object({
- environments = list(string)
- hostnames = list(string)
- }))
- default = {}
-}
-
-variable "apigee_environments" {
- description = "Apigee Environment Names."
- type = list(string)
- default = []
-}
-
-variable "apigee_org_id" {
- description = "Apigee Organization ID."
- type = string
-}
-
-variable "ip_range" {
- description = "Customer-provided CIDR block of length 22 for the Apigee instance."
- type = string
- validation {
- condition = try(cidrnetmask(var.ip_range), null) == "255.255.252.0"
- error_message = "Invalid CIDR block provided; Allowed pattern for ip_range: X.X.X.X/22."
- }
-}
-
-variable "disk_encryption_key" {
- description = "Customer Managed Encryption Key (CMEK) self link (e.g. `projects/foo/locations/us/keyRings/bar/cryptoKeys/baz`) used for disk and volume encryption (required for PAID Apigee Orgs only)."
- type = string
- default = null
-}
-
-variable "name" {
- description = "Apigee instance name."
- type = string
-}
-
-variable "region" {
- description = "Compute region."
- type = string
-}
diff --git a/modules/apigee-x-instance/versions.tf b/modules/apigee-x-instance/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/modules/apigee-x-instance/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/modules/apigee/README.md b/modules/apigee/README.md
new file mode 100644
index 0000000000..fb26c1e727
--- /dev/null
+++ b/modules/apigee/README.md
@@ -0,0 +1,192 @@
+# Apigee
+
+This module simplifies the creation of a Apigee resources (organization, environment groups, environment group attachments, environments, instances and instance attachments).
+
+## Example
+
+### All resources (CLOUD)
+
+```hcl
+module "apigee" {
+ source = "./fabric/modules/apigee"
+ project_id = "my-project"
+ organization = {
+ display_name = "My Organization"
+ description = "My Organization"
+ authorized_network = "my-vpc"
+ runtime_type = "CLOUD"
+ billing_type = "PAYG"
+ database_encryption_key = "123456789"
+ analytics_region = "europe-west1"
+ }
+ envgroups = {
+ test = ["test.example.com"]
+ prod = ["prod.example.com"]
+ }
+ environments = {
+ apis-test = {
+ display_name = "APIs test"
+ description = "APIs Test"
+ envgroups = ["test"]
+ }
+ apis-prod = {
+ display_name = "APIs prod"
+ description = "APIs prod"
+ envgroups = ["prod"]
+ iam = {
+ "roles/viewer" = ["group:devops@myorg.com"]
+ }
+ }
+ }
+ instances = {
+ instance-test-ew1 = {
+ region = "europe-west1"
+ environments = ["apis-test"]
+ psa_ip_cidr_range = "10.0.4.0/22"
+ }
+ instance-prod-ew3 = {
+ region = "europe-west3"
+ environments = ["apis-prod"]
+ psa_ip_cidr_range = "10.0.5.0/22"
+ }
+ }
+ endpoint_attachments = {
+ endpoint-backend-1 = {
+ region = "europe-west1"
+ service_attachment = "projects/my-project-1/serviceAttachments/gkebackend1"
+ }
+ endpoint-backend-2 = {
+ region = "europe-west1"
+ service_attachment = "projects/my-project-2/serviceAttachments/gkebackend2"
+ }
+ }
+}
+# tftest modules=1 resources=14
+```
+
+### All resources (HYBRID control plane)
+
+```hcl
+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"
+ }
+ envgroups = {
+ test = ["test.example.com"]
+ prod = ["prod.example.com"]
+ }
+ environments = {
+ apis-test = {
+ display_name = "APIs test"
+ description = "APIs Test"
+ envgroups = ["test"]
+ }
+ apis-prod = {
+ display_name = "APIs prod"
+ description = "APIs prod"
+ envgroups = ["prod"]
+ iam = {
+ "roles/viewer" = ["group:devops@myorg.com"]
+ }
+ }
+ }
+}
+# tftest modules=1 resources=8
+```
+
+### New environment group in an existing organization
+
+```hcl
+module "apigee" {
+ source = "./fabric/modules/apigee"
+ project_id = "my-project"
+ envgroups = {
+ test = ["test.example.com"]
+ }
+}
+# tftest modules=1 resources=1
+```
+
+### New environment in an existing environment group
+
+```hcl
+module "apigee" {
+ source = "./fabric/modules/apigee"
+ project_id = "my-project"
+ environments = {
+ apis-test = {
+ display_name = "APIs test"
+ description = "APIs Test"
+ envgroups = ["test"]
+ }
+ }
+}
+# tftest modules=1 resources=2
+```
+
+### New instance attached to an existing environment
+
+```hcl
+module "apigee" {
+ source = "./fabric/modules/apigee"
+ project_id = "my-project"
+ instances = {
+ instance-test-ew1 = {
+ region = "europe-west1"
+ environments = ["apis-test"]
+ psa_ip_cidr_range = "10.0.4.0/22"
+ }
+ }
+}
+# tftest modules=1 resources=2
+```
+
+### New endpoint attachment
+
+Endpoint attachments allow to implement [Apigee southbound network patterns](https://cloud.google.com/apigee/docs/api-platform/architecture/southbound-networking-patterns-endpoints#create-the-psc-attachments).
+
+```hcl
+module "apigee" {
+ source = "./fabric/modules/apigee"
+ project_id = "my-project"
+ endpoint_attachments = {
+ endpoint-backend-1 = {
+ region = "europe-west1"
+ service_attachment = "projects/my-project-1/serviceAttachments/gkebackend1"
+ }
+ }
+}
+# tftest modules=1 resources=1
+```
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [project_id](variables.tf#L75) | 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
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [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
new file mode 100644
index 0000000000..fe34a73829
--- /dev/null
+++ b/modules/apigee/main.tf
@@ -0,0 +1,118 @@
+/**
+ * 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 {
+ org_id = try(google_apigee_organization.organization[0].id, "organizations/${var.project_id}")
+ envgroups = coalesce(var.envgroups, {})
+ environments = coalesce(var.environments, {})
+ instances = coalesce(var.instances, {})
+ endpoint_attachments = coalesce(var.endpoint_attachments, {})
+}
+
+resource "google_apigee_organization" "organization" {
+ count = var.organization == null ? 0 : 1
+ analytics_region = var.organization.analytics_region
+ project_id = var.project_id
+ authorized_network = var.organization.authorized_network
+ billing_type = var.organization.billing_type
+ runtime_type = var.organization.runtime_type
+ runtime_database_encryption_key_name = var.organization.database_encryption_key
+}
+
+resource "google_apigee_envgroup" "envgroups" {
+ for_each = local.envgroups
+ name = each.key
+ hostnames = each.value
+ org_id = local.org_id
+}
+
+resource "google_apigee_environment" "environments" {
+ for_each = local.environments
+ name = each.key
+ display_name = each.value.display_name
+ description = each.value.description
+ dynamic "node_config" {
+ for_each = try(each.value.node_config, null) != null ? [""] : []
+ content {
+ min_node_count = each.value.node_config.min_node_count
+ max_node_count = each.value.node_config.max_node_count
+ }
+ }
+ org_id = local.org_id
+ lifecycle {
+ ignore_changes = [
+ node_config["current_aggregate_node_count"]
+ ]
+ }
+}
+
+resource "google_apigee_envgroup_attachment" "envgroup_attachments" {
+ for_each = merge(concat([for k1, v1 in local.environments : {
+ for v2 in v1.envgroups : "${k1}-${v2}" => {
+ environment = k1
+ envgroup = v2
+ }
+ }])...)
+ envgroup_id = try(google_apigee_envgroup.envgroups[each.value.envgroup].id, each.value.envgroup)
+ environment = google_apigee_environment.environments[each.value.environment].name
+}
+
+resource "google_apigee_environment_iam_binding" "binding" {
+ for_each = merge(concat([for k1, v1 in local.environments : {
+ for k2, v2 in coalesce(v1.iam, {}) : "${k1}-${k2}" => {
+ environment = "${k1}"
+ role = k2
+ members = v2
+ }
+ }])...)
+ org_id = local.org_id
+ env_id = google_apigee_environment.environments[each.value.environment].name
+ role = each.value.role
+ members = each.value.members
+}
+
+resource "google_apigee_instance" "instances" {
+ for_each = local.instances
+ name = each.key
+ display_name = each.value.display_name
+ description = each.value.description
+ location = each.value.region
+ org_id = local.org_id
+ ip_range = each.value.psa_ip_cidr_range
+ disk_encryption_key_name = each.value.disk_encryption_key
+ consumer_accept_list = each.value.consumer_accept_list
+}
+
+resource "google_apigee_instance_attachment" "instance_attachments" {
+ for_each = merge(concat([for k1, v1 in local.instances : {
+ for v2 in v1.environments :
+ "${k1}-${v2}" => {
+ instance = k1
+ environment = v2
+ }
+ }])...)
+ instance_id = google_apigee_instance.instances[each.value.instance].id
+ environment = try(google_apigee_environment.environments[each.value.environment].name,
+ "${local.org_id}/environments/${each.value.environment}")
+}
+
+resource "google_apigee_endpoint_attachment" "endpoint_attachments" {
+ for_each = local.endpoint_attachments
+ org_id = local.org_id
+ endpoint_attachment_id = each.key
+ location = each.value.region
+ service_attachment = each.value.service_attachment
+}
diff --git a/modules/apigee/outputs.tf b/modules/apigee/outputs.tf
new file mode 100644
index 0000000000..74ad9f18da
--- /dev/null
+++ b/modules/apigee/outputs.tf
@@ -0,0 +1,55 @@
+/**
+ * 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 "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)
+}
+
+output "environments" {
+ description = "Environment."
+ value = try(google_apigee_environment.environments, null)
+}
+
+output "instances" {
+ description = "Instances."
+ value = try(google_apigee_instance.instances, null)
+}
+
+output "org_id" {
+ description = "Organization ID."
+ value = local.org_id
+}
+
+output "org_name" {
+ description = "Organization name."
+ value = try(google_apigee_organization.organization[0].name, var.project_id)
+}
+
+output "organization" {
+ description = "Organization."
+ value = try(google_apigee_organization.organization[0], null)
+}
+
+output "service_attachments" {
+ description = "Service attachments."
+ value = { for k, v in google_apigee_instance.instances : k => v.service_attachment }
+}
diff --git a/modules/apigee/variables.tf b/modules/apigee/variables.tf
new file mode 100644
index 0000000000..266f0d34ed
--- /dev/null
+++ b/modules/apigee/variables.tf
@@ -0,0 +1,78 @@
+/**
+ * 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 "endpoint_attachments" {
+ description = "Endpoint attachments."
+ type = map(object({
+ region = string
+ service_attachment = string
+ }))
+ default = null
+}
+
+variable "envgroups" {
+ description = "Environment groups (NAME => [HOSTNAMES])."
+ type = map(list(string))
+ default = null
+}
+
+variable "environments" {
+ description = "Environments."
+ type = map(object({
+ display_name = optional(string)
+ description = optional(string, "Terraform-managed")
+ node_config = optional(object({
+ min_node_count = optional(number)
+ max_node_count = optional(number)
+ }))
+ iam = optional(map(list(string)))
+ envgroups = list(string)
+ }))
+ default = null
+}
+
+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))
+ }))
+ default = null
+}
+
+variable "organization" {
+ description = "Apigee organization. If set to null the organization must already exist."
+ type = object({
+ display_name = optional(string)
+ description = optional(string, "Terraform-managed")
+ authorized_network = optional(string)
+ runtime_type = optional(string, "CLOUD")
+ billing_type = optional(string)
+ database_encryption_key = optional(string)
+ analytics_region = optional(string, "europe-west1")
+ })
+ default = null
+}
+
+variable "project_id" {
+ description = "Project ID."
+ type = string
+}
diff --git a/modules/apigee/versions.tf b/modules/apigee/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/modules/apigee/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/modules/artifact-registry/README.md b/modules/artifact-registry/README.md
index ac463ec197..92f103a530 100644
--- a/modules/artifact-registry/README.md
+++ b/modules/artifact-registry/README.md
@@ -8,7 +8,7 @@ Note: Artifact Registry is still in beta, hence this module currently uses the b
```hcl
module "docker_artifact_registry" {
- source = "./modules/artifact-registry"
+ source = "./fabric/modules/artifact-registry"
project_id = "myproject"
location = "europe-west1"
format = "DOCKER"
diff --git a/modules/artifact-registry/versions.tf b/modules/artifact-registry/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/artifact-registry/versions.tf
+++ b/modules/artifact-registry/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/bigquery-dataset/README.md b/modules/bigquery-dataset/README.md
index 59a6924150..381ffab7ce 100644
--- a/modules/bigquery-dataset/README.md
+++ b/modules/bigquery-dataset/README.md
@@ -15,13 +15,13 @@ Access configuration defaults to using the separate `google_bigquery_dataset_acc
You can choose to manage the `google_bigquery_dataset` access rules instead via the `dataset_access` variable, but be sure to always have at least one `OWNER` access and to avoid duplicating accesses, or `terraform apply` will fail.
-The access variables are split into `access_roles` and `access_identities` variables, so that dynamic values can be passed in for identities (eg a service account email generated by a different module or resource). The `access_views` variable is separate, so as to allow proper type constraints.
+The access variables are split into `access` and `access_identities` variables, so that dynamic values can be passed in for identities (eg a service account email generated by a different module or resource).
```hcl
module "bigquery-dataset" {
- source = "./modules/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" }
@@ -44,9 +44,9 @@ Access configuration can also be specified via IAM instead of basic roles via th
```hcl
module "bigquery-dataset" {
- source = "./modules/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"]
}
@@ -54,15 +54,13 @@ module "bigquery-dataset" {
# tftest modules=1 resources=2
```
-roles/bigquery.dataOwner
-
### Dataset options
Dataset options are set via the `options` variable. all options must be specified, but a `null` value can be set to options that need to use defaults.
```hcl
module "bigquery-dataset" {
- source = "./modules/bigquery-dataset"
+ source = "./fabric/modules/bigquery-dataset"
project_id = "my-project"
id = "my-dataset"
options = {
@@ -87,7 +85,7 @@ locals {
}
module "bigquery-dataset" {
- source = "./modules/bigquery-dataset"
+ source = "./fabric/modules/bigquery-dataset"
project_id = "my-project"
id = "my_dataset"
tables = {
@@ -115,7 +113,7 @@ locals {
}
module "bigquery-dataset" {
- source = "./modules/bigquery-dataset"
+ source = "./fabric/modules/bigquery-dataset"
project_id = "my-project"
id = "my-dataset"
tables = {
@@ -147,7 +145,7 @@ locals {
}
module "bigquery-dataset" {
- source = "./modules/bigquery-dataset"
+ source = "./fabric/modules/bigquery-dataset"
project_id = "my-project"
id = "my_dataset"
tables = {
@@ -182,7 +180,7 @@ module "bigquery-dataset" {
| [id](variables.tf#L69) | Dataset id. | string
| ✓ | |
| [project_id](variables.tf#L100) | 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)
| | {}
|
+| [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
|
| [description](variables.tf#L45) | Optional description. | string
| | "Terraform managed."
|
| [encryption_key](variables.tf#L51) | Self link of the KMS key that will be used to protect destination table. | string
| | null
|
diff --git a/modules/bigquery-dataset/versions.tf b/modules/bigquery-dataset/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/bigquery-dataset/versions.tf
+++ b/modules/bigquery-dataset/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/bigtable-instance/README.md b/modules/bigtable-instance/README.md
index 2ff7f59537..39e0bc5364 100644
--- a/modules/bigtable-instance/README.md
+++ b/modules/bigtable-instance/README.md
@@ -4,52 +4,242 @@ This module allows managing a single BigTable instance, including access configu
## TODO
-- [ ] support bigtable_gc_policy
- [ ] support bigtable_app_profile
+- [ ] support IAM for tables
## Examples
-### Simple instance with access configuration
+### Instance with access configuration
```hcl
module "bigtable-instance" {
- source = "./modules/bigtable-instance"
- project_id = "my-project"
- name = "instance"
- cluster_id = "instance"
- zone = "europe-west1-b"
- tables = {
- test1 = null,
+ 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_family = null
+ split_keys = ["a", "b", "c"]
}
}
- iam = {
+ 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`.
+
+```hcl
+
+module "bigtable-instance" {
+ source = "./fabric/modules/bigtable-instance"
+ project_id = "my-project"
+ name = "instance"
+ clusters = {
+ my-cluster = {
+ zone = "europe-west1-b"
+ num_nodes = 5
+ }
+ }
+}
+# tftest modules=1 resources=1
+```
+
+### Instance with autoscaling (based on CPU only)
+
+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"
+ clusters = {
+ my-cluster = {
+ zone = "europe-southwest1-b"
+ autoscaling = {
+ min_nodes = 3
+ max_nodes = 7
+ cpu_target = 70
+ }
+ }
+ }
+
+
+}
+# tftest modules=1 resources=1
+```
+
+### Instance with autoscaling (based on CPU and/or storage)
+
+```hcl
+
+module "bigtable-instance" {
+ 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
+```
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [name](variables.tf#L45) | The name of the Cloud Bigtable instance. | string
| ✓ | |
-| [project_id](variables.tf#L56) | Id of the project where datasets will be created. | string
| ✓ | |
-| [zone](variables.tf#L88) | The zone to create the Cloud Bigtable cluster in. | string
| ✓ | |
-| [cluster_id](variables.tf#L17) | The ID of the Cloud Bigtable cluster. | string
| | "europe-west1"
|
-| [deletion_protection](variables.tf#L23) | 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#L28) | The human-readable display name of the Bigtable instance. |
| | null
|
-| [iam](variables.tf#L33) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
-| [instance_type](variables.tf#L39) | (deprecated) The instance type to create. One of 'DEVELOPMENT' or 'PRODUCTION'. | string
| | null
|
-| [num_nodes](variables.tf#L50) | The number of nodes in your Cloud Bigtable cluster. | number
| | 1
|
-| [storage_type](variables.tf#L61) | The storage type to use. | string
| | "SSD"
|
-| [table_options_defaults](variables.tf#L67) | Default option of tables created in the BigTable instance. | object({…})
| | {…}
|
-| [tables](variables.tf#L79) | 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 49423d948d..3a00eab890 100644
--- a/modules/bigtable-instance/main.tf
+++ b/modules/bigtable-instance/main.tf
@@ -15,23 +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
+ }
}
}
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
- }
- instance_type = var.instance_type
+ 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
+ }
+ }
+ }
+ }
}
resource "google_bigtable_instance_iam_binding" "default" {
@@ -43,21 +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
}
}
+}
- # lifecycle {
- # prevent_destroy = true
- # }
+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 d98cfab579..f7b75c1359 100644
--- a/modules/bigtable-instance/variables.tf
+++ b/modules/bigtable-instance/variables.tf
@@ -14,10 +14,43 @@
* limitations under the License.
*/
-variable "cluster_id" {
- description = "The ID of the Cloud Bigtable cluster."
- type = string
- default = "europe-west1"
+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)
+ })
+ default = null
+}
+
+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" {
@@ -47,45 +80,26 @@ variable "name" {
type = string
}
-variable "num_nodes" {
- description = "The number of nodes in your Cloud Bigtable cluster."
- 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 290412687a..90b632f6d4 100644
--- a/modules/bigtable-instance/versions.tf
+++ b/modules/bigtable-instance/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/billing-budget/README.md b/modules/billing-budget/README.md
index 0b39e19000..d995c0e0ac 100644
--- a/modules/billing-budget/README.md
+++ b/modules/billing-budget/README.md
@@ -15,7 +15,7 @@ Send a notification to an email when a set of projects reach $100 of spend.
```hcl
module "budget" {
- source = "./modules/billing-budget"
+ source = "./fabric/modules/billing-budget"
billing_account = var.billing_account_id
name = "$100 budget"
amount = 100
@@ -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
@@ -42,7 +42,7 @@ Send a notification to a PubSub topic the total spend of a billing account reach
```hcl
module "budget" {
- source = "./modules/billing-budget"
+ source = "./fabric/modules/billing-budget"
billing_account = var.billing_account_id
name = "previous period budget"
amount = 0
@@ -54,7 +54,7 @@ module "budget" {
}
module "pubsub" {
- source = "./modules/pubsub"
+ source = "./fabric/modules/pubsub"
project_id = var.project_id
name = "budget-topic"
}
diff --git a/modules/billing-budget/versions.tf b/modules/billing-budget/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/billing-budget/versions.tf
+++ b/modules/billing-budget/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/binauthz/README.md b/modules/binauthz/README.md
new file mode 100644
index 0000000000..960a7da31d
--- /dev/null
+++ b/modules/binauthz/README.md
@@ -0,0 +1,79 @@
+# Google Cloud Artifact Registry Module
+
+This module simplifies the creation of a Binary Authorization policy, attestors and attestor IAM bindings.
+
+## Example
+
+### Binary Athorization
+
+```hcl
+module "binauthz" {
+ source = "./fabric/modules/binauthz"
+ project_id = "my_project"
+ global_policy_evaluation_mode = "DISABLE"
+ default_admission_rule = {
+ evaluation_mode = "ALWAYS_DENY"
+ enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
+ attestors = null
+ }
+ cluster_admission_rules = {
+ "europe-west1-c.cluster" = {
+ evaluation_mode = "REQUIRE_ATTESTATION"
+ enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
+ attestors = ["test"]
+ }
+ }
+ attestors_config = {
+ "test" : {
+ note_reference = null
+ pgp_public_keys = [
+ <string
| ✓ | |
+| [admission_whitelist_patterns](variables.tf#L17) | An image name pattern to allowlist. | list(string)
| | null
|
+| [attestors_config](variables.tf#L23) | Attestors configuration. | map(object({…}))
| | null
|
+| [cluster_admission_rules](variables.tf#L38) | Admission rules. | map(object({…}))
| | null
|
+| [default_admission_rule](variables.tf#L48) | Default admission rule. | object({…})
| | {…}
|
+| [global_policy_evaluation_mode](variables.tf#L62) | Global policy evaluation mode. | string
| | null
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [attestors](outputs.tf#L17) | Attestors. | |
+| [id](outputs.tf#L25) | Binary Authorization policy ID. | |
+| [notes](outputs.tf#L30) | Notes. | |
+
+
diff --git a/modules/binauthz/main.tf b/modules/binauthz/main.tf
new file mode 100644
index 0000000000..2c1af463ac
--- /dev/null
+++ b/modules/binauthz/main.tf
@@ -0,0 +1,91 @@
+/**
+ * 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_binary_authorization_policy" "policy" {
+ project = var.project_id
+ dynamic "admission_whitelist_patterns" {
+ for_each = toset(coalesce(var.admission_whitelist_patterns, []))
+ content {
+ name_pattern = admission_whitelist_patterns.value
+ }
+ }
+ default_admission_rule {
+ evaluation_mode = var.default_admission_rule.evaluation_mode
+ enforcement_mode = var.default_admission_rule.enforcement_mode
+ require_attestations_by = [for attestor in coalesce(var.default_admission_rule.attestors, []) : google_binary_authorization_attestor.attestors[attestor].name]
+ }
+ dynamic "cluster_admission_rules" {
+ for_each = coalesce(var.cluster_admission_rules, {})
+ content {
+ cluster = cluster_admission_rules.key
+ evaluation_mode = cluster_admission_rules.value.evaluation_mode
+ enforcement_mode = cluster_admission_rules.value.enforcement_mode
+ require_attestations_by = [for attestor in cluster_admission_rules.value.attestors : google_binary_authorization_attestor.attestors[attestor].name]
+ }
+ }
+}
+
+resource "google_binary_authorization_attestor" "attestors" {
+ for_each = coalesce(var.attestors_config, {})
+ name = each.key
+ project = var.project_id
+ attestation_authority_note {
+ note_reference = each.value.note_reference == null ? google_container_analysis_note.notes[each.key].name : each.value.note_reference
+ dynamic "public_keys" {
+ for_each = coalesce(each.value.pgp_public_keys, [])
+ content {
+ ascii_armored_pgp_public_key = public_keys.value
+ }
+ }
+ dynamic "public_keys" {
+ for_each = {
+ for pkix_public_key in coalesce(each.value.pkix_public_keys, []) :
+ "${pkix_public_key.public_key_pem}-${pkix_public_key.signature_algorithm}" => pkix_public_key
+ }
+ content {
+ id = public_keys.value.id
+ pkix_public_key {
+ public_key_pem = public_keys.value.public_key_pem
+ signature_algorithm = public_keys.value.signature_algorithm
+ }
+ }
+ }
+ }
+}
+
+resource "google_binary_authorization_attestor_iam_binding" "bindings" {
+ for_each = merge(flatten([
+ for name, attestor_config in var.attestors_config : { for role, members in coalesce(attestor_config.iam, {}) : "${name}-${role}" => {
+ name = name
+ role = role
+ members = members
+ } }])...)
+ project = google_binary_authorization_attestor.attestors[each.value.name].project
+ attestor = google_binary_authorization_attestor.attestors[each.value.name].name
+ role = each.value.role
+ members = each.value.members
+}
+
+resource "google_container_analysis_note" "notes" {
+ for_each = toset([for name, attestor_config in var.attestors_config : name if attestor_config.note_reference == null])
+ name = "${each.value}-note"
+ project = var.project_id
+ attestation_authority {
+ hint {
+ human_readable_name = "Attestor ${each.value} note"
+ }
+ }
+}
diff --git a/modules/binauthz/outputs.tf b/modules/binauthz/outputs.tf
new file mode 100644
index 0000000000..6a1d7c6de3
--- /dev/null
+++ b/modules/binauthz/outputs.tf
@@ -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.
+ */
+
+output "attestors" {
+ description = "Attestors."
+ value = google_binary_authorization_attestor.attestors
+ depends_on = [
+ google_binary_authorization_attestor_iam_binding.bindings
+ ]
+}
+
+output "id" {
+ description = "Binary Authorization policy ID."
+ value = google_binary_authorization_policy.policy.id
+}
+
+output "notes" {
+ description = "Notes."
+ value = google_container_analysis_note.notes
+}
diff --git a/modules/binauthz/variables.tf b/modules/binauthz/variables.tf
new file mode 100644
index 0000000000..6d21083be8
--- /dev/null
+++ b/modules/binauthz/variables.tf
@@ -0,0 +1,71 @@
+/**
+ * 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 "admission_whitelist_patterns" {
+ description = "An image name pattern to allowlist."
+ type = list(string)
+ default = null
+}
+
+variable "attestors_config" {
+ description = "Attestors configuration."
+ type = map(object({
+ note_reference = string
+ iam = map(list(string))
+ pgp_public_keys = list(string)
+ pkix_public_keys = list(object({
+ id = string
+ public_key_pem = string
+ signature_algorithm = string
+ }))
+ }))
+ default = null
+}
+
+variable "cluster_admission_rules" {
+ description = "Admission rules."
+ type = map(object({
+ evaluation_mode = string
+ enforcement_mode = string
+ attestors = list(string)
+ }))
+ default = null
+}
+
+variable "default_admission_rule" {
+ description = "Default admission rule."
+ type = object({
+ evaluation_mode = string
+ enforcement_mode = string
+ attestors = list(string)
+ })
+ default = {
+ evaluation_mode = "ALWAYS_ALLOW"
+ enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
+ attestors = null
+ }
+}
+
+variable "global_policy_evaluation_mode" {
+ description = "Global policy evaluation mode."
+ type = string
+ default = null
+}
+
+variable "project_id" {
+ description = "Project ID."
+ type = string
+}
diff --git a/modules/binauthz/versions.tf b/modules/binauthz/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/modules/binauthz/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/modules/cloud-config-container/README.md b/modules/cloud-config-container/README.md
index 2d227ad469..713ffa8831 100644
--- a/modules/cloud-config-container/README.md
+++ b/modules/cloud-config-container/README.md
@@ -1,6 +1,6 @@
# Instance Configuration via `cloud-config`
-This set of modules creates specialized [cloud-config](https://cloud.google.com/container-optimized-os/docs/how-to/run-container-instance#starting_a_docker_container_via_cloud-config) configurations, which are designed for use with [Container Optimized OS](https://cloud.google.com/container-optimized-os/docs) (the [onprem module](./onprem/) is the only exception) but can also be used as a basis for other image types or cloud providers.
+This set of modules creates specialized [cloud-config](https://cloud.google.com/container-optimized-os/docs/how-to/run-container-instance#starting_a_docker_container_via_cloud-config) configurations, which are designed for use with [Container Optimized OS](https://cloud.google.com/container-optimized-os/docs) (the onprem module is the only exception) but can also be used as a basis for other image types or cloud providers.
These modules are designed for several use cases:
@@ -14,19 +14,15 @@ These modules are designed for several use cases:
- [CoreDNS](./coredns)
- [MySQL](./mysql)
- [Nginx](./nginx)
-- [On-prem in Docker](./onprem)
- [Squid forward proxy](./squid)
+- On-prem in Docker (*needs fixing*)
## Using the modules
All modules are designed to be as lightweight as possible, so that specialized modules like [compute-vm](../compute-vm) can be leveraged to manage instances or instance templates, and to allow simple forking to create custom derivatives.
-Modules use Docker's [Google Cloud Logging driver](https://docs.docker.com/config/containers/logging/gcplogs/) by default, so projects need to have the logging API enabled. If that's not desirable simply remove `--log-driver=gcplogs` from the relevant systemd unit in `cloud-config.yaml`.
-
To use the modules with instances or instance templates, simply set use their `cloud_config` output for the `user-data` metadata. When updating the metadata after a variable change remember to manually restart the instances that use a module's output, or the changes won't effect the running system.
-For convenience when developing or prototyping infrastructure, an optional test instance is included in all modules. If it's not needed, the linked `*instance.tf` files can be removed from the modules without harm.
-
## TODO
- [ ] convert all `xxx_config` variables to use file content instead of path
diff --git a/modules/cloud-config-container/onprem/Corefile b/modules/cloud-config-container/__need_fixing/onprem/Corefile
similarity index 100%
rename from modules/cloud-config-container/onprem/Corefile
rename to modules/cloud-config-container/__need_fixing/onprem/Corefile
diff --git a/modules/cloud-config-container/__need_fixing/onprem/README.md b/modules/cloud-config-container/__need_fixing/onprem/README.md
new file mode 100644
index 0000000000..bc81f437e4
--- /dev/null
+++ b/modules/cloud-config-container/__need_fixing/onprem/README.md
@@ -0,0 +1,88 @@
+# Containerized on-premises infrastructure
+
+This module manages a `cloud-config` configuration that starts an emulated on-premises infrastructure running in Docker Compose on a single instance, and connects it via static or dynamic VPN to a Google Cloud VPN gateway.
+
+The emulated on-premises infrastructure is composed of:
+
+- a [Strongswan container](./docker-images/strongswan) managing the VPN tunnel to GCP
+- an optional Bird container managing the BGP session
+- a CoreDNS container servng local DNS and forwarding to GCP
+- an Nginx container serving a simple static web page
+- a [generic Linux container](./docker-images/toolbox) used as a jump host inside the on-premises network
+
+A complete scenario using this module is available in the networking blueprints.
+
+The module renders the generated cloud config in the `cloud_config` output, to be used in instances or instance templates via the `user-data` metadata.
+
+## Examples
+
+### Static VPN
+
+```hcl
+module "cloud-vpn" {
+ source = "./fabric/modules/net-vpn-static"
+ project_id = "my-project"
+ region = "europe-west1"
+ network = "my-vpc"
+ name = "to-on-prem"
+ remote_ranges = ["192.168.192.0/24"]
+ tunnels = {
+ remote-0 = {
+ peer_ip = module.vm.external_ip
+ traffic_selectors = { local = ["0.0.0.0/0"], remote = null }
+ }
+ }
+}
+
+module "on-prem" {
+ source = "./fabric/modules/cloud-config-container/onprem"
+ vpn_config = {
+ type = "static"
+ peer_ip = module.cloud-vpn.address
+ shared_secret = module.cloud-vpn.random_secret
+ }
+}
+
+module "vm" {
+ source = "./fabric/modules/compute-vm"
+ project_id = "my-project"
+ zone = "europe-west8-b"
+ name = "cos-nginx-tls"
+ network_interfaces = [{
+ nat = true
+ network = "default"
+ subnetwork = "gce"
+ }]
+ metadata = {
+ user-data = module.on-prem.cloud_config
+ google-logging-enabled = true
+ }
+ boot_disk = {
+ image = "projects/cos-cloud/global/images/family/cos-stable"
+ type = "pd-ssd"
+ size = 10
+ }
+ tags = ["ssh"]
+}
+# tftest skip
+```
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [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"
|
+| [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"]
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [cloud_config](outputs.tf#L17) | Rendered cloud-config file to be passed as user-data instance metadata. | |
+
+
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/__need_fixing/onprem/docker-images/strongswan/Dockerfile b/modules/cloud-config-container/__need_fixing/onprem/docker-images/strongswan/Dockerfile
new file mode 100644
index 0000000000..8bb6165bac
--- /dev/null
+++ b/modules/cloud-config-container/__need_fixing/onprem/docker-images/strongswan/Dockerfile
@@ -0,0 +1,37 @@
+# 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.
+
+FROM debian:bullseye-slim
+
+ENV STRONGSWAN_VERSION=5.9
+
+RUN apt-get update \
+ && DEBIAN_FRONTEND=noninteractive apt-get install -y sudo iptables procps strongswan=${STRONGSWAN_VERSION}* \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY entrypoint.sh /entrypoint.sh
+RUN chmod 0755 /entrypoint.sh
+
+COPY ipsec-vti.sh /var/lib/strongswan/ipsec-vti.sh
+RUN chmod 0755 /var/lib/strongswan/ipsec-vti.sh
+
+RUN echo 'ipsec ALL=NOPASSWD:SETENV:/usr/sbin/ipsec,/sbin/ip,/sbin/sysctl' > /etc/sudoers.d/ipsec
+RUN chmod 0440 /etc/sudoers.d/ipsec
+
+ENV VPN_DEVICE=eth0
+ENV LAN_NETWORKS=192.168.0.0/24
+
+EXPOSE 500/udp 4500/udp
+
+ENTRYPOINT ["/entrypoint.sh"]
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 97%
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
index e99d1ec828..bf596bc0f8 100644
--- a/modules/cloud-config-container/onprem/docker-images/strongswan/entrypoint.sh
+++ b/modules/cloud-config-container/__need_fixing/onprem/docker-images/strongswan/entrypoint.sh
@@ -22,7 +22,7 @@ _stop_ipsec() {
echo "Shutting down strongSwan/ipsec..."
ipsec stop
}
-trap _stop_ipsec SIGTERM
+trap _stop_ipsec TERM
# Making the containter to work as a default gateway for LAN_NETWORKS
iptables -t nat -A POSTROUTING -s ${LAN_NETWORKS} -o ${VPN_DEVICE} -m policy --dir out --pol ipsec -j ACCEPT
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/__need_fixing/onprem/variables.tf b/modules/cloud-config-container/__need_fixing/onprem/variables.tf
new file mode 100644
index 0000000000..06eb27603e
--- /dev/null
+++ b/modules/cloud-config-container/__need_fixing/onprem/variables.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
+ *
+ * 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.
+ */
+
+variable "config_variables" {
+ description = "Additional variables used to render the cloud-config and CoreDNS templates."
+ type = map(any)
+ default = {}
+}
+
+variable "coredns_config" {
+ description = "CoreDNS configuration path, if null default will be used."
+ type = string
+ default = null
+}
+
+variable "local_ip_cidr_range" {
+ description = "IP CIDR range used for the Docker onprem network."
+ type = string
+ default = "192.168.192.0/24"
+}
+
+variable "vpn_config" {
+ description = "VPN configuration, type must be one of 'dynamic' or 'static'."
+ type = object({
+ peer_ip = string
+ shared_secret = string
+ type = optional(string, "static")
+ peer_ip2 = optional(string)
+ shared_secret2 = optional(string)
+ })
+}
+
+variable "vpn_dynamic_config" {
+ description = "BGP configuration for dynamic VPN, ignored if VPN type is 'static'."
+ type = object({
+ local_bgp_asn = number
+ local_bgp_address = string
+ peer_bgp_asn = number
+ peer_bgp_address = string
+ local_bgp_asn2 = number
+ local_bgp_address2 = string
+ peer_bgp_asn2 = number
+ peer_bgp_address2 = string
+ })
+ default = {
+ local_bgp_asn = 64514
+ local_bgp_address = "169.254.1.2"
+ peer_bgp_asn = 64513
+ peer_bgp_address = "169.254.1.1"
+ local_bgp_asn2 = 64514
+ local_bgp_address2 = "169.254.2.2"
+ peer_bgp_asn2 = 64520
+ peer_bgp_address2 = "169.254.2.1"
+ }
+}
+
+variable "vpn_static_ranges" {
+ description = "Remote CIDR ranges for static VPN, ignored if VPN type is 'dynamic'."
+ type = list(string)
+ default = ["10.0.0.0/8"]
+}
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 0000000000..90b632f6d4
--- /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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/modules/cloud-config-container/coredns/README.md b/modules/cloud-config-container/coredns/README.md
index 09cc44916a..4abe69e323 100644
--- a/modules/cloud-config-container/coredns/README.md
+++ b/modules/cloud-config-container/coredns/README.md
@@ -10,7 +10,7 @@ The resulting `cloud-config` can be customized in a number of ways:
The default instance configuration inserts iptables rules to allow traffic on the DNS TCP and UDP ports, and the 8080 port for the optional HTTP health check that can be enabled via the CoreDNS [health plugin](https://coredns.io/plugins/health/).
-Logging and monitoring are enabled via the [Google Cloud Logging driver](https://docs.docker.com/config/containers/logging/gcplogs/) configured for the CoreDNS container, and the [Node Problem Detector](https://cloud.google.com/container-optimized-os/docs/how-to/monitoring) service is started by default on boot.
+Logging and monitoring are enabled via the [Google Cloud Logging agent](https://cloud.google.com/container-optimized-os/docs/how-to/logging) configured for the instance via the `google-logging-enabled` metadata property, and the [Node Problem Detector](https://cloud.google.com/container-optimized-os/docs/how-to/monitoring) service is started by default on boot.
The module renders the generated cloud config in the `cloud_config` output, to be used in instances or instance templates via the `user-data` metadata.
@@ -24,14 +24,30 @@ This example will create a `cloud-config` that uses the module's defaults, creat
```hcl
module "cos-coredns" {
- source = "./modules/cloud-config-container/coredns"
+ source = "./fabric/modules/cloud-config-container/coredns"
}
-# use it as metadata in a compute instance or template
-resource "google_compute_instance" "default" {
+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
+ 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
@@ -40,33 +56,17 @@ This example will create a `cloud-config` using a custom CoreDNS configuration,
```hcl
module "cos-coredns" {
- source = "./modules/cloud-config-container/coredns"
- coredns_config = "./modules/cloud-config-container/coredns/Corefile-hosts"
+ source = "./fabric/modules/cloud-config-container/coredns"
+ coredns_config = "./fabric/modules/cloud-config-container/coredns/Corefile-hosts"
files = {
"/etc/coredns/example.hosts" = {
content = "127.0.0.2 foo.example.org foo"
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 = "./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
```
@@ -79,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/cloud-config.yaml b/modules/cloud-config-container/coredns/cloud-config.yaml
index 0796fc6b95..9fe929e9fd 100644
--- a/modules/cloud-config-container/coredns/cloud-config.yaml
+++ b/modules/cloud-config-container/coredns/cloud-config.yaml
@@ -17,8 +17,6 @@
# https://hub.docker.com/r/coredns/coredns/
# https://coredns.io/manual/toc/#installation
-# TODO: switch to the gcplogs logging driver, and set driver labels
-
write_files:
- path: /var/lib/docker/daemon.json
permissions: 0644
@@ -58,7 +56,7 @@ write_files:
Wants=gcr-online.target docker.socket docker-events-collector.service
[Service]
ExecStart=/usr/bin/docker run --rm --name=coredns \
- --log-driver=gcplogs --network host \
+ --network host \
-v /etc/coredns:/etc/coredns \
coredns/coredns -conf /etc/coredns/Corefile
ExecStop=/usr/bin/docker stop coredns
@@ -80,4 +78,4 @@ runcmd:
- iptables -I INPUT 1 -p udp -m udp --dport 53 -m state --state NEW,ESTABLISHED -j ACCEPT
- systemctl daemon-reload
- systemctl restart systemd-resolved.service
- - systemctl start coredns
\ No newline at end of file
+ - systemctl start coredns
diff --git a/modules/cloud-config-container/coredns/instance.tf b/modules/cloud-config-container/coredns/instance.tf
deleted file mode 120000
index bdef596b6d..0000000000
--- 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 ea9e240458..0000000000
--- 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 94af61e4dd..0000000000
--- 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 290412687a..90b632f6d4 100644
--- a/modules/cloud-config-container/coredns/versions.tf
+++ b/modules/cloud-config-container/coredns/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.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 5a01b43ec7..88073986d4 100644
--- a/modules/cloud-config-container/cos-generic-metadata/README.md
+++ b/modules/cloud-config-container/cos-generic-metadata/README.md
@@ -2,8 +2,6 @@
This helper module manages a `cloud-config` configuration that can start a container on [Container Optimized OS](https://cloud.google.com/container-optimized-os/docs) (COS). Either a complete `cloud-config` template can be provided via the `cloud_config` variable with optional template variables via the `config_variables`, or a generic `cloud-config` can be generated based on typical parameters needed to start a container.
-Logging can be enabled via the [Google Cloud Logging docker driver](https://docs.docker.com/config/containers/logging/gcplogs/) using the `gcp_logging` variable. This is enabled by default, but requires that the service account running the COS instance have the `roles/logging.logWriter` IAM role or equivalent permissions on the project. If it doesn't, the container will fail to start unless this is disabled.
-
The module renders the generated cloud config in the `cloud_config` output, which can be directly used in instances or instance templates via the `user-data` metadata attribute.
## Examples
@@ -14,31 +12,27 @@ This example will create a `cloud-config` that starts [Envoy Proxy](https://www.
```hcl
module "cos-envoy" {
- source = "./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",
@@ -48,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
```
@@ -63,20 +56,20 @@ module "cos-envoy" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [container_image](variables.tf#L42) | Container image. | string
| ✓ | |
-| [authenticate_gcr](variables.tf#L112) | Setup docker to pull images from private GCR. Requires at least one user since the token is stored in the home of the first user defined. | bool
| | false
|
-| [boot_commands](variables.tf#L17) | List of cloud-init `bootcmd`s. | list(string)
| | []
|
-| [cloud_config](variables.tf#L23) | Cloud config template path. If provided, takes precedence over all other arguments. | string
| | null
|
-| [config_variables](variables.tf#L29) | Additional variables used to render the template passed via `cloud_config`. | map(any)
| | {}
|
-| [container_args](variables.tf#L35) | Arguments for container. | string
| | ""
|
-| [container_name](variables.tf#L47) | Name of the container to be run. | string
| | "container"
|
-| [container_volumes](variables.tf#L53) | List of volumes. | list(object({…}))
| | []
|
-| [docker_args](variables.tf#L62) | Extra arguments to be passed for docker. | string
| | null
|
-| [file_defaults](variables.tf#L68) | Default owner and permissions for files. | object({…})
| | {…}
|
-| [files](variables.tf#L80) | Map of extra files to create on the instance, path as key. Owner and permissions will use defaults if null. | map(object({…}))
| | {}
|
-| [gcp_logging](variables.tf#L90) | Should container logs be sent to Google Cloud Logging. | bool
| | true
|
-| [run_commands](variables.tf#L96) | List of cloud-init `runcmd`s. | list(string)
| | []
|
-| [users](variables.tf#L102) | List of usernames to be created. If provided, first user will be used to run the container. | list(object({…}))
| | […]
|
+| [container_image](variables.tf#L47) | Container image. | string
| ✓ | |
+| [authenticate_gcr](variables.tf#L17) | Setup docker to pull images from private GCR. Requires at least one user since the token is stored in the home of the first user defined. | bool
| | false
|
+| [boot_commands](variables.tf#L23) | List of cloud-init `bootcmd`s. | list(string)
| | []
|
+| [cloud_config](variables.tf#L29) | Cloud config template path. If provided, takes precedence over all other arguments. | string
| | null
|
+| [config_variables](variables.tf#L35) | Additional variables used to render the template passed via `cloud_config`. | map(any)
| | {}
|
+| [container_args](variables.tf#L41) | Arguments for container. | string
| | ""
|
+| [container_name](variables.tf#L52) | Name of the container to be run. | string
| | "container"
|
+| [container_volumes](variables.tf#L58) | List of volumes. | list(object({…}))
| | []
|
+| [docker_args](variables.tf#L67) | Extra arguments to be passed for docker. | string
| | null
|
+| [file_defaults](variables.tf#L73) | Default owner and permissions for files. | object({…})
| | {…}
|
+| [files](variables.tf#L85) | Map of extra files to create on the instance, path as key. Owner and permissions will use defaults if null. | map(object({…}))
| | {}
|
+| [run_as_first_user](variables.tf#L95) | Run as the first user if users are specified. | bool
| | true
|
+| [run_commands](variables.tf#L101) | List of cloud-init `runcmd`s. | list(string)
| | []
|
+| [users](variables.tf#L107) | List of usernames to be created. If provided, first user will be used to run the container. | list(object({…}))
| | […]
|
## Outputs
diff --git a/modules/cloud-config-container/cos-generic-metadata/cloud-config.yaml b/modules/cloud-config-container/cos-generic-metadata/cloud-config.yaml
index fc75616a51..a8d1f22973 100644
--- a/modules/cloud-config-container/cos-generic-metadata/cloud-config.yaml
+++ b/modules/cloud-config-container/cos-generic-metadata/cloud-config.yaml
@@ -44,23 +44,20 @@ write_files:
After=gcr-online.target docker.socket
Wants=gcr-online.target docker.socket docker-events-collector.service
[Service]
-%{ if authenticate_gcr && length(users) > 0 ~}
+ %{~ if authenticate_gcr && length(users) > 0 ~}
Environment="HOME=/home/${users[0].username}"
ExecStartPre=/usr/bin/docker-credential-gcr configure-docker
-%{ endif ~}
- ExecStart=/usr/bin/docker run --rm --name=${container_name} \
-%{ if length(users) > 0 ~}
+ %{~ endif ~}
+ ExecStart=/usr/bin/docker run --rm --name=${container_name} \
+ %{~ if length(users) > 0 && run_as_first_user ~}
--user=${users[0].uid} \
-%{ endif ~}
-%{ if gcp_logging == true ~}
- --log-driver=gcplogs \
-%{ endif ~}
-%{ if docker_args != null ~}
+ %{~ endif ~}
+ %{~ if docker_args != null ~}
${docker_args} \
-%{ endif ~}
-%{ for volume in container_volumes ~}
+ %{~ endif ~}
+ %{~ for volume in container_volumes ~}
-v ${volume.host}:${volume.container} \
-%{ endfor ~}
+ %{~ endfor ~}
${container_image} ${container_args}
ExecStop=/usr/bin/docker stop ${container_name}
%{ for path, data in files ~}
diff --git a/modules/cloud-config-container/cos-generic-metadata/main.tf b/modules/cloud-config-container/cos-generic-metadata/main.tf
index 5019fa0977..eb807c5ae4 100644
--- a/modules/cloud-config-container/cos-generic-metadata/main.tf
+++ b/modules/cloud-config-container/cos-generic-metadata/main.tf
@@ -23,10 +23,10 @@ locals {
container_volumes = var.container_volumes
docker_args = var.docker_args
files = local.files
- gcp_logging = var.gcp_logging
run_commands = var.run_commands
users = var.users
authenticate_gcr = var.authenticate_gcr
+ run_as_first_user = var.run_as_first_user
}))
files = {
for path, attrs in var.files : path => {
diff --git a/modules/cloud-config-container/cos-generic-metadata/variables.tf b/modules/cloud-config-container/cos-generic-metadata/variables.tf
index e9aa051a04..0225916492 100644
--- a/modules/cloud-config-container/cos-generic-metadata/variables.tf
+++ b/modules/cloud-config-container/cos-generic-metadata/variables.tf
@@ -14,6 +14,12 @@
* limitations under the License.
*/
+variable "authenticate_gcr" {
+ description = "Setup docker to pull images from private GCR. Requires at least one user since the token is stored in the home of the first user defined."
+ type = bool
+ default = false
+}
+
variable "boot_commands" {
description = "List of cloud-init `bootcmd`s."
type = list(string)
@@ -38,7 +44,6 @@ variable "container_args" {
default = ""
}
-
variable "container_image" {
description = "Container image."
type = string
@@ -87,8 +92,8 @@ variable "files" {
default = {}
}
-variable "gcp_logging" {
- description = "Should container logs be sent to Google Cloud Logging."
+variable "run_as_first_user" {
+ description = "Run as the first user if users are specified."
type = bool
default = true
}
@@ -108,9 +113,3 @@ variable "users" {
default = [
]
}
-
-variable "authenticate_gcr" {
- description = "Setup docker to pull images from private GCR. Requires at least one user since the token is stored in the home of the first user defined."
- type = bool
- default = false
-}
diff --git a/modules/cloud-config-container/cos-generic-metadata/versions.tf b/modules/cloud-config-container/cos-generic-metadata/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/cloud-config-container/cos-generic-metadata/versions.tf
+++ b/modules/cloud-config-container/cos-generic-metadata/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.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 4493e9ba71..caa0ec5ec2 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 = "./modules/cloud-config-container/envoy-traffic-director"
+ source = "./fabric/modules/cloud-config-container/envoy-traffic-director"
}
-# COS VM
-module "vm-cos" {
- source = "./modules/compute-vm"
- project_id = local.project_id
- region = local.region
+module "vm" {
+ source = "./fabric/modules/compute-vm"
+ 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"
}]
- instance_count = 1
- tags = ["ssh", "http"]
-
metadata = {
- user-data = module.cos-envoy-td.cloud_config
+ 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
```
@@ -50,8 +43,7 @@ module "vm-cos" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [envoy_image](variables.tf#L17) | Envoy Proxy container image to use. | string
| | "envoyproxy/envoy:v1.14.1"
|
-| [gcp_logging](variables.tf#L23) | Should container logs be sent to Google Cloud Logging. | bool
| | true
|
+| [envoy_image](variables.tf#L17) | Envoy Proxy container image to use. | string
| | "envoyproxy/envoy:v1.15.5"
|
## Outputs
diff --git a/modules/cloud-config-container/envoy-traffic-director/files/customize.sh b/modules/cloud-config-container/envoy-traffic-director/files/customize.sh
index 85c8746ea0..eb9ae82d51 100644
--- a/modules/cloud-config-container/envoy-traffic-director/files/customize.sh
+++ b/modules/cloud-config-container/envoy-traffic-director/files/customize.sh
@@ -13,11 +13,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-ENVOY_NODE_ID=$(uuidgen)~$(curl -s -H "Metadata-Flavor: Google" http://metadata/computeMetadata/v1/instance/network-interfaces/0/ip)
+ENVOY_NODE_ID=$(uuidgen)
ENVOY_ZONE=$(curl -s -H "Metadata-Flavor: Google" http://metadata/computeMetadata/v1/instance/zone | cut -f 4 -d '/')
CONFIG_PROJECT_NUMBER=$(curl -s -H "Metadata-Flavor: Google" http://metadata/computeMetadata/v1/instance/network-interfaces/0/network | cut -f 2 -d '/')
VPC_NETWORK_NAME=$(curl -s -H "Metadata-Flavor: Google" http://metadata/computeMetadata/v1/instance/network-interfaces/0/network | cut -f 4 -d '/')
-sed -i "s/_ENVOY_NODE_ID_/${ENVOY_NODE_ID}/" /etc/envoy/envoy.yaml
-sed -i "s/_ENVOY_ZONE_/${ENVOY_ZONE}/" /etc/envoy/envoy.yaml
-sed -i "s/_CONFIG_PROJECT_NUMBER_/${CONFIG_PROJECT_NUMBER}/" /etc/envoy/envoy.yaml
-sed -i "s/_VPC_NETWORK_NAME_/${VPC_NETWORK_NAME}/" /etc/envoy/envoy.yaml
+sed -i "s/ENVOY_NODE_ID/${ENVOY_NODE_ID}/" /etc/envoy/envoy.yaml
+sed -i "s/ENVOY_ZONE/${ENVOY_ZONE}/" /etc/envoy/envoy.yaml
+sed -i "s/CONFIG_PROJECT_NUMBER/${CONFIG_PROJECT_NUMBER}/" /etc/envoy/envoy.yaml
+sed -i "s/VPC_NETWORK_NAME/${VPC_NETWORK_NAME}/" /etc/envoy/envoy.yaml
diff --git a/modules/cloud-config-container/envoy-traffic-director/files/envoy.yaml b/modules/cloud-config-container/envoy-traffic-director/files/envoy.yaml
index 2be4ef3c52..d9a14623db 100644
--- a/modules/cloud-config-container/envoy-traffic-director/files/envoy.yaml
+++ b/modules/cloud-config-container/envoy-traffic-director/files/envoy.yaml
@@ -13,47 +13,68 @@
# limitations under the License.
node:
- id: "_ENVOY_NODE_ID_"
+ # The id must be in the following format: projects/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/cloud-config.yaml b/modules/cloud-config-container/mysql/cloud-config.yaml
index 1c792a744f..07706ae2ea 100644
--- a/modules/cloud-config-container/mysql/cloud-config.yaml
+++ b/modules/cloud-config-container/mysql/cloud-config.yaml
@@ -96,7 +96,6 @@ write_files:
ExecStartPre=/bin/chown -R 2000 /run/mysql/secrets /run/mysql/data
ExecStart=/usr/bin/docker run --rm --name=mysql \
--user 2000:2000 \
- --log-driver=gcplogs \
--network host \
-e MYSQL_ROOT_PASSWORD_FILE=/etc/secrets/mysql-passwd.txt \
-v /run/mysql/secrets:/etc/secrets \
@@ -114,4 +113,4 @@ bootcmd:
runcmd:
- iptables -I INPUT 1 -p tcp -m tcp --dport 3306 -m state --state NEW,ESTABLISHED -j ACCEPT
- systemctl daemon-reload
- - systemctl start mysql
\ No newline at end of file
+ - systemctl start mysql
diff --git a/modules/cloud-config-container/mysql/instance.tf b/modules/cloud-config-container/mysql/instance.tf
deleted file mode 120000
index bdef596b6d..0000000000
--- 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 ea9e240458..0000000000
--- 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 94af61e4dd..0000000000
--- 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 290412687a..90b632f6d4 100644
--- a/modules/cloud-config-container/mysql/versions.tf
+++ b/modules/cloud-config-container/mysql/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/cloud-config-container/nginx-tls/README.md b/modules/cloud-config-container/nginx-tls/README.md
new file mode 100644
index 0000000000..9dfe9b0610
--- /dev/null
+++ b/modules/cloud-config-container/nginx-tls/README.md
@@ -0,0 +1,52 @@
+# 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.
+
+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.
+
+## Example
+
+```hcl
+module "cos-nginx-tls" {
+ source = "./fabric/modules/cloud-config-container/nginx-tls"
+}
+
+module "vm-nginx-tls" {
+ source = "./fabric/modules/compute-vm"
+ project_id = "my-project"
+ zone = "europe-west8-b"
+ name = "cos-nginx-tls"
+ network_interfaces = [{
+ 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
+ }
+ tags = ["http-server", "https-server", "ssh"]
+}
+# tftest modules=1 resources=1
+```
+
+
+## Variables
+
+| 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({…}))
| | {}
|
+| [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
+
+| name | description | sensitive |
+|---|---|:---:|
+| [cloud_config](outputs.tf#L17) | Rendered cloud-config file to be passed as user-data instance metadata. | |
+
+
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 0000000000..2b7ebe8475
--- /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/assets/customize.sh b/modules/cloud-config-container/nginx-tls/assets/customize.sh
new file mode 100644
index 0000000000..22b40064bd
--- /dev/null
+++ b/modules/cloud-config-container/nginx-tls/assets/customize.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+# 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.
+
+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
+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 0000000000..2be98ff278
--- /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/outputs.tf b/modules/cloud-config-container/nginx-tls/outputs.tf
new file mode 100644
index 0000000000..2acd83f642
--- /dev/null
+++ b/modules/cloud-config-container/nginx-tls/outputs.tf
@@ -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.
+ */
+
+output "cloud_config" {
+ description = "Rendered cloud-config file to be passed as user-data instance metadata."
+ 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
new file mode 100644
index 0000000000..f3cab5826c
--- /dev/null
+++ b/modules/cloud-config-container/nginx-tls/variables.tf
@@ -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.
+ */
+
+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 = optional(string, "root")
+ permissions = optional(string, "0644")
+ }))
+ default = {}
+ nullable = false
+}
+
+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"
+}
diff --git a/modules/cloud-config-container/nginx-tls/versions.tf b/modules/cloud-config-container/nginx-tls/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/modules/cloud-config-container/nginx-tls/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/modules/cloud-config-container/nginx/README.md b/modules/cloud-config-container/nginx/README.md
index c993eb72ea..c5fbc09bdc 100644
--- a/modules/cloud-config-container/nginx/README.md
+++ b/modules/cloud-config-container/nginx/README.md
@@ -10,7 +10,7 @@ The resulting `cloud-config` can be customized in a number of ways:
The default instance configuration inserts iptables rules to allow traffic on port 80.
-Logging and monitoring are enabled via the [Google Cloud Logging driver](https://docs.docker.com/config/containers/logging/gcplogs/) configured for the CoreDNS container, and the [Node Problem Detector](https://cloud.google.com/container-optimized-os/docs/how-to/monitoring) service started by default on boot.
+Logging and monitoring are enabled via the [Google Cloud Logging agent](https://cloud.google.com/container-optimized-os/docs/how-to/logging) configured for the instance via the `google-logging-enabled` metadata property, and the [Node Problem Detector](https://cloud.google.com/container-optimized-os/docs/how-to/monitoring) service started by default on boot.
The module renders the generated cloud config in the `cloud_config` output, to be used in instances or instance templates via the `user-data` metadata.
@@ -24,32 +24,30 @@ This example will create a `cloud-config` that uses the module's defaults, creat
```hcl
module "cos-nginx" {
- source = "./modules/cloud-config-container/nginx"
+ source = "./fabric/modules/cloud-config-container/nginx"
}
-# use it as metadata in a compute instance or template
-resource "google_compute_instance" "default" {
+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
+ 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 = "./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
```
@@ -59,18 +57,18 @@ module "cos-nginx" {
|---|---|:---:|:---:|:---:|
| [cloud_config](variables.tf#L17) | Cloud config template path. If null default will be used. | string
| | null
|
| [config_variables](variables.tf#L23) | Additional variables used to render the cloud-config and Nginx templates. | map(any)
| | {}
|
-| [file_defaults](variables.tf#L41) | Default owner and permissions for files. | object({…})
| | {…}
|
-| [files](variables.tf#L53) | Map of extra files to create on the instance, path as key. Owner and permissions will use defaults if null. | map(object({…}))
| | {}
|
-| [image](variables.tf#L29) | Nginx container image. | string
| | "nginxdemos/hello:plain-text"
|
-| [nginx_config](variables.tf#L35) | Nginx configuration path, if null container 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({…})
| | {…}
|
+| [file_defaults](variables.tf#L29) | Default owner and permissions for files. | object({…})
| | {…}
|
+| [files](variables.tf#L41) | Map of extra files to create on the instance, path as key. Owner and permissions will use defaults if null. | map(object({…}))
| | {}
|
+| [image](variables.tf#L51) | Nginx container image. | string
| | "nginxdemos/hello:plain-text"
|
+| [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)
| | []
|
+| [users](variables.tf#L75) | List of additional usernames to be created. | list(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/nginx/cloud-config.yaml b/modules/cloud-config-container/nginx/cloud-config.yaml
index d061e03760..f4d05bc452 100644
--- a/modules/cloud-config-container/nginx/cloud-config.yaml
+++ b/modules/cloud-config-container/nginx/cloud-config.yaml
@@ -20,6 +20,10 @@
users:
- name: nginx
uid: 2000
+ %{ for user in users }
+ - name: ${user.username}
+ uid: ${user.uid}
+ %{ endfor }
write_files:
- path: /var/lib/docker/daemon.json
@@ -52,8 +56,10 @@ write_files:
After=gcr-online.target docker.socket
Wants=gcr-online.target docker.socket docker-events-collector.service
[Service]
+ Environment="HOME=/home/nginx"
+ ExecStartPre=/usr/bin/docker-credential-gcr configure-docker
ExecStart=/usr/bin/docker run --rm --name=nginx \
- --log-driver=gcplogs --network host \
+ --network host \
%{~ if etc_mount ~}
-v /etc/nginx/conf.d:/etc/nginx/conf.d \
%{~ endif ~}
@@ -65,13 +71,19 @@ write_files:
owner: ${lookup(data, "owner", "root")}
permissions: ${lookup(data, "permissions", "0644")}
content: |
- ${indent(4, data.content)}
+ ${indent(6, data.content)}
%{ endfor }
bootcmd:
- systemctl start node-problem-detector
runcmd:
+%{ for cmd in runcmd_pre ~}
+ - ${cmd}
+%{ endfor ~}
- iptables -I INPUT 1 -p tcp -m tcp --dport 80 -m state --state NEW,ESTABLISHED -j ACCEPT
- systemctl daemon-reload
- systemctl start nginx
+%{ for cmd in runcmd_post ~}
+ - ${cmd}
+%{ endfor ~}
diff --git a/modules/cloud-config-container/nginx/instance.tf b/modules/cloud-config-container/nginx/instance.tf
deleted file mode 120000
index bdef596b6d..0000000000
--- 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/main.tf b/modules/cloud-config-container/nginx/main.tf
index a2fce41b8a..39c59930fe 100644
--- a/modules/cloud-config-container/nginx/main.tf
+++ b/modules/cloud-config-container/nginx/main.tf
@@ -20,13 +20,16 @@ locals {
var.nginx_config != null || length([
for name in keys(var.files) :
name if substr(name, 0, 18) == "/etc/nginx/conf.d/"
- ]) > 1
+ ]) > 0
)
files = local.files
+ users = var.users
image = var.image
nginx_config = (var.nginx_config == null ? null : templatefile(
var.nginx_config, var.config_variables
))
+ runcmd_pre = var.runcmd_pre
+ runcmd_post = var.runcmd_post
}))
files = {
for path, attrs in var.files : path => {
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 ea9e240458..0000000000
--- 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 94af61e4dd..0000000000
--- 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/variables.tf b/modules/cloud-config-container/nginx/variables.tf
index dec89cc2b4..973baff283 100644
--- a/modules/cloud-config-container/nginx/variables.tf
+++ b/modules/cloud-config-container/nginx/variables.tf
@@ -26,18 +26,6 @@ variable "config_variables" {
default = {}
}
-variable "image" {
- description = "Nginx container image."
- type = string
- default = "nginxdemos/hello:plain-text"
-}
-
-variable "nginx_config" {
- description = "Nginx configuration path, if null container default will be used."
- type = string
- default = null
-}
-
variable "file_defaults" {
description = "Default owner and permissions for files."
type = object({
@@ -59,3 +47,37 @@ variable "files" {
}))
default = {}
}
+
+variable "image" {
+ description = "Nginx container image."
+ type = string
+ default = "nginxdemos/hello:plain-text"
+}
+
+variable "nginx_config" {
+ description = "Nginx configuration path, if null container default will be used."
+ type = string
+ default = null
+}
+
+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 = "List of additional usernames to be created."
+ type = list(object({
+ username = string,
+ uid = number,
+ }))
+ default = [
+ ]
+}
diff --git a/modules/cloud-config-container/nginx/versions.tf b/modules/cloud-config-container/nginx/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/cloud-config-container/nginx/versions.tf
+++ b/modules/cloud-config-container/nginx/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/cloud-config-container/onprem/README.md b/modules/cloud-config-container/onprem/README.md
deleted file mode 100644
index 222d25b043..0000000000
--- a/modules/cloud-config-container/onprem/README.md
+++ /dev/null
@@ -1,83 +0,0 @@
-# Containerized on-premises infrastructure
-
-This module manages a `cloud-config` configuration that starts an emulated on-premises infrastructure running in Docker Compose on a single instance, and connects it via static or dynamic VPN to a Google Cloud VPN gateway.
-
-The emulated on-premises infrastructure is composed of:
-
-- a [Strongswan container](./docker-images/strongswan) managing the VPN tunnel to GCP
-- an optional Bird container managing the BGP session
-- a CoreDNS container servng local DNS and forwarding to GCP
-- an Nginx container serving a simple static web page
-- a [generic Linux container](./docker-images/toolbox) used as a jump host inside the on-premises network
-
-A [complete scenario using this module](../../../examples/networking/onprem-google-access-dns) is available in the networking examples.
-
-The module renders the generated cloud config in the `cloud_config` output, to be used in instances or instance templates via the `user-data` metadata.
-
-For convenience during development or for simple use cases, the module can optionally manage a single instance via the `test_instance` variable. If the instance is not needed the `instance*tf` files can be safely removed. Refer to the [top-level README](../README.md) for more details on the included instance.
-
-## Examples
-
-### Static VPN
-
-The test instance is optional, as described above.
-
-```hcl
-module "cloud-vpn" {
- source = "./modules/net-vpn-static"
- project_id = "my-project"
- region = "europe-west1"
- network = "my-vpc"
- name = "to-on-prem"
- remote_ranges = ["192.168.192.0/24"]
- tunnels = {
- remote-0 = {
- ike_version = 2
- peer_ip = module.on-prem.external_address
- shared_secret = ""
- traffic_selectors = { local = ["0.0.0.0/0"], remote = null }
- }
- }
-}
-
-module "on-prem" {
- source = "./modules/cos-container/on-prem"
- name = "onprem"
- vpn_config = {
- type = "static"
- peer_ip = module.cloud-vpn.address
- shared_secret = module.cloud-vpn.random_secret
- }
- 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"
- }
-}
-```
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [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"]
|
-
-## 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/onprem/docker-images/strongswan/Dockerfile b/modules/cloud-config-container/onprem/docker-images/strongswan/Dockerfile
deleted file mode 100644
index 7a22d94369..0000000000
--- a/modules/cloud-config-container/onprem/docker-images/strongswan/Dockerfile
+++ /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
-#
-# 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 alpine:latest
-
-RUN set -xe \
- && apk add --no-cache strongswan bash sudo
-
-COPY entrypoint.sh /entrypoint.sh
-RUN chmod 0755 /entrypoint.sh
-
-COPY ipsec-vti.sh /var/lib/strongswan/ipsec-vti.sh
-RUN chmod 0755 /var/lib/strongswan/ipsec-vti.sh
-
-RUN echo 'ipsec ALL=NOPASSWD:SETENV:/usr/sbin/ipsec,/sbin/ip,/sbin/sysctl' > /etc/sudoers.d/ipsec
-RUN chmod 0440 /etc/sudoers.d/ipsec
-
-ENV VPN_DEVICE=eth0
-ENV LAN_NETWORKS=192.168.0.0/24
-
-EXPOSE 500/udp 4500/udp
-
-ENTRYPOINT ["/entrypoint.sh"]
diff --git a/modules/cloud-config-container/onprem/instance.tf b/modules/cloud-config-container/onprem/instance.tf
deleted file mode 120000
index bdef596b6d..0000000000
--- 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 ea9e240458..0000000000
--- 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 94af61e4dd..0000000000
--- 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/onprem/variables.tf b/modules/cloud-config-container/onprem/variables.tf
deleted file mode 100644
index 3b09e23665..0000000000
--- a/modules/cloud-config-container/onprem/variables.tf
+++ /dev/null
@@ -1,74 +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
- *
- * 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.
- */
-
-variable "config_variables" {
- description = "Additional variables used to render the cloud-config and CoreDNS templates."
- type = map(any)
- default = {}
-}
-
-variable "coredns_config" {
- description = "CoreDNS configuration path, if null default will be used."
- type = string
- default = null
-}
-
-variable "local_ip_cidr_range" {
- description = "IP CIDR range used for the Docker onprem network."
- type = string
- default = "192.168.192.0/24"
-}
-
-variable "vpn_config" {
- description = "VPN configuration, type must be one of 'dynamic' or 'static'."
- type = object({
- peer_ip = string
- shared_secret = string
- type = string
- peer_ip2 = string
- shared_secret2 = string
- })
-}
-
-variable "vpn_dynamic_config" {
- description = "BGP configuration for dynamic VPN, ignored if VPN type is 'static'."
- type = object({
- local_bgp_asn = number
- local_bgp_address = string
- peer_bgp_asn = number
- peer_bgp_address = string
- local_bgp_asn2 = number
- local_bgp_address2 = string
- peer_bgp_asn2 = number
- peer_bgp_address2 = string
- })
- default = {
- local_bgp_asn = 64514
- local_bgp_address = "169.254.1.2"
- peer_bgp_asn = 64513
- peer_bgp_address = "169.254.1.1"
- local_bgp_asn2 = 64514
- local_bgp_address2 = "169.254.2.2"
- peer_bgp_asn2 = 64520
- peer_bgp_address2 = "169.254.2.1"
- }
-}
-
-variable "vpn_static_ranges" {
- description = "Remote CIDR ranges for static VPN, ignored if VPN type is 'dynamic'."
- type = list(string)
- default = ["10.0.0.0/8"]
-}
diff --git a/modules/cloud-config-container/onprem/versions.tf b/modules/cloud-config-container/onprem/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/modules/cloud-config-container/onprem/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/modules/cloud-config-container/outputs-instance.tf b/modules/cloud-config-container/outputs-instance.tf
deleted file mode 100644
index 8c657eb050..0000000000
--- 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
new file mode 100644
index 0000000000..f70842e8c4
--- /dev/null
+++ b/modules/cloud-config-container/simple-nva/README.md
@@ -0,0 +1,82 @@
+# Google Simple NVA Module
+
+This module allows for the creation of a NVA (Network Virtual Appliance) to be used for experiments and as a stub for future appliances deployment.
+
+This NVA can be used to interconnect up to 8 VPCs.
+
+## Examples
+
+### Simple example
+
+```hcl
+locals {
+ network_interfaces = [
+ {
+ addresses = null
+ name = "dev"
+ nat = false
+ network = "dev_vpc_self_link"
+ routes = ["10.128.0.0/9"]
+ subnetwork = "dev_vpc_nva_subnet_self_link"
+ },
+ {
+ addresses = null
+ name = "prod"
+ nat = false
+ network = "prod_vpc_self_link"
+ routes = ["10.0.0.0/9"]
+ subnetwork = "prod_vpc_nva_subnet_self_link"
+ }
+ ]
+}
+
+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
+ # }
+ # }
+}
+
+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
+ }
+ tags = ["nva", "ssh"]
+}
+# tftest modules=1 resources=1
+```
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [network_interfaces](variables.tf#L39) | Network interfaces configuration. | list(object({…}))
| ✓ | |
+| [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({…}))
| | {}
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [cloud_config](outputs.tf#L17) | Rendered cloud-config file to be passed as user-data instance metadata. | |
+
+
diff --git a/modules/cloud-config-container/simple-nva/cloud-config.yaml b/modules/cloud-config-container/simple-nva/cloud-config.yaml
new file mode 100644
index 0000000000..f1d71e8262
--- /dev/null
+++ b/modules/cloud-config-container/simple-nva/cloud-config.yaml
@@ -0,0 +1,58 @@
+#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.
+
+write_files:
+%{ for path, data in files }
+ - path: ${path}
+ owner: ${lookup(data, "owner", "root")}
+ permissions: ${lookup(data, "permissions", "0644")}
+ content: |
+ ${indent(6, data.content)}
+%{ endfor }
+ - path: /etc/systemd/system/routing.service
+ permissions: 0644
+ owner: root
+ content: |
+ [Install]
+ WantedBy=multi-user.target
+ [Unit]
+ Description=Start routing
+ After=network-online.target
+ Wants=network-online.target
+ [Service]
+ ExecStart=/bin/sh -c "/var/run/nva/start-routing.sh"
+ - path: /var/run/nva/start-routing.sh
+ permissions: 0744
+ owner: root
+ content: |
+ iptables --policy FORWARD ACCEPT
+%{ for interface in network_interfaces ~}
+%{ if enable_health_checks ~}
+ /var/run/nva/policy_based_routing.sh ${interface.name}
+%{ endif ~}
+%{ for route in interface.routes ~}
+ ip route add ${route} via `curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/${interface.number}/gateway -H "Metadata-Flavor:Google"` dev ${interface.name}
+%{ endfor ~}
+%{ endfor ~}
+
+bootcmd:
+ - systemctl start node-problem-detector
+
+runcmd:
+ - systemctl daemon-reload
+ - systemctl enable routing
+ - systemctl start routing
+
diff --git a/modules/cloud-config-container/simple-nva/files/ipprefix_by_netmask.sh b/modules/cloud-config-container/simple-nva/files/ipprefix_by_netmask.sh
new file mode 100644
index 0000000000..a1c69822df
--- /dev/null
+++ b/modules/cloud-config-container/simple-nva/files/ipprefix_by_netmask.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# 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.
+
+# https://stackoverflow.com/questions/50413579/bash-convert-netmask-in-cidr-notation
+c=0 x=0$(printf '%o' ${1//./ })
+while [ $x -gt 0 ]; do
+ let c+=$((x % 2)) 'x>>=1'
+done
+echo $c
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
new file mode 100644
index 0000000000..951396d356
--- /dev/null
+++ b/modules/cloud-config-container/simple-nva/files/policy_based_routing.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+# 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.
+
+IF_NAME=$1
+IP_LB=$(ip r show table local | grep "$IF_NAME proto 66" | cut -f 2 -d " ")
+
+# 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/main.tf b/modules/cloud-config-container/simple-nva/main.tf
new file mode 100644
index 0000000000..4ff0afe29b
--- /dev/null
+++ b/modules/cloud-config-container/simple-nva/main.tf
@@ -0,0 +1,56 @@
+/**
+ * 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 {
+ cloud_config = templatefile(local.template, merge({
+ files = local.files
+ enable_health_checks = var.enable_health_checks
+ network_interfaces = local.network_interfaces
+ }))
+
+ files = merge({
+ "/var/run/nva/ipprefix_by_netmask.sh" = {
+ content = file("${path.module}/files/ipprefix_by_netmask.sh")
+ owner = "root"
+ permissions = "0744"
+ }
+ "/var/run/nva/policy_based_routing.sh" = {
+ content = file("${path.module}/files/policy_based_routing.sh")
+ owner = "root"
+ permissions = "0744"
+ }
+ }, {
+ for path, attrs in var.files : path => {
+ content = attrs.content,
+ owner = attrs.owner,
+ permissions = attrs.permissions
+ }
+ })
+
+ network_interfaces = [
+ for index, interface in var.network_interfaces : {
+ name = "eth${index}"
+ number = index
+ routes = interface.routes
+ }
+ ]
+
+ template = (
+ var.cloud_config == null
+ ? "${path.module}/cloud-config.yaml"
+ : var.cloud_config
+ )
+}
diff --git a/modules/cloud-config-container/simple-nva/outputs.tf b/modules/cloud-config-container/simple-nva/outputs.tf
new file mode 100644
index 0000000000..7d8d41656b
--- /dev/null
+++ b/modules/cloud-config-container/simple-nva/outputs.tf
@@ -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.
+ */
+
+output "cloud_config" {
+ description = "Rendered cloud-config file to be passed as user-data instance metadata."
+ value = local.cloud_config
+}
diff --git a/modules/cloud-config-container/simple-nva/variables.tf b/modules/cloud-config-container/simple-nva/variables.tf
new file mode 100644
index 0000000000..39d96d913f
--- /dev/null
+++ b/modules/cloud-config-container/simple-nva/variables.tf
@@ -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.
+ */
+
+variable "cloud_config" {
+ description = "Cloud config template path. If null default will be used."
+ type = string
+ default = null
+}
+
+variable "enable_health_checks" {
+ description = "Configures routing to enable responses to health check probes."
+ type = bool
+ default = false
+}
+
+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
+ }))
+ default = {}
+}
+
+variable "network_interfaces" {
+ description = "Network interfaces configuration."
+ type = list(object({
+ routes = optional(list(string))
+ }))
+}
diff --git a/modules/cloud-config-container/simple-nva/versions.tf b/modules/cloud-config-container/simple-nva/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/modules/cloud-config-container/simple-nva/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/modules/cloud-config-container/squid/README.md b/modules/cloud-config-container/squid/README.md
index 912c526228..4ee29ed912 100644
--- a/modules/cloud-config-container/squid/README.md
+++ b/modules/cloud-config-container/squid/README.md
@@ -10,7 +10,7 @@ The resulting `cloud-config` can be customized in a number of ways:
The default instance configuration inserts iptables rules to allow traffic on TCP port 3128. With the default `squid.conf`, deny rules take precedence over allow rules.
-Logging and monitoring are enabled via the [Google Cloud Logging driver](https://docs.docker.com/config/containers/logging/gcplogs/) configured for the Squid container, and the [Node Problem Detector](https://cloud.google.com/container-optimized-os/docs/how-to/monitoring) service started by default on boot.
+Logging and monitoring are enabled via the [Google Cloud Logging agent](https://cloud.google.com/container-optimized-os/docs/how-to/logging) configured for the instance via the `google-logging-enabled` metadata property, and the [Node Problem Detector](https://cloud.google.com/container-optimized-os/docs/how-to/monitoring) service started by default on boot.
The module renders the generated cloud config in the `cloud_config` output, to be used in instances or instance templates via the `user-data` metadata.
@@ -24,36 +24,32 @@ This example will create a `cloud-config` that allows any client in the 10.0.0.0
```hcl
module "cos-squid" {
- source = "./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
-resource "google_compute_instance" "default" {
+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
+ 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 = "./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
```
@@ -61,23 +57,20 @@ module "cos-squid" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [allow](variables.tf#L57) | List of domains Squid will allow connections to. | list(string)
| | []
|
-| [clients](variables.tf#L69) | List of CIDR ranges from which Squid will allow connections. | list(string)
| | []
|
-| [cloud_config](variables.tf#L17) | Cloud config template path. If null default will be used. | string
| | null
|
-| [config_variables](variables.tf#L23) | Additional variables used to render the cloud-config and Squid templates. | map(any)
| | {}
|
-| [default_action](variables.tf#L75) | Default action for domains not matching neither the allow or deny lists. | string
| | "deny"
|
-| [deny](variables.tf#L63) | List of domains Squid will deny connections to. | list(string)
| | []
|
-| [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({…}))
| | {}
|
-| [squid_config](variables.tf#L29) | 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({…})
| | {…}
|
+| [allow](variables.tf#L18) | List of domains Squid will allow connections to. | list(string)
| | []
|
+| [clients](variables.tf#L24) | List of CIDR ranges from which Squid will allow connections. | list(string)
| | []
|
+| [cloud_config](variables.tf#L30) | Cloud config template path. If null default will be used. | string
| | null
|
+| [config_variables](variables.tf#L36) | Additional variables used to render the cloud-config and Squid templates. | map(any)
| | {}
|
+| [default_action](variables.tf#L42) | Default action for domains not matching neither the allow or deny lists. | string
| | "deny"
|
+| [deny](variables.tf#L52) | List of domains Squid will deny connections to. | list(string)
| | []
|
+| [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
|
## 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/cloud-config.yaml b/modules/cloud-config-container/squid/cloud-config.yaml
index b0c4b7fe34..5ba6e9878c 100644
--- a/modules/cloud-config-container/squid/cloud-config.yaml
+++ b/modules/cloud-config-container/squid/cloud-config.yaml
@@ -14,8 +14,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-# TODO: switch to the gcplogs logging driver, and set driver labels
-
users:
- name: squid
uid: 2000
@@ -70,10 +68,10 @@ write_files:
[Service]
Environment="HOME=/home/squid"
ExecStartPre=/usr/bin/docker-credential-gcr configure-docker
- ExecStart=/usr/bin/docker run --rm --name=squid \
- --log-driver=gcplogs --network host \
+ ExecStart=/usr/bin/docker run --rm --name=squid \
+ --network host \
-v /etc/squid:/etc/squid \
- gcr.io/pso-cft-fabric/squid:0.10
+ gcr.io/pso-cft-fabric/squid:20221116
ExecStop=/usr/bin/docker stop squid
ExecStopPost=/usr/bin/docker rm squid
diff --git a/modules/cloud-config-container/squid/docker/Dockerfile b/modules/cloud-config-container/squid/docker/Dockerfile
index bdbc7d04cb..2ae03a4fdc 100644
--- a/modules/cloud-config-container/squid/docker/Dockerfile
+++ b/modules/cloud-config-container/squid/docker/Dockerfile
@@ -17,6 +17,7 @@ FROM debian:buster-slim
ENV SQUID_VERSION=4.6 \
SQUID_CACHE_DIR=/var/spool/squid \
SQUID_LOG_DIR=/var/log/squid \
+ SQUID_PID_DIR=/var/run/squid \
SQUID_USER=proxy
RUN apt-get update \
@@ -26,5 +27,12 @@ RUN apt-get update \
COPY entrypoint.sh /sbin/entrypoint.sh
RUN chmod 755 /sbin/entrypoint.sh
+# Create the PID file directory as root, as the non-privileged user squid is not
+# allowed to write in /var/run.
+RUN mkdir -p ${SQUID_PID_DIR} \
+ && chown ${SQUID_USER}:${SQUID_USER} ${SQUID_PID_DIR}
+
+USER ${SQUID_USER}
+
EXPOSE 3128/tcp
ENTRYPOINT ["/sbin/entrypoint.sh"]
diff --git a/modules/cloud-config-container/squid/docker/cloudbuild.yaml b/modules/cloud-config-container/squid/docker/cloudbuild.yaml
index e2e725fb3d..aca00b9bf0 100644
--- a/modules/cloud-config-container/squid/docker/cloudbuild.yaml
+++ b/modules/cloud-config-container/squid/docker/cloudbuild.yaml
@@ -24,7 +24,7 @@ steps:
- .
substitutions:
- _IMAGE_VERSION: "20210215"
+ _IMAGE_VERSION: "20221116"
images:
- "gcr.io/$PROJECT_ID/squid:${_IMAGE_VERSION}"
- "gcr.io/$PROJECT_ID/squid:latest"
diff --git a/modules/cloud-config-container/squid/instance.tf b/modules/cloud-config-container/squid/instance.tf
deleted file mode 120000
index bdef596b6d..0000000000
--- 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 ea9e240458..0000000000
--- 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/squid.conf b/modules/cloud-config-container/squid/squid.conf
index b1c45fc8e2..fda94b3c4a 100644
--- a/modules/cloud-config-container/squid/squid.conf
+++ b/modules/cloud-config-container/squid/squid.conf
@@ -4,6 +4,14 @@ http_port 0.0.0.0:3128
# only proxy, don't cache
cache deny all
+# redirect all logs to /dev/stdout
+logfile_rotate 0
+cache_log stdio:/dev/stdout
+access_log stdio:/dev/stdout
+cache_store_log stdio:/dev/stdout
+
+pid_filename /var/run/squid/squid.pid
+
acl ssl_ports port 443
acl safe_ports port 80
acl safe_ports port 443
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 94af61e4dd..0000000000
--- 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/variables.tf b/modules/cloud-config-container/squid/variables.tf
index 5180c6d327..b7708821c4 100644
--- a/modules/cloud-config-container/squid/variables.tf
+++ b/modules/cloud-config-container/squid/variables.tf
@@ -14,6 +14,19 @@
* limitations under the License.
*/
+
+variable "allow" {
+ description = "List of domains Squid will allow connections to."
+ type = list(string)
+ default = []
+}
+
+variable "clients" {
+ description = "List of CIDR ranges from which Squid will allow connections."
+ type = list(string)
+ default = []
+}
+
variable "cloud_config" {
description = "Cloud config template path. If null default will be used."
type = string
@@ -26,10 +39,20 @@ variable "config_variables" {
default = {}
}
-variable "squid_config" {
- description = "Squid configuration path, if null default will be used."
+variable "default_action" {
+ description = "Default action for domains not matching neither the allow or deny lists."
type = string
- default = null
+ default = "deny"
+ validation {
+ condition = var.default_action == "deny" || var.default_action == "allow"
+ error_message = "Default action must be allow or deny."
+ }
+}
+
+variable "deny" {
+ description = "List of domains Squid will deny connections to."
+ type = list(string)
+ default = []
}
variable "file_defaults" {
@@ -54,30 +77,8 @@ variable "files" {
default = {}
}
-variable "allow" {
- description = "List of domains Squid will allow connections to."
- type = list(string)
- default = []
-}
-
-variable "deny" {
- description = "List of domains Squid will deny connections to."
- type = list(string)
- default = []
-}
-
-variable "clients" {
- description = "List of CIDR ranges from which Squid will allow connections."
- type = list(string)
- default = []
-}
-
-variable "default_action" {
- description = "Default action for domains not matching neither the allow or deny lists."
+variable "squid_config" {
+ description = "Squid configuration path, if null default will be used."
type = string
- default = "deny"
- validation {
- condition = var.default_action == "deny" || var.default_action == "allow"
- error_message = "Default action must be allow or deny."
- }
+ default = null
}
diff --git a/modules/cloud-config-container/squid/versions.tf b/modules/cloud-config-container/squid/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/cloud-config-container/squid/versions.tf
+++ b/modules/cloud-config-container/squid/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.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 3697133ea7..0000000000
--- 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 46a5b4586a..ebf300fccf 100644
--- a/modules/cloud-function/README.md
+++ b/modules/cloud-function/README.md
@@ -16,17 +16,32 @@ This deploys a Cloud Function with an HTTP endpoint, using a pre-existing GCS bu
```hcl
module "cf-http" {
- source = "./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 = "my-cf-source-folder"
+ source_dir = "fabric/assets/"
output_path = "bundle.zip"
- excludes = null
}
}
-# tftest skip
+# tftest modules=1 resources=2
+```
+
+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"
+ bundle_config = {
+ source_dir = "fabric/assets/"
+ output_path = "bundle.zip"
+ }
+}
+# tftest modules=1 resources=2
```
### PubSub and non-HTTP triggers
@@ -35,23 +50,60 @@ Other trigger types other than HTTP are configured via the `trigger_config` vari
```hcl
module "cf-http" {
- source = "./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"
+ }
+ trigger_config = {
+ v1 = {
+ event = "google.pubsub.topic.publish"
+ resource = "local.my-topic"
+ }
+ }
+}
+# tftest modules=1 resources=2
+```
+
+Cloud Functions 2nd gen support only [Eventarc](https://cloud.google.com/eventarc/docs) and uses separate structure
+to configure:
+```hcl
+module "trigger-service-account" {
+ source = "./fabric/modules/iam-service-account"
+ project_id = "my-project"
+ name = "sa-cloudfunction"
+ iam_project_roles = {
+ "my-project" = [
+ "roles/run.invoker"
+ ]
+ }
+}
+
+module "cf-http" {
+ source = "./fabric/modules/cloud-function"
+ project_id = "my-project"
+ v2 = true
+ name = "test-cf-http"
+ bucket_name = "test-cf-bundles"
bundle_config = {
- source_dir = "my-cf-source-folder"
+ source_dir = "fabric/assets/"
output_path = "bundle.zip"
- excludes = null
}
trigger_config = {
- event = "google.pubsub.topic.publish"
- resource = local.my-topic
- retry = null
+ v2 = {
+ event_type = "google.cloud.pubsub.topic.v1.messagePublished"
+ pubsub_topic = "local.my-topic"
+ service_account_email = module.trigger-service-account.email
+ }
}
}
-# tftest skip
+# tftest modules=2 resources=4
```
+Ensure that pubsub robo-account `service-%s@gcp-sa-pubsub.iam.gserviceaccount.com` has `roles/iam.serviceAccountTokenCreatator`
+as documented [here](https://cloud.google.com/eventarc/docs/roles-permissions#pubsub-topic)
### Controlling HTTP access
@@ -59,20 +111,19 @@ To allow anonymous access to the function, grant the `roles/cloudfunctions.invok
```hcl
module "cf-http" {
- source = "./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 = "my-cf-source-folder"
+ source_dir = "fabric/assets/"
output_path = "bundle.zip"
- excludes = null
}
- iam = {
+ iam = {
"roles/cloudfunctions.invoker" = ["allUsers"]
}
}
-# tftest skip
+# tftest modules=1 resources=3
```
### GCS bucket creation
@@ -81,21 +132,18 @@ You can have the module auto-create the GCS bucket used for deployment via the `
```hcl
module "cf-http" {
- source = "./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 = {
- location = null
- lifecycle_delete_age = 1
+ lifecycle_delete_age_days = 1
}
bundle_config = {
- source_dir = "my-cf-source-folder"
- output_path = "bundle.zip"
- excludes = null
+ source_dir = "fabric/assets/"
}
}
-# tftest skip
+# tftest modules=1 resources=3
```
### Service account management
@@ -104,36 +152,34 @@ To use a custom service account managed by the module, set `service_account_crea
```hcl
module "cf-http" {
- source = "./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 = "my-cf-source-folder"
+ source_dir = "fabric/assets/"
output_path = "bundle.zip"
- excludes = null
}
service_account_create = true
}
-# tftest skip
+# tftest modules=1 resources=3
```
To use an externally managed service account, pass its email in `service_account` and leave `service_account_create` to `false` (the default).
```hcl
module "cf-http" {
- source = "./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 = "my-cf-source-folder"
+ source_dir = "fabric/assets/"
output_path = "bundle.zip"
- excludes = null
}
- service_account = local.service_account_email
+ service_account = "non-existent@serice.account.email"
}
-# tftest skip
+# tftest modules=1 resources=2
```
### Custom bundle config
@@ -142,17 +188,36 @@ In order to help prevent `archive_zip.output_md5` from changing cross platform (
```hcl
module "cf-http" {
- source = "./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 = "my-cf-source-folder"
+ source_dir = "fabric/assets"
output_path = "bundle.zip"
excludes = ["__pycache__"]
}
}
-# tftest skip
+# tftest modules=1 resources=2
+```
+
+### Private Cloud Build Pool
+
+This deploys a Cloud Function with an HTTP endpoint, using a pre-existing GCS bucket for deployment using a pre existing private Cloud Build worker pool.
+
+```hcl
+module "cf-http" {
+ 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"
+ output_path = "bundle.zip"
+ }
+}
+# tftest modules=1 resources=2
```
@@ -161,23 +226,26 @@ module "cf-http" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [bucket_name](variables.tf#L26) | Name of the bucket that will be used for the function code. It will be created with prefix prepended if bucket_config is not null. | string
| ✓ | |
-| [bundle_config](variables.tf#L31) | Cloud function source folder and generated zip bundle paths. Output path defaults to '/tmp/bundle.zip' if null. | object({…})
| ✓ | |
-| [name](variables.tf#L88) | Name used for cloud function and associated resources. | string
| ✓ | |
-| [project_id](variables.tf#L99) | Project id used for all resources. | string
| ✓ | |
-| [bucket_config](variables.tf#L17) | Enable and configure auto-created bucket. Set fields to null to use defaults. | object({…})
| | null
|
-| [description](variables.tf#L40) | Optional description. | string
| | "Terraform managed."
|
-| [environment_variables](variables.tf#L46) | Cloud function environment variables. | map(string)
| | {}
|
-| [function_config](variables.tf#L52) | Cloud function configuration. | object({…})
| | {…}
|
-| [iam](variables.tf#L70) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
-| [ingress_settings](variables.tf#L76) | Control traffic that reaches the cloud function. Allowed values are ALLOW_ALL and ALLOW_INTERNAL_ONLY. | string
| | null
|
-| [labels](variables.tf#L82) | Resource labels. | map(string)
| | {}
|
-| [prefix](variables.tf#L93) | Optional prefix used for resource names. | string
| | null
|
-| [region](variables.tf#L104) | Region used for all resources. | string
| | "europe-west1"
|
-| [service_account](variables.tf#L110) | Service account email. Unused if service account is auto-created. | string
| | null
|
-| [service_account_create](variables.tf#L116) | Auto-create service account. | bool
| | false
|
-| [trigger_config](variables.tf#L122) | Function trigger configuration. Leave null for HTTP trigger. | object({…})
| | null
|
-| [vpc_connector](variables.tf#L132) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…})
| | null
|
-| [vpc_connector_config](variables.tf#L142) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…})
| | null
|
+| [bundle_config](variables.tf#L37) | Cloud function source folder and generated zip bundle paths. Output path defaults to '/tmp/bundle.zip' if null. | object({…})
| ✓ | |
+| [name](variables.tf#L94) | Name used for cloud function and associated resources. | string
| ✓ | |
+| [project_id](variables.tf#L109) | Project id used for all resources. | string
| ✓ | |
+| [bucket_config](variables.tf#L17) | Enable and configure auto-created bucket. Set fields to null to use defaults. | object({…})
| | null
|
+| [build_worker_pool](variables.tf#L31) | Build worker pool, in projects/string
| | null
|
+| [description](variables.tf#L46) | Optional description. | string
| | "Terraform managed."
|
+| [environment_variables](variables.tf#L52) | Cloud function environment variables. | map(string)
| | {}
|
+| [function_config](variables.tf#L58) | Cloud function configuration. Defaults to using main as entrypoint, 1 instance with 256MiB of memory, and 180 second timeout. | object({…})
| | {…}
|
+| [iam](variables.tf#L76) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
+| [ingress_settings](variables.tf#L82) | Control traffic that reaches the cloud function. Allowed values are ALLOW_ALL, ALLOW_INTERNAL_AND_GCLB and ALLOW_INTERNAL_ONLY . | string
| | null
|
+| [labels](variables.tf#L88) | Resource labels. | map(string)
| | {}
|
+| [prefix](variables.tf#L99) | Optional prefix used for resource names. | string
| | null
|
+| [region](variables.tf#L114) | Region used for all resources. | string
| | "europe-west1"
|
+| [secrets](variables.tf#L120) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…}))
| | {}
|
+| [service_account](variables.tf#L132) | Service account email. Unused if service account is auto-created. | string
| | null
|
+| [service_account_create](variables.tf#L138) | Auto-create service account. | bool
| | false
|
+| [trigger_config](variables.tf#L144) | Function trigger configuration. Leave null for HTTP trigger. | object({…})
| | { v1 = null, v2 = null }
|
+| [v2](variables.tf#L173) | Whether to use Cloud Function version 2nd Gen or 1st Gen. | bool
| | false
|
+| [vpc_connector](variables.tf#L179) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…})
| | null
|
+| [vpc_connector_config](variables.tf#L189) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…})
| | null
|
## Outputs
@@ -190,6 +258,10 @@ module "cf-http" {
| [service_account](outputs.tf#L39) | Service account resource. | |
| [service_account_email](outputs.tf#L44) | Service account email. | |
| [service_account_iam_email](outputs.tf#L49) | Service account email. | |
-| [vpc_connector](outputs.tf#L57) | VPC connector resource if created. | |
+| [trigger_service_account](outputs.tf#L57) | Service account resource. | |
+| [trigger_service_account_email](outputs.tf#L62) | Service account email. | |
+| [trigger_service_account_iam_email](outputs.tf#L67) | Service account email. | |
+| [uri](outputs.tf#L75) | Cloud function service uri. | |
+| [vpc_connector](outputs.tf#L80) | VPC connector resource if created. | |
diff --git a/modules/cloud-function/main.tf b/modules/cloud-function/main.tf
index 3a37a63f1b..35dba72942 100644
--- a/modules/cloud-function/main.tf
+++ b/modules/cloud-function/main.tf
@@ -24,15 +24,17 @@ locals {
: null
)
)
- prefix = var.prefix == null ? "" : "${var.prefix}-"
- service_account_email = (
- var.service_account_create
- ? (
- length(google_service_account.service_account) > 0
- ? google_service_account.service_account[0].email
- : null
- )
- : var.service_account
+ function = (
+ var.v2
+ ? google_cloudfunctions2_function.function[0]
+ : google_cloudfunctions_function.function[0]
+ )
+ prefix = var.prefix == null ? "" : "${var.prefix}-"
+ service_account_email = var.service_account_create ? google_service_account.service_account[0].email : var.service_account
+ trigger_service_account_email = (
+ coalesce(try(var.trigger_config.v2.service_account_create, false), false)
+ ? google_service_account.trigger_service_account[0].email
+ : null
)
vpc_connector = (
var.vpc_connector == null
@@ -55,22 +57,25 @@ resource "google_vpc_access_connector" "connector" {
}
resource "google_cloudfunctions_function" "function" {
+ count = var.v2 ? 0 : 1
project = var.project_id
region = var.region
name = "${local.prefix}${var.name}"
description = var.description
runtime = var.function_config.runtime
- available_memory_mb = var.function_config.memory
- max_instances = var.function_config.instances
- timeout = var.function_config.timeout
+ available_memory_mb = var.function_config.memory_mb
+ max_instances = var.function_config.instance_count
+ timeout = var.function_config.timeout_seconds
entry_point = var.function_config.entry_point
environment_variables = var.environment_variables
service_account_email = local.service_account_email
source_archive_bucket = local.bucket
source_archive_object = google_storage_bucket_object.bundle.name
labels = var.labels
- trigger_http = var.trigger_config == null ? true : null
- ingress_settings = var.ingress_settings
+ trigger_http = var.trigger_config.v1 == null ? true : null
+
+ ingress_settings = var.ingress_settings
+ build_worker_pool = var.build_worker_pool
vpc_connector = local.vpc_connector
vpc_connector_egress_settings = try(
@@ -78,34 +83,155 @@ resource "google_cloudfunctions_function" "function" {
)
dynamic "event_trigger" {
- for_each = var.trigger_config == null ? [] : [""]
+ for_each = var.trigger_config.v1 == null ? [] : [""]
content {
- event_type = var.trigger_config.event
- resource = var.trigger_config.resource
+ event_type = var.trigger_config.v1.event
+ resource = var.trigger_config.v1.resource
dynamic "failure_policy" {
- for_each = var.trigger_config.retry == null ? [] : [""]
+ for_each = var.trigger_config.v1.retry == null ? [] : [""]
content {
- retry = var.trigger_config.retry
+ retry = var.trigger_config.v1.retry
}
}
}
}
+ dynamic "secret_environment_variables" {
+ for_each = { for k, v in var.secrets : k => v if !v.is_volume }
+ iterator = secret
+ content {
+ key = secret.key
+ project_id = secret.value.project_id
+ secret = secret.value.secret
+ version = try(secret.value.versions.0, "latest")
+ }
+ }
+
+ dynamic "secret_volumes" {
+ for_each = { for k, v in var.secrets : k => v if v.is_volume }
+ iterator = secret
+ content {
+ mount_path = secret.key
+ project_id = secret.value.project_id
+ secret = secret.value.secret
+ dynamic "versions" {
+ for_each = secret.value.versions
+ iterator = version
+ content {
+ path = split(":", version)[1]
+ version = split(":", version)[0]
+ }
+ }
+ }
+ }
+}
+
+resource "google_cloudfunctions2_function" "function" {
+ count = var.v2 ? 1 : 0
+ provider = google-beta
+ project = var.project_id
+ location = var.region
+ name = "${local.prefix}${var.name}"
+ description = var.description
+ build_config {
+ worker_pool = var.build_worker_pool
+ runtime = var.function_config.runtime
+ entry_point = "${var.function_config.entry_point}_http" # Set the entry point
+ environment_variables = var.environment_variables
+ source {
+ storage_source {
+ bucket = local.bucket
+ object = google_storage_bucket_object.bundle.name
+ }
+ }
+ }
+ dynamic "event_trigger" {
+ for_each = var.trigger_config.v2 == null ? [] : [""]
+ content {
+ trigger_region = var.trigger_config.v2.region
+ event_type = var.trigger_config.v2.event_type
+ pubsub_topic = var.trigger_config.v2.pubsub_topic
+ dynamic "event_filters" {
+ for_each = var.trigger_config.v2.event_filters == null ? [] : var.trigger_config.v2.event_filters
+ iterator = event_filter
+ content {
+ attribute = event_filter.attribute
+ value = event_filter.value
+ operator = event_filter.operator
+ }
+ }
+ service_account_email = var.trigger_config.v2.service_account_email
+ retry_policy = var.trigger_config.v2.retry_policy
+ }
+ }
+ service_config {
+ max_instance_count = var.function_config.instance_count
+ min_instance_count = 0
+ available_memory = "${var.function_config.memory_mb}M"
+ timeout_seconds = var.function_config.timeout_seconds
+ environment_variables = var.environment_variables
+ ingress_settings = var.ingress_settings
+ all_traffic_on_latest_revision = true
+ service_account_email = local.service_account_email
+ vpc_connector = local.vpc_connector
+ vpc_connector_egress_settings = try(
+ var.vpc_connector.egress_settings, null)
+
+ dynamic "secret_environment_variables" {
+ for_each = { for k, v in var.secrets : k => v if !v.is_volume }
+ iterator = secret
+ content {
+ key = secret.key
+ project_id = secret.value.project_id
+ secret = secret.value.secret
+ version = try(secret.value.versions.0, "latest")
+ }
+ }
+
+ dynamic "secret_volumes" {
+ for_each = { for k, v in var.secrets : k => v if v.is_volume }
+ iterator = secret
+ content {
+ mount_path = secret.key
+ project_id = secret.value.project_id
+ secret = secret.value.secret
+ dynamic "versions" {
+ for_each = secret.value.versions
+ iterator = version
+ content {
+ path = split(":", version)[1]
+ version = split(":", version)[0]
+ }
+ }
+ }
+ }
+ }
+ labels = var.labels
}
resource "google_cloudfunctions_function_iam_binding" "default" {
- for_each = var.iam
+ for_each = !var.v2 ? var.iam : {}
project = var.project_id
region = var.region
- cloud_function = google_cloudfunctions_function.function.name
+ cloud_function = local.function.name
+ role = each.key
+ members = each.value
+}
+
+resource "google_cloudfunctions2_function_iam_binding" "default" {
+ for_each = var.v2 ? var.iam : {}
+ project = var.project_id
+ location = google_cloudfunctions2_function.function[0].location
+ cloud_function = local.function.name
role = each.key
members = each.value
}
resource "google_storage_bucket" "bucket" {
- count = var.bucket_config == null ? 0 : 1
- project = var.project_id
- name = "${local.prefix}${var.bucket_name}"
+ count = var.bucket_config == null ? 0 : 1
+ project = var.project_id
+ name = "${local.prefix}${var.bucket_name}"
+ uniform_bucket_level_access = true
location = (
var.bucket_config.location == null
? var.region
@@ -114,10 +240,20 @@ resource "google_storage_bucket" "bucket" {
labels = var.labels
dynamic "lifecycle_rule" {
- for_each = var.bucket_config.lifecycle_delete_age == null ? [] : [""]
+ for_each = var.bucket_config.lifecycle_delete_age_days == null ? [] : [""]
content {
action { type = "Delete" }
- condition { age = var.bucket_config.lifecycle_delete_age }
+ condition {
+ age = var.bucket_config.lifecycle_delete_age_days
+ with_state = "ARCHIVED"
+ }
+ }
+ }
+
+ dynamic "versioning" {
+ for_each = var.bucket_config.lifecycle_delete_age_days == null ? [] : [""]
+ content {
+ enabled = true
}
}
}
@@ -129,13 +265,9 @@ resource "google_storage_bucket_object" "bundle" {
}
data "archive_file" "bundle" {
- type = "zip"
- source_dir = var.bundle_config.source_dir
- output_path = (
- var.bundle_config.output_path == null
- ? "/tmp/bundle.zip"
- : var.bundle_config.output_path
- )
+ type = "zip"
+ source_dir = var.bundle_config.source_dir
+ output_path = var.bundle_config.output_path
output_file_mode = "0666"
excludes = var.bundle_config.excludes
}
@@ -146,3 +278,17 @@ resource "google_service_account" "service_account" {
account_id = "tf-cf-${var.name}"
display_name = "Terraform Cloud Function ${var.name}."
}
+
+resource "google_service_account" "trigger_service_account" {
+ count = coalesce(try(var.trigger_config.v2.service_account_create, false), false) ? 1 : 0
+ project = var.project_id
+ account_id = "tf-cf-trigger-${var.name}"
+ display_name = "Terraform trigger for Cloud Function ${var.name}."
+}
+
+resource "google_project_iam_member" "trigger_iam" {
+ count = coalesce(try(var.trigger_config.v2.service_account_create, false), false) ? 1 : 0
+ project = var.project_id
+ member = "serviceAccount:${google_service_account.trigger_service_account[0].email}"
+ role = "roles/run.invoker"
+}
diff --git a/modules/cloud-function/outputs.tf b/modules/cloud-function/outputs.tf
index 1071270e1a..04ce290503 100644
--- a/modules/cloud-function/outputs.tf
+++ b/modules/cloud-function/outputs.tf
@@ -28,12 +28,12 @@ output "bucket_name" {
output "function" {
description = "Cloud function resources."
- value = google_cloudfunctions_function.function
+ value = local.function
}
output "function_name" {
description = "Cloud function name."
- value = google_cloudfunctions_function.function.name
+ value = local.function.name
}
output "service_account" {
@@ -54,6 +54,29 @@ output "service_account_iam_email" {
])
}
+output "trigger_service_account" {
+ description = "Service account resource."
+ value = try(google_service_account.trigger_service_account[0], null)
+}
+
+output "trigger_service_account_email" {
+ description = "Service account email."
+ value = local.trigger_service_account_email
+}
+
+output "trigger_service_account_iam_email" {
+ description = "Service account email."
+ value = join("", [
+ "serviceAccount:",
+ local.trigger_service_account_email == null ? "" : local.trigger_service_account_email
+ ])
+}
+
+output "uri" {
+ description = "Cloud function service uri."
+ value = var.v2 ? google_cloudfunctions2_function.function[0].service_config[0].uri : null
+}
+
output "vpc_connector" {
description = "VPC connector resource if created."
value = try(google_vpc_access_connector.connector.0.id, null)
diff --git a/modules/cloud-function/variables.tf b/modules/cloud-function/variables.tf
index 2ac663b132..97a6217a6f 100644
--- a/modules/cloud-function/variables.tf
+++ b/modules/cloud-function/variables.tf
@@ -17,8 +17,8 @@
variable "bucket_config" {
description = "Enable and configure auto-created bucket. Set fields to null to use defaults."
type = object({
- location = string
- lifecycle_delete_age = number
+ location = optional(string)
+ lifecycle_delete_age_days = optional(number)
})
default = null
}
@@ -28,12 +28,18 @@ variable "bucket_name" {
type = string
}
+variable "build_worker_pool" {
+ description = "Build worker pool, in projects/string
| ✓ | |
| [display_name](variables.tf#L32) | Group display name. | string
| ✓ | |
-| [name](variables.tf#L43) | Group ID (usually an email). | string
| ✓ | |
+| [name](variables.tf#L49) | Group ID (usually an email). | string
| ✓ | |
| [description](variables.tf#L26) | Group description. | string
| | null
|
-| [members](variables.tf#L37) | List of group members. | list(string)
| | []
|
+| [managers](variables.tf#L37) | List of group managers. | list(string)
| | []
|
+| [members](variables.tf#L43) | List of group members. | list(string)
| | []
|
## Outputs
diff --git a/modules/cloud-identity-group/main.tf b/modules/cloud-identity-group/main.tf
index 2bed44351c..7e0455ef06 100644
--- a/modules/cloud-identity-group/main.tf
+++ b/modules/cloud-identity-group/main.tf
@@ -17,6 +17,7 @@
resource "google_cloud_identity_group" "group" {
display_name = var.display_name
parent = var.customer_id
+ description = var.description
group_key {
id = var.name
@@ -36,17 +37,18 @@ resource "google_cloud_identity_group" "group" {
# roles { name = "MANAGER" }
# }
-# resource "google_cloud_identity_group_membership" "managers" {
-# group = google_cloud_identity_group.group.id
-# for_each = toset(var.managers)
-# preferred_member_key { id = each.key }
-# roles { name = "MEMBER" }
-# roles { name = "MANAGER" }
-# }
+resource "google_cloud_identity_group_membership" "managers" {
+ group = google_cloud_identity_group.group.id
+ for_each = toset(var.managers)
+ preferred_member_key { id = each.key }
+ roles { name = "MEMBER" }
+ roles { name = "MANAGER" }
+}
resource "google_cloud_identity_group_membership" "members" {
group = google_cloud_identity_group.group.id
for_each = toset(var.members)
preferred_member_key { id = each.key }
roles { name = "MEMBER" }
+ depends_on = [google_cloud_identity_group_membership.managers]
}
diff --git a/modules/cloud-identity-group/variables.tf b/modules/cloud-identity-group/variables.tf
index b95905372d..221bcb8494 100644
--- a/modules/cloud-identity-group/variables.tf
+++ b/modules/cloud-identity-group/variables.tf
@@ -34,6 +34,12 @@ variable "display_name" {
type = string
}
+variable "managers" {
+ description = "List of group managers."
+ type = list(string)
+ default = []
+}
+
variable "members" {
description = "List of group members."
type = list(string)
@@ -50,9 +56,3 @@ variable "name" {
# type = list(string)
# default = []
# }
-
-# variable "managers" {
-# description = "List of group managers."
-# type = list(string)
-# default = []
-# }
diff --git a/modules/cloud-identity-group/versions.tf b/modules/cloud-identity-group/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/cloud-identity-group/versions.tf
+++ b/modules/cloud-identity-group/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/cloud-run/README.md b/modules/cloud-run/README.md
index 3ac4075da9..5fde6ed47b 100644
--- a/modules/cloud-run/README.md
+++ b/modules/cloud-run/README.md
@@ -10,22 +10,22 @@ This deploys a Cloud Run service and sets some environment variables.
```hcl
module "cloud_run" {
- source = "./modules/cloud-run"
+ source = "./fabric/modules/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
}]
}
@@ -36,24 +36,24 @@ module "cloud_run" {
```hcl
module "cloud_run" {
- source = "./modules/cloud-run"
+ source = "./fabric/modules/cloud-run"
project_id = "my-project"
name = "hello"
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 = "./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 = "./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
}
}
@@ -123,7 +123,7 @@ This deploys a Cloud Run service that will be triggered when messages are publis
```hcl
module "cloud_run" {
- source = "./modules/cloud-run"
+ source = "./fabric/modules/cloud-run"
project_id = "my-project"
name = "hello"
containers = [{
@@ -147,7 +147,7 @@ This deploys a Cloud Run service that will be triggered when specific log events
```hcl
module "cloud_run" {
- source = "./modules/cloud-run"
+ source = "./fabric/modules/cloud-run"
project_id = "my-project"
name = "hello"
containers = [{
@@ -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"
}
]
}
@@ -173,7 +173,7 @@ To use a custom service account managed by the module, set `service_account_crea
```hcl
module "cloud_run" {
- source = "./modules/cloud-run"
+ source = "./fabric/modules/cloud-run"
project_id = "my-project"
name = "hello"
containers = [{
@@ -192,7 +192,7 @@ To use an externally managed service account, pass its email in `service_account
```hcl
module "cloud_run" {
- source = "./modules/cloud-run"
+ source = "./fabric/modules/cloud-run"
project_id = "my-project"
name = "hello"
containers = [{
@@ -214,21 +214,21 @@ module "cloud_run" {
|---|---|:---:|:---:|:---:|
| [containers](variables.tf#L27) | Containers. | list(object({…}))
| ✓ | |
| [name](variables.tf#L77) | Name used for cloud run service. | string
| ✓ | |
-| [project_id](variables.tf#L88) | Project id used for all resources. | string
| ✓ | |
+| [project_id](variables.tf#L92) | Project id used for all resources. | string
| ✓ | |
| [audit_log_triggers](variables.tf#L18) | Event arc triggers (Audit log). | list(object({…}))
| | null
|
| [iam](variables.tf#L59) | IAM bindings for Cloud Run service in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
| [ingress_settings](variables.tf#L65) | Ingress settings. | string
| | null
|
| [labels](variables.tf#L71) | Resource labels. | map(string)
| | {}
|
| [prefix](variables.tf#L82) | Optional prefix used for resource names. | string
| | null
|
-| [pubsub_triggers](variables.tf#L93) | Eventarc triggers (Pub/Sub). | list(string)
| | null
|
-| [region](variables.tf#L99) | Region used for all resources. | string
| | "europe-west1"
|
-| [revision_name](variables.tf#L105) | Revision name. | string
| | null
|
-| [service_account](variables.tf#L111) | Service account email. Unused if service account is auto-created. | string
| | null
|
-| [service_account_create](variables.tf#L117) | Auto-create service account. | bool
| | false
|
-| [traffic](variables.tf#L123) | Traffic. | map(number)
| | null
|
-| [volumes](variables.tf#L129) | Volumes. | list(object({…}))
| | null
|
-| [vpc_connector](variables.tf#L142) | VPC connector configuration. Set create to 'true' if a new connecto needs to be created. | object({…})
| | null
|
-| [vpc_connector_config](variables.tf#L152) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…})
| | null
|
+| [pubsub_triggers](variables.tf#L97) | Eventarc triggers (Pub/Sub). | list(string)
| | null
|
+| [region](variables.tf#L103) | Region used for all resources. | string
| | "europe-west1"
|
+| [revision_annotations](variables.tf#L109) | Configure revision template annotations. | object({…})
| | null
|
+| [revision_name](variables.tf#L123) | Revision name. | string
| | null
|
+| [service_account](variables.tf#L129) | Service account email. Unused if service account is auto-created. | string
| | null
|
+| [service_account_create](variables.tf#L135) | Auto-create service account. | bool
| | false
|
+| [traffic](variables.tf#L141) | Traffic. | map(number)
| | null
|
+| [volumes](variables.tf#L147) | Volumes. | list(object({…}))
| | null
|
+| [vpc_connector_create](variables.tf#L160) | Populate this to create a VPC connector. You can then refer to it in the template annotations. | object({…})
| | null
|
## Outputs
diff --git a/modules/cloud-run/main.tf b/modules/cloud-run/main.tf
index d471f42e04..52267d37a9 100644
--- a/modules/cloud-run/main.tf
+++ b/modules/cloud-run/main.tf
@@ -15,7 +15,47 @@
*/
locals {
+ _vpcaccess_annotation = (
+ local.vpc_connector_create
+ ? {
+ "run.googleapis.com/vpc-access-connector" = google_vpc_access_connector.connector.0.id
+ }
+ : (
+ try(var.revision_annotations.vpcaccess_connector, null) == null
+ ? {}
+ : {
+ "run.googleapis.com/vpc-access-connector" = var.revision_annotations.vpcaccess_connector
+ }
+ )
+ )
+ annotations = merge(
+ var.ingress_settings == null ? {} : {
+ "run.googleapis.com/ingress" = var.ingress_settings
+ }
+ )
prefix = var.prefix == null ? "" : "${var.prefix}-"
+ revision_annotations = merge(
+ try(var.revision_annotations.autoscaling.max_scale, null) == null ? {} : {
+ "autoscaling.knative.dev/maxScale" = var.revision_annotations.autoscaling.max_scale
+ },
+ try(var.revision_annotations.autoscaling.min_scale, null) == null ? {} : {
+ "autoscaling.knative.dev/minScale" = var.revision_annotations.autoscaling.min_scale
+ },
+ try(var.revision_annotations.cloudsql_instances, null) == null ? {} : {
+ "run.googleapis.com/cloudsql-instances" = join(",", coalesce(
+ var.revision_annotations.cloudsql_instances, []
+ ))
+ },
+ local._vpcaccess_annotation,
+ try(var.revision_annotations.vpcaccess_egress, null) == null ? {} : {
+ "run.googleapis.com/vpc-access-egress" = var.revision_annotations.vpcaccess_egress
+ },
+ )
+ revision_name = (
+ try(var.revision_name, null) == null
+ ? null
+ : "${var.name}-${var.revision_name}"
+ )
service_account_email = (
var.service_account_create
? (
@@ -25,26 +65,16 @@ locals {
)
: var.service_account
)
-
- annotations = merge(var.ingress_settings == null ? {} : { "run.googleapis.com/ingress" = var.ingress_settings },
- var.vpc_connector == null
- ? {}
- : try(var.vpc_connector.create, false)
- ? { "run.googleapis.com/vpc-access-connector" = var.vpc_connector.name }
- : { "run.googleapis.com/vpc-access-connector" = google_vpc_access_connector.connector.0.id }
- ,
- try(var.vpc_connector.egress_settings, null) == null
- ? {}
- : { "run.googleapis.com/vpc-access-egress" = var.vpc_connector.egress_settings })
+ vpc_connector_create = var.vpc_connector_create != null
}
resource "google_vpc_access_connector" "connector" {
- count = try(var.vpc_connector.create, false) == false ? 0 : 1
+ count = local.vpc_connector_create ? 1 : 0
project = var.project_id
- name = var.vpc_connector.name
+ name = var.vpc_connector_create.name
region = var.region
- ip_cidr_range = var.vpc_connector_config.ip_cidr_range
- network = var.vpc_connector_config.network
+ ip_cidr_range = var.vpc_connector_create.ip_cidr_range
+ network = var.vpc_connector_create.vpc_self_link
}
resource "google_cloud_run_service" "service" {
@@ -56,47 +86,68 @@ resource "google_cloud_run_service" "service" {
template {
spec {
dynamic "containers" {
- for_each = var.containers == null ? {} : { for i, container in var.containers : i => container }
+ for_each = var.containers == null ? {} : {
+ for i, container in var.containers : i => container
+ }
content {
- image = containers.value["image"]
- command = try(containers.value["options"]["command"], null)
- args = try(containers.value["options"]["args"], null)
+ image = containers.value.image
+ command = try(containers.value.options.command, null)
+ args = try(containers.value.options.args, null)
dynamic "env" {
- for_each = try(containers.value["options"]["env"], null) == null ? {} : containers.value["options"]["env"]
+ for_each = (
+ try(containers.value.options.env, null) == null
+ ? {}
+ : containers.value.options.env
+ )
content {
name = env.key
value = env.value
}
}
dynamic "env" {
- for_each = try(containers.value["options"]["env_from"], null) == null ? {} : containers.value["options"]["env_from"]
+ for_each = (
+ try(containers.value.options.env_from, null) == null
+ ? {}
+ : containers.value.options.env_from
+ )
content {
name = env.key
value_from {
secret_key_ref {
- name = env.value["name"]
- key = env.value["key"]
+ name = env.value.name
+ key = env.value.key
}
}
}
}
dynamic "ports" {
- for_each = containers.value["ports"] == null ? {} : { for port in containers.value["ports"] : "${port.name}-${port.container_port}" => port }
+ for_each = (
+ containers.value.ports == null
+ ? {}
+ : {
+ for port in containers.value.ports :
+ "${port.name}-${port.container_port}" => port
+ }
+ )
content {
- name = ports.value["name"]
- protocol = ports.value["protocol"]
- container_port = ports.value["container_port"]
+ name = ports.value.name
+ protocol = ports.value.protocol
+ container_port = ports.value.container_port
}
}
dynamic "resources" {
- for_each = containers.value["resources"] == null ? [] : [""]
+ for_each = containers.value.resources == null ? [] : [""]
content {
- limits = containers.value["resources"]["limits"]
- requests = containers.value["resources"]["requests"]
+ limits = containers.value.resources.limits
+ requests = containers.value.resources.requests
}
}
dynamic "volume_mounts" {
- for_each = containers.value["volume_mounts"] == null ? {} : containers.value["volume_mounts"]
+ for_each = (
+ containers.value.volume_mounts == null
+ ? {}
+ : containers.value.volume_mounts
+ )
content {
name = volume_mounts.key
mount_path = volume_mounts.value
@@ -108,29 +159,28 @@ resource "google_cloud_run_service" "service" {
dynamic "volumes" {
for_each = var.volumes == null ? [] : var.volumes
content {
- name = volumes.value["name"]
+ name = volumes.value.name
secret {
- secret_name = volumes.value["secret_name"]
+ secret_name = volumes.value.secret_name
dynamic "items" {
- for_each = volumes.value["items"] == null ? [] : volumes.value["items"]
+ for_each = (
+ volumes.value.items == null ? [] : volumes.value.items
+ )
content {
- key = items.value["key"]
- path = items.value["path"]
+ key = items.value.key
+ path = items.value.path
}
}
}
}
}
}
- dynamic "metadata" {
- for_each = var.revision_name == null ? [] : [""]
- content {
- name = "${var.name}-${var.revision_name}"
- }
+ metadata {
+ name = local.revision_name
+ annotations = local.revision_annotations
}
}
-
metadata {
annotations = local.annotations
}
@@ -162,7 +212,10 @@ resource "google_service_account" "service_account" {
}
resource "google_eventarc_trigger" "audit_log_triggers" {
- for_each = var.audit_log_triggers == null ? {} : { for trigger in var.audit_log_triggers : "${trigger.service_name}-${trigger.method_name}" => trigger }
+ for_each = var.audit_log_triggers == null ? {} : {
+ for trigger in var.audit_log_triggers :
+ "${trigger.service_name}-${trigger.method_name}" => trigger
+ }
name = "${local.prefix}${each.key}-audit-log-trigger"
location = google_cloud_run_service.service.location
project = google_cloud_run_service.service.project
@@ -172,11 +225,11 @@ resource "google_eventarc_trigger" "audit_log_triggers" {
}
matching_criteria {
attribute = "serviceName"
- value = each.value["service_name"]
+ value = each.value.service_name
}
matching_criteria {
attribute = "methodName"
- value = each.value["method_name"]
+ value = each.value.method_name
}
destination {
cloud_run_service {
@@ -188,7 +241,11 @@ resource "google_eventarc_trigger" "audit_log_triggers" {
resource "google_eventarc_trigger" "pubsub_triggers" {
for_each = var.pubsub_triggers == null ? [] : toset(var.pubsub_triggers)
- name = each.value == "" ? "${local.prefix}default-pubsub-trigger" : "${local.prefix}${each.value}-pubsub-trigger"
+ name = (
+ each.value == ""
+ ? "${local.prefix}default-pubsub-trigger"
+ : "${local.prefix}${each.value}-pubsub-trigger"
+ )
location = google_cloud_run_service.service.location
project = google_cloud_run_service.service.project
matching_criteria {
diff --git a/modules/cloud-run/variables.tf b/modules/cloud-run/variables.tf
index 81777d9af3..8029f1c411 100644
--- a/modules/cloud-run/variables.tf
+++ b/modules/cloud-run/variables.tf
@@ -83,6 +83,10 @@ variable "prefix" {
description = "Optional prefix used for resource names."
type = string
default = null
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty, please use null instead."
+ }
}
variable "project_id" {
@@ -102,6 +106,20 @@ variable "region" {
default = "europe-west1"
}
+variable "revision_annotations" {
+ description = "Configure revision template annotations."
+ type = object({
+ autoscaling = object({
+ max_scale = number
+ min_scale = number
+ })
+ cloudsql_instances = list(string)
+ vpcaccess_connector = string
+ vpcaccess_egress = string
+ })
+ default = null
+}
+
variable "revision_name" {
description = "Revision name."
type = string
@@ -139,21 +157,12 @@ variable "volumes" {
default = null
}
-variable "vpc_connector" {
- description = "VPC connector configuration. Set create to 'true' if a new connecto needs to be created."
- type = object({
- create = bool
- name = string
- egress_settings = string
- })
- default = null
-}
-
-variable "vpc_connector_config" {
- description = "VPC connector network configuration. Must be provided if new VPC connector is being created."
+variable "vpc_connector_create" {
+ description = "Populate this to create a VPC connector. You can then refer to it in the template annotations."
type = object({
ip_cidr_range = string
- network = string
+ name = string
+ vpc_self_link = string
})
default = null
}
diff --git a/modules/cloud-run/versions.tf b/modules/cloud-run/versions.tf
index b709800c02..90b632f6d4 100644
--- a/modules/cloud-run/versions.tf
+++ b/modules/cloud-run/versions.tf
@@ -1,20 +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
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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
+#
+# 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 = ">= 0.12.6"
+ required_version = ">= 1.3.1"
+ required_providers {
+ google = {
+ source = "hashicorp/google"
+ version = ">= 4.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
}
+
+
diff --git a/modules/cloudsql-instance/README.md b/modules/cloudsql-instance/README.md
index 31fe491624..92fc189437 100644
--- a/modules/cloudsql-instance/README.md
+++ b/modules/cloudsql-instance/README.md
@@ -12,7 +12,7 @@ This example shows how to setup a project, VPC and a standalone Cloud SQL instan
```hcl
module "project" {
- source = "./modules/project"
+ source = "./fabric/modules/project"
billing_account = var.billing_account_id
parent = var.organization_id
name = "my-db-project"
@@ -22,7 +22,7 @@ module "project" {
}
module "vpc" {
- source = "./modules/net-vpc"
+ source = "./fabric/modules/net-vpc"
project_id = module.project.project_id
name = "my-network"
psa_config = {
@@ -32,7 +32,7 @@ module "vpc" {
}
module "db" {
- source = "./modules/cloudsql-instance"
+ source = "./fabric/modules/cloudsql-instance"
project_id = module.project.project_id
network = module.vpc.self_link
name = "db"
@@ -47,7 +47,7 @@ module "db" {
```hcl
module "db" {
- source = "./modules/cloudsql-instance"
+ source = "./fabric/modules/cloudsql-instance"
project_id = var.project_id
network = var.vpc.self_link
name = "db"
@@ -56,8 +56,8 @@ module "db" {
tier = "db-g1-small"
replicas = {
- replica1 = "europe-west3"
- replica2 = "us-central1"
+ replica1 = { region = "europe-west3", encryption_key_name = null }
+ replica2 = { region = "us-central1", encryption_key_name = null }
}
}
# tftest modules=1 resources=3
@@ -67,7 +67,7 @@ module "db" {
```hcl
module "db" {
- source = "./modules/cloudsql-instance"
+ source = "./fabric/modules/cloudsql-instance"
project_id = var.project_id
network = var.vpc.self_link
name = "db"
@@ -88,35 +88,87 @@ module "db" {
# generatea password for user1
user1 = null
# assign a password to user2
- user2 = "mypassword"
+ user2 = "mypassword"
}
}
# tftest modules=1 resources=6
```
+
+### CMEK encryption
+```hcl
+
+module "project" {
+ source = "./fabric/modules/project"
+ billing_account = var.billing_account_id
+ parent = var.organization_id
+ name = "my-db-project"
+ services = [
+ "servicenetworking.googleapis.com",
+ "sqladmin.googleapis.com",
+ ]
+}
+
+module "kms" {
+ source = "./fabric/modules/kms"
+ project_id = module.project.project_id
+ keyring = {
+ name = "keyring"
+ location = var.region
+ }
+ keys = {
+ key-sql = null
+ }
+ key_iam = {
+ key-sql = {
+ "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [
+ "serviceAccount:${module.project.service_accounts.robots.sqladmin}"
+ ]
+ }
+ }
+}
+
+module "db" {
+ source = "./fabric/modules/cloudsql-instance"
+ project_id = module.project.project_id
+ encryption_key_name = module.kms.keys["key-sql"].id
+ network = var.vpc.self_link
+ name = "db"
+ region = var.region
+ database_version = "POSTGRES_13"
+ tier = "db-g1-small"
+}
+
+# tftest modules=3 resources=10
+```
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [database_version](variables.tf#L50) | Database type and version to create. | string
| ✓ | |
-| [name](variables.tf#L91) | Name of primary replica. | string
| ✓ | |
-| [network](variables.tf#L96) | 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#L107) | The ID of the project where this instances will be created. | string
| ✓ | |
-| [region](variables.tf#L112) | Region of the primary replica. | string
| ✓ | |
-| [tier](variables.tf#L123) | 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#L55) | Databases to create once the primary instance is created. | list(string)
| | null
|
-| [deletion_protection](variables.tf#L61) | Allow terraform to delete instances. | bool
| | false
|
-| [disk_size](variables.tf#L67) | Disk size in GB. Set to null to enable autoresize. | number
| | null
|
-| [disk_type](variables.tf#L73) | The type of data disk: `PD_SSD` or `PD_HDD`. | string
| | "PD_SSD"
|
-| [flags](variables.tf#L79) | Map FLAG_NAME=>VALUE for database-specific tuning. | map(string)
| | null
|
-| [labels](variables.tf#L85) | Labels to be attached to all instances. | map(string)
| | null
|
-| [prefix](variables.tf#L101) | Prefix used to generate instance names. | string
| | null
|
-| [replicas](variables.tf#L117) | Map of NAME=>REGION for additional read replicas. Set to null to disable replica creation. | map(any)
| | null
|
-| [users](variables.tf#L128) | 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#L58) | Database type and version to create. | string
| ✓ | |
+| [name](variables.tf#L111) | Name of primary instance. | string
| ✓ | |
+| [network](variables.tf#L116) | 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#L137) | The ID of the project where this instances will be created. | string
| ✓ | |
+| [region](variables.tf#L142) | Region of the primary instance. | string
| ✓ | |
+| [tier](variables.tf#L162) | 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#L63) | Databases to create once the primary instance is created. | list(string)
| | null
|
+| [deletion_protection](variables.tf#L69) | Allow terraform to delete instances. | bool
| | false
|
+| [disk_size](variables.tf#L75) | Disk size in GB. Set to null to enable autoresize. | number
| | null
|
+| [disk_type](variables.tf#L81) | The type of data disk: `PD_SSD` or `PD_HDD`. | string
| | "PD_SSD"
|
+| [encryption_key_name](variables.tf#L87) | The full path to the encryption key used for the CMEK disk encryption of the primary instance. | string
| | null
|
+| [flags](variables.tf#L93) | Map FLAG_NAME=>VALUE for database-specific tuning. | map(string)
| | null
|
+| [ipv4_enabled](variables.tf#L99) | Add a public IP address to database instance. | bool
| | false
|
+| [labels](variables.tf#L105) | Labels to be attached to all instances. | map(string)
| | null
|
+| [postgres_client_certificates](variables.tf#L121) | Map of cert keys connect to the application(s) using public IP. | list(string)
| | null
|
+| [prefix](variables.tf#L127) | Optional prefix used to generate instance names. | string
| | null
|
+| [replicas](variables.tf#L147) | Map of NAME=> {REGION, KMS_KEY} for additional read replicas. Set to null to disable replica creation. | map(object({…}))
| | {}
|
+| [root_password](variables.tf#L156) | Root password of the Cloud SQL instance. Required for MS SQL Server. | string
| | null
|
+| [users](variables.tf#L167) | 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
@@ -129,8 +181,11 @@ module "db" {
| [instances](outputs.tf#L50) | Cloud SQL instance resources. | ✓ |
| [ip](outputs.tf#L56) | IP address of the primary instance. | |
| [ips](outputs.tf#L61) | IP addresses of all instances. | |
-| [self_link](outputs.tf#L69) | Self link of the primary instance. | |
-| [self_links](outputs.tf#L74) | Self links of all instances. | |
-| [user_passwords](outputs.tf#L82) | Map of containing the password of all users created through terraform. | ✓ |
+| [name](outputs.tf#L69) | Name of the primary instance. | |
+| [names](outputs.tf#L74) | Names of all instances. | |
+| [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 f50121abc5..ebfc3c42a6 100644
--- a/modules/cloudsql-instance/main.tf
+++ b/modules/cloudsql-instance/main.tf
@@ -18,6 +18,11 @@ locals {
prefix = var.prefix == null ? "" : "${var.prefix}-"
is_mysql = can(regex("^MYSQL", var.database_version))
has_replicas = try(length(var.replicas) > 0, false)
+ is_regional = var.availability_type == "REGIONAL" ? true : false
+
+ // Enable backup if the user asks for it or if the user is deploying
+ // MySQL in HA configuration (regional or with specified replicas)
+ enable_backup = var.backup_configuration.enabled || (local.is_mysql && local.has_replicas) || (local.is_mysql && local.is_regional)
users = {
for user, password in coalesce(var.users, {}) :
@@ -39,10 +44,13 @@ locals {
}
resource "google_sql_database_instance" "primary" {
- project = var.project_id
- name = "${local.prefix}${var.name}"
- region = var.region
- database_version = var.database_version
+ provider = google-beta
+ project = var.project_id
+ name = "${local.prefix}${var.name}"
+ region = var.region
+ database_version = var.database_version
+ encryption_key_name = var.encryption_key_name
+ root_password = var.root_password
settings {
tier = var.tier
@@ -53,8 +61,9 @@ resource "google_sql_database_instance" "primary" {
user_labels = var.labels
ip_configuration {
- ipv4_enabled = false
- 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
@@ -65,24 +74,25 @@ resource "google_sql_database_instance" "primary" {
}
}
- backup_configuration {
- // Enable backup if the user asks for it or if the user is
- // deploying MySQL with replicas
- enabled = var.backup_configuration.enabled || (local.is_mysql && local.has_replicas)
-
- // enable binary log if the user asks for it or we have replicas,
- // but only form MySQL
- binary_log_enabled = (
- local.is_mysql
- ? var.backup_configuration.binary_log_enabled || local.has_replicas
- : null
- )
- start_time = var.backup_configuration.start_time
- location = var.backup_configuration.location
- transaction_log_retention_days = var.backup_configuration.log_retention_days
- backup_retention_settings {
- retained_backups = var.backup_configuration.retention_count
- retention_unit = "COUNT"
+ dynamic "backup_configuration" {
+ for_each = local.enable_backup ? { 1 = 1 } : {}
+ content {
+ enabled = true
+
+ // enable binary log if the user asks for it or we have replicas (default in regional),
+ // but only for MySQL
+ binary_log_enabled = (
+ local.is_mysql
+ ? var.backup_configuration.binary_log_enabled || local.has_replicas || local.is_regional
+ : null
+ )
+ start_time = var.backup_configuration.start_time
+ location = var.backup_configuration.location
+ transaction_log_retention_days = var.backup_configuration.log_retention_days
+ backup_retention_settings {
+ retained_backups = var.backup_configuration.retention_count
+ retention_unit = "COUNT"
+ }
}
}
@@ -99,11 +109,13 @@ resource "google_sql_database_instance" "primary" {
}
resource "google_sql_database_instance" "replicas" {
+ provider = google-beta
for_each = local.has_replicas ? var.replicas : {}
project = var.project_id
name = "${local.prefix}${each.key}"
- region = each.value
+ region = each.value.region
database_version = var.database_version
+ encryption_key_name = each.value.encryption_key_name
master_instance_name = google_sql_database_instance.primary.name
settings {
@@ -115,8 +127,9 @@ resource "google_sql_database_instance" "replicas" {
user_labels = var.labels
ip_configuration {
- ipv4_enabled = false
- 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
@@ -164,3 +177,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 e2ca316d6c..8c814c06a1 100644
--- a/modules/cloudsql-instance/outputs.tf
+++ b/modules/cloudsql-instance/outputs.tf
@@ -66,6 +66,25 @@ output "ips" {
}
}
+output "name" {
+ description = "Name of the primary instance."
+ value = google_sql_database_instance.primary.name
+}
+
+output "names" {
+ description = "Names of all instances."
+ value = {
+ for id, instance in local._all_intances :
+ id => instance.name
+ }
+}
+
+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 148acee6fb..538fd1fea1 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)
@@ -40,13 +49,12 @@ variable "backup_configuration" {
enabled = false
binary_log_enabled = false
start_time = "23:00"
- location = "EU"
+ location = null
log_retention_days = 7
retention_count = 7
}
}
-
variable "database_version" {
description = "Database type and version to create."
type = string
@@ -76,12 +84,24 @@ variable "disk_type" {
default = "PD_SSD"
}
+variable "encryption_key_name" {
+ description = "The full path to the encryption key used for the CMEK disk encryption of the primary instance."
+ type = string
+ default = null
+}
+
variable "flags" {
description = "Map FLAG_NAME=>VALUE for database-specific tuning."
type = map(string)
default = null
}
+variable "ipv4_enabled" {
+ description = "Add a public IP address to database instance."
+ type = bool
+ default = false
+}
+
variable "labels" {
description = "Labels to be attached to all instances."
type = map(string)
@@ -89,7 +109,7 @@ variable "labels" {
}
variable "name" {
- description = "Name of primary replica."
+ description = "Name of primary instance."
type = string
}
@@ -98,10 +118,20 @@ 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 = "Prefix used to generate instance names."
+ description = "Optional prefix used to generate instance names."
type = string
default = null
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty, please use null instead."
+ }
}
variable "project_id" {
@@ -110,13 +140,22 @@ variable "project_id" {
}
variable "region" {
- description = "Region of the primary replica."
+ description = "Region of the primary instance."
type = string
}
variable "replicas" {
- description = "Map of NAME=>REGION for additional read replicas. Set to null to disable replica creation."
- type = map(any)
+ description = "Map of NAME=> {REGION, KMS_KEY} for additional read replicas. Set to null to disable replica creation."
+ type = map(object({
+ region = string
+ encryption_key_name = string
+ }))
+ default = {}
+}
+
+variable "root_password" {
+ description = "Root password of the Cloud SQL instance. Required for MS SQL Server."
+ type = string
default = null
}
diff --git a/modules/cloudsql-instance/versions.tf b/modules/cloudsql-instance/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/cloudsql-instance/versions.tf
+++ b/modules/cloudsql-instance/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/compute-mig/README.md b/modules/compute-mig/README.md
index 92bec4e802..8bdb233868 100644
--- a/modules/compute-mig/README.md
+++ b/modules/compute-mig/README.md
@@ -2,7 +2,7 @@
This module allows creating a managed instance group supporting one or more application versions via instance templates. Optionally, a health check and an autoscaler can be created, and the managed instance group can be configured to be stateful.
-This module can be coupled with the [`compute-vm`](../compute-vm) module which can manage instance templates, and the [`net-ilb`](../net-ilb) module to assign the MIG to a backend wired to an Internal Load Balancer. The first use case is shown in the examples below.
+This module can be coupled with the [`compute-vm`](../compute-vm) module which can manage instance templates, and the [`net-ilb`](../net-ilb) module to assign the MIG to a backend wired to an Internal Load Balancer. The first use case is shown in the examples below.
Stateful disks can be created directly, as shown in the last example below.
@@ -12,14 +12,14 @@ This example shows how to manage a simple MIG that leverages the `compute-vm` mo
```hcl
module "cos-nginx" {
- source = "./modules/cloud-config-container/nginx"
+ source = "./fabric/modules/cloud-config-container/nginx"
}
module "nginx-template" {
- source = "./modules/compute-vm"
+ source = "./fabric/modules/compute-vm"
project_id = var.project_id
name = "nginx-template"
- zone = "europe-west1-b"
+ zone = "europe-west1-b"
tags = ["http-server", "ssh"]
network_interfaces = [{
network = var.vpc.self_link
@@ -39,15 +39,12 @@ module "nginx-template" {
}
module "nginx-mig" {
- source = "./modules/compute-mig"
- project_id = "my-project"
- location = "europe-west1-b"
- name = "mig-test"
- target_size = 2
- default_version = {
- instance_template = module.nginx-template.template.self_link
- name = "default"
- }
+ source = "./fabric/modules/compute-mig"
+ project_id = "my-project"
+ location = "europe-west1-b"
+ name = "mig-test"
+ target_size = 2
+ instance_template = module.nginx-template.template.self_link
}
# tftest modules=2 resources=2
```
@@ -58,14 +55,14 @@ If multiple versions are desired, use more `compute-vm` instances for the additi
```hcl
module "cos-nginx" {
- source = "./modules/cloud-config-container/nginx"
+ source = "./fabric/modules/cloud-config-container/nginx"
}
module "nginx-template" {
- source = "./modules/compute-vm"
+ source = "./fabric/modules/compute-vm"
project_id = var.project_id
name = "nginx-template"
- zone = "europe-west1-b"
+ zone = "europe-west1-b"
tags = ["http-server", "ssh"]
network_interfaces = [{
network = var.vpc.self_link
@@ -78,27 +75,25 @@ module "nginx-template" {
type = "pd-ssd"
size = 10
}
- create_template = true
+ create_template = true
metadata = {
user-data = module.cos-nginx.cloud_config
}
}
module "nginx-mig" {
- source = "./modules/compute-mig"
- project_id = "my-project"
- location = "europe-west1-b"
- name = "mig-test"
- target_size = 3
- default_version = {
- instance_template = module.nginx-template.template.self_link
- name = "default"
- }
+ source = "./fabric/modules/compute-mig"
+ project_id = "my-project"
+ location = "europe-west1-b"
+ name = "mig-test"
+ target_size = 3
+ instance_template = module.nginx-template.template.self_link
versions = {
canary = {
instance_template = module.nginx-template.template.self_link
- target_type = "fixed"
- target_size = 1
+ target_size = {
+ fixed = 1
+ }
}
}
}
@@ -111,14 +106,14 @@ Autohealing policies can use an externally defined health check, or have this mo
```hcl
module "cos-nginx" {
- source = "./modules/cloud-config-container/nginx"
+ source = "./fabric/modules/cloud-config-container/nginx"
}
module "nginx-template" {
- source = "./modules/compute-vm"
+ source = "./fabric/modules/compute-vm"
project_id = var.project_id
name = "nginx-template"
- zone = "europe-west1-b"
+ zone = "europe-west1-b"
tags = ["http-server", "ssh"]
network_interfaces = [{
network = var.vpc.self_link,
@@ -131,31 +126,27 @@ module "nginx-template" {
type = "pd-ssd"
size = 10
}
- create_template = true
+ create_template = true
metadata = {
user-data = module.cos-nginx.cloud_config
}
}
module "nginx-mig" {
- source = "./modules/compute-mig"
- project_id = "my-project"
- location = "europe-west1-b"
- name = "mig-test"
- target_size = 3
- default_version = {
- instance_template = module.nginx-template.template.self_link
- name = "default"
- }
+ source = "./fabric/modules/compute-mig"
+ project_id = "my-project"
+ location = "europe-west1-b"
+ name = "mig-test"
+ target_size = 3
+ instance_template = module.nginx-template.template.self_link
auto_healing_policies = {
- health_check = module.nginx-mig.health_check.self_link
initial_delay_sec = 30
}
health_check_config = {
- type = "http"
- check = { port = 80 }
- config = {}
- logging = true
+ enable_logging = true
+ http = {
+ port = 80
+ }
}
}
# tftest modules=2 resources=3
@@ -167,14 +158,14 @@ The module can create and manage an autoscaler associated with the MIG. When usi
```hcl
module "cos-nginx" {
- source = "./modules/cloud-config-container/nginx"
+ source = "./fabric/modules/cloud-config-container/nginx"
}
module "nginx-template" {
- source = "./modules/compute-vm"
+ source = "./fabric/modules/compute-vm"
project_id = var.project_id
name = "nginx-template"
- zone = "europe-west1-b"
+ zone = "europe-west1-b"
tags = ["http-server", "ssh"]
network_interfaces = [{
network = var.vpc.self_link
@@ -187,29 +178,28 @@ module "nginx-template" {
type = "pd-ssd"
size = 10
}
- create_template = true
+ create_template = true
metadata = {
user-data = module.cos-nginx.cloud_config
}
}
module "nginx-mig" {
- source = "./modules/compute-mig"
- project_id = "my-project"
- location = "europe-west1-b"
- name = "mig-test"
- target_size = 3
- default_version = {
- instance_template = module.nginx-template.template.self_link
- name = "default"
- }
+ source = "./fabric/modules/compute-mig"
+ project_id = "my-project"
+ location = "europe-west1-b"
+ name = "mig-test"
+ target_size = 3
+ instance_template = module.nginx-template.template.self_link
autoscaler_config = {
- max_replicas = 3
- min_replicas = 1
- cooldown_period = 30
- cpu_utilization_target = 0.65
- load_balancing_utilization_target = null
- metric = null
+ max_replicas = 3
+ min_replicas = 1
+ cooldown_period = 30
+ scaling_signals = {
+ cpu_utilization = {
+ target = 0.65
+ }
+ }
}
}
# tftest modules=2 resources=3
@@ -219,14 +209,14 @@ module "nginx-mig" {
```hcl
module "cos-nginx" {
- source = "./modules/cloud-config-container/nginx"
+ source = "./fabric/modules/cloud-config-container/nginx"
}
module "nginx-template" {
- source = "./modules/compute-vm"
+ source = "./fabric/modules/compute-vm"
project_id = var.project_id
name = "nginx-template"
- zone = "europe-west1-b"
+ zone = "europe-west1-b"
tags = ["http-server", "ssh"]
network_interfaces = [{
network = var.vpc.self_link
@@ -239,30 +229,26 @@ module "nginx-template" {
type = "pd-ssd"
size = 10
}
- create_template = true
+ create_template = true
metadata = {
user-data = module.cos-nginx.cloud_config
}
}
module "nginx-mig" {
- source = "./modules/compute-mig"
- project_id = "my-project"
- location = "europe-west1-b"
- name = "mig-test"
- target_size = 3
- default_version = {
- instance_template = module.nginx-template.template.self_link
- name = "default"
- }
+ source = "./fabric/modules/compute-mig"
+ project_id = "my-project"
+ location = "europe-west1-b"
+ name = "mig-test"
+ target_size = 3
+ instance_template = module.nginx-template.template.self_link
update_policy = {
- type = "PROACTIVE"
- minimal_action = "REPLACE"
- min_ready_sec = 30
- max_surge_type = "fixed"
- max_surge = 1
- max_unavailable_type = null
- max_unavailable = null
+ minimal_action = "REPLACE"
+ type = "PROACTIVE"
+ min_ready_sec = 30
+ max_surge = {
+ fixed = 1
+ }
}
}
# tftest modules=2 resources=2
@@ -270,7 +256,7 @@ module "nginx-mig" {
### Stateful MIGs - MIG Config
-Stateful MIGs have some limitations documented [here](https://cloud.google.com/compute/docs/instance-groups/configuring-stateful-migs#limitations). Enforcement of these requirements is the responsibility of users of this module.
+Stateful MIGs have some limitations documented [here](https://cloud.google.com/compute/docs/instance-groups/configuring-stateful-migs#limitations). Enforcement of these requirements is the responsibility of users of this module.
You can configure a disk defined in the instance template to be stateful for all instances in the MIG by configuring in the MIG's stateful policy, using the `stateful_disk_mig` variable. Alternatively, you can also configure stateful persistent disks individually per instance of the MIG by setting the `stateful_disk_instance` variable. A discussion on these scenarios can be found in the [docs](https://cloud.google.com/compute/docs/instance-groups/configuring-stateful-disks-in-migs).
@@ -278,17 +264,16 @@ An example using only the configuration at the MIG level can be seen below.
Note that when referencing the stateful disk, you use `device_name` and not `disk_name`.
-
```hcl
module "cos-nginx" {
- source = "./modules/cloud-config-container/nginx"
+ source = "./fabric/modules/cloud-config-container/nginx"
}
module "nginx-template" {
- source = "./modules/compute-vm"
+ source = "./fabric/modules/compute-vm"
project_id = var.project_id
name = "nginx-template"
- zone = "europe-west1-b"
+ zone = "europe-west1-b"
tags = ["http-server", "ssh"]
network_interfaces = [{
network = var.vpc.self_link
@@ -312,60 +297,53 @@ module "nginx-template" {
type = "PERSISTENT"
}
}]
- create_template = true
+ create_template = true
metadata = {
user-data = module.cos-nginx.cloud_config
}
}
module "nginx-mig" {
- source = "./modules/compute-mig"
- project_id = "my-project"
- location = "europe-west1-b"
- name = "mig-test"
- target_size = 3
- default_version = {
- instance_template = module.nginx-template.template.self_link
- name = "default"
- }
+ source = "./fabric/modules/compute-mig"
+ project_id = "my-project"
+ location = "europe-west1-b"
+ name = "mig-test"
+ target_size = 3
+ instance_template = module.nginx-template.template.self_link
autoscaler_config = {
- max_replicas = 3
- min_replicas = 1
- cooldown_period = 30
- cpu_utilization_target = 0.65
- load_balancing_utilization_target = null
- metric = null
- }
- stateful_config = {
- per_instance_config = {},
- mig_config = {
- stateful_disks = {
- persistent-disk-1 = {
- delete_rule = "NEVER"
- }
+ max_replicas = 3
+ min_replicas = 1
+ cooldown_period = 30
+ scaling_signals = {
+ cpu_utilization = {
+ target = 0.65
}
}
}
+ stateful_disks = {
+ repd-1 = false
+ }
}
# tftest modules=2 resources=3
```
### Stateful MIGs - Instance Config
-Here is an example defining the stateful config at the instance level.
+
+Here is an example defining the stateful config at the instance level.
Note that you will need to know the instance name in order to use this configuration.
```hcl
module "cos-nginx" {
- source = "./modules/cloud-config-container/nginx"
+ source = "./fabric/modules/cloud-config-container/nginx"
}
module "nginx-template" {
- source = "./modules/compute-vm"
+ source = "./fabric/modules/compute-vm"
project_id = var.project_id
name = "nginx-template"
- zone = "europe-west1-b"
+ zone = "europe-west1-b"
tags = ["http-server", "ssh"]
network_interfaces = [{
network = var.vpc.self_link
@@ -389,53 +367,43 @@ module "nginx-template" {
type = "PERSISTENT"
}
}]
- create_template = true
+ create_template = true
metadata = {
user-data = module.cos-nginx.cloud_config
}
}
module "nginx-mig" {
- source = "./modules/compute-mig"
- project_id = "my-project"
- location = "europe-west1-b"
- name = "mig-test"
- target_size = 3
- default_version = {
- instance_template = module.nginx-template.template.self_link
- name = "default"
- }
+ source = "./fabric/modules/compute-mig"
+ project_id = "my-project"
+ location = "europe-west1-b"
+ name = "mig-test"
+ target_size = 3
+ instance_template = module.nginx-template.template.self_link
autoscaler_config = {
- max_replicas = 3
- min_replicas = 1
- cooldown_period = 30
- cpu_utilization_target = 0.65
- load_balancing_utilization_target = null
- metric = null
+ max_replicas = 3
+ min_replicas = 1
+ cooldown_period = 30
+ scaling_signals = {
+ cpu_utilization = {
+ target = 0.65
+ }
+ }
}
stateful_config = {
- per_instance_config = {
- # note that this needs to be the name of an existing instance within the Managed Instance Group
- instance-1 = {
- stateful_disks = {
+ # name needs to match a MIG instance name
+ instance-1 = {
+ minimal_action = "NONE",
+ most_disruptive_allowed_action = "REPLACE"
+ preserved_state = {
+ disks = {
persistent-disk-1 = {
- source = "test-disk",
- mode = "READ_ONLY",
- delete_rule= "NEVER",
- },
- },
+ source = "test-disk",
+ }
+ }
metadata = {
foo = "bar"
- },
- update_config = {
- minimal_action = "NONE",
- most_disruptive_allowed_action = "REPLACE",
- remove_instance_state_on_destroy = false,
- },
- },
- },
- mig_config = {
- stateful_disks = {
+ }
}
}
}
@@ -449,21 +417,25 @@ module "nginx-mig" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [default_version](variables.tf#L45) | Default application version template. Additional versions can be specified via the `versions` variable. | object({…})
| ✓ | |
-| [location](variables.tf#L64) | Compute zone, or region if `regional` is set to true. | string
| ✓ | |
-| [name](variables.tf#L68) | Managed group name. | string
| ✓ | |
-| [project_id](variables.tf#L79) | Project id. | string
| ✓ | |
-| [auto_healing_policies](variables.tf#L17) | Auto-healing policies for this group. | object({…})
| | null
|
-| [autoscaler_config](variables.tf#L26) | Optional autoscaler configuration. Only one of 'cpu_utilization_target' 'load_balancing_utilization_target' or 'metric' can be not null. | object({…})
| | null
|
-| [health_check_config](variables.tf#L53) | 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#L73) | Named ports. | map(number)
| | null
|
-| [regional](variables.tf#L84) | Use regional instance group. When set, `location` should be set to the region. | bool
| | false
|
-| [stateful_config](variables.tf#L90) | Stateful configuration can be done by individual instances or for all instances in the MIG. They key in per_instance_config is the name of the specific instance. The key of the stateful_disks is the 'device_name' field of the resource. Please note that device_name is defined at the OS mount level, unlike the disk name. | object({…})
| | null
|
-| [target_pools](variables.tf#L121) | Optional list of URLs for target pools to which new instances in the group are added. | list(string)
| | []
|
-| [target_size](variables.tf#L127) | Group target size, leave null when using an autoscaler. | number
| | null
|
-| [update_policy](variables.tf#L133) | Update policy. Type can be 'OPPORTUNISTIC' or 'PROACTIVE', action 'REPLACE' or 'restart', surge type 'fixed' or 'percent'. | object({…})
| | null
|
-| [versions](variables.tf#L147) | Additional application versions, target_type is either 'fixed' or 'percent'. | map(object({…}))
| | null
|
-| [wait_for_instances](variables.tf#L157) | Wait for all instances to be created/updated before returning. | bool
| | null
|
+| [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
|
+| [default_version_name](variables.tf#L83) | Name used for the default version. | string
| | "default"
|
+| [description](variables.tf#L89) | Optional description used for all resources managed by this module. | string
| | "Terraform managed."
|
+| [distribution_policy](variables.tf#L95) | DIstribution policy for regional MIG. | object({…})
| | null
|
+| [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#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
@@ -474,6 +446,3 @@ module "nginx-mig" {
| [health_check](outputs.tf#L35) | Auto-created health-check resource. | |
-## TODO
-
-- [✓] add support for instance groups
diff --git a/modules/compute-mig/autoscaler.tf b/modules/compute-mig/autoscaler.tf
new file mode 100644
index 0000000000..b8bd0acc80
--- /dev/null
+++ b/modules/compute-mig/autoscaler.tf
@@ -0,0 +1,229 @@
+/**
+ * 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:file:description Autoscaler resource.
+
+locals {
+ as_enabled = true
+ as_scaling = try(var.autoscaler_config.scaling_control, null)
+ as_signals = try(var.autoscaler_config.scaling_signals, null)
+}
+
+resource "google_compute_autoscaler" "default" {
+ provider = google-beta
+ count = local.is_regional || var.autoscaler_config == null ? 0 : 1
+ project = var.project_id
+ name = var.name
+ zone = var.location
+ description = var.description
+ target = google_compute_instance_group_manager.default.0.id
+
+ autoscaling_policy {
+ max_replicas = var.autoscaler_config.max_replicas
+ min_replicas = var.autoscaler_config.min_replicas
+ cooldown_period = var.autoscaler_config.cooldown_period
+
+ dynamic "scale_down_control" {
+ for_each = local.as_scaling.down == null ? [] : [""]
+ content {
+ time_window_sec = local.as_scaling.down.time_window_sec
+ dynamic "max_scaled_down_replicas" {
+ for_each = (
+ local.as_scaling.down.max_replicas_fixed == null &&
+ local.as_scaling.down.max_replicas_percent == null
+ ? []
+ : [""]
+ )
+ content {
+ fixed = local.as_scaling.down.max_replicas_fixed
+ percent = local.as_scaling.down.max_replicas_percent
+ }
+ }
+ }
+ }
+
+ dynamic "scale_in_control" {
+ for_each = local.as_scaling.in == null ? [] : [""]
+ content {
+ time_window_sec = local.as_scaling.in.time_window_sec
+ dynamic "max_scaled_in_replicas" {
+ for_each = (
+ local.as_scaling.in.max_replicas_fixed == null &&
+ local.as_scaling.in.max_replicas_percent == null
+ ? []
+ : [""]
+ )
+ content {
+ fixed = local.as_scaling.in.max_replicas_fixed
+ percent = local.as_scaling.in.max_replicas_percent
+ }
+ }
+ }
+ }
+
+ dynamic "cpu_utilization" {
+ for_each = local.as_signals.cpu_utilization == null ? [] : [""]
+ content {
+ target = local.as_signals.cpu_utilization.target
+ predictive_method = (
+ local.as_signals.cpu_utilization.optimize_availability == true
+ ? "OPTIMIZE_AVAILABILITY"
+ : null
+ )
+ }
+ }
+
+ dynamic "load_balancing_utilization" {
+ for_each = local.as_signals.load_balancing_utilization == null ? [] : [""]
+ content {
+ target = local.as_signals.load_balancing_utilization.target
+ }
+ }
+
+ dynamic "metric" {
+ for_each = toset(
+ local.as_signals.metrics == null ? [] : local.as_signals.metrics
+ )
+ content {
+ name = metric.value.name
+ type = metric.value.type
+ target = metric.value.target_value
+ single_instance_assignment = metric.value.single_instance_assignment
+ filter = metric.value.time_series_filter
+ }
+ }
+
+ dynamic "scaling_schedules" {
+ for_each = toset(
+ local.as_signals.schedules == null ? [] : local.as_signals.schedules
+ )
+ iterator = schedule
+ content {
+ duration_sec = schedule.value.duration_sec
+ min_required_replicas = schedule.value.min_required_replicas
+ name = schedule.value.name
+ schedule = schedule.value.cron_schedule
+ description = schedule.value.description
+ disabled = schedule.value.disabled
+ time_zone = schedule.value.timezone
+ }
+ }
+
+ }
+}
+
+resource "google_compute_region_autoscaler" "default" {
+ provider = google-beta
+ count = local.is_regional && var.autoscaler_config != null ? 1 : 0
+ project = var.project_id
+ name = var.name
+ region = var.location
+ description = var.description
+ target = google_compute_region_instance_group_manager.default.0.id
+
+ autoscaling_policy {
+ max_replicas = var.autoscaler_config.max_replicas
+ min_replicas = var.autoscaler_config.min_replicas
+ cooldown_period = var.autoscaler_config.cooldown_period
+
+ dynamic "scale_down_control" {
+ for_each = local.as_scaling.down == null ? [] : [""]
+ content {
+ time_window_sec = local.as_scaling.down.time_window_sec
+ dynamic "max_scaled_down_replicas" {
+ for_each = (
+ local.as_scaling.down.max_replicas_fixed == null &&
+ local.as_scaling.down.max_replicas_percent == null
+ ? []
+ : [""]
+ )
+ content {
+ fixed = local.as_scaling.down.max_replicas_fixed
+ percent = local.as_scaling.down.max_replicas_percent
+ }
+ }
+ }
+ }
+
+ dynamic "scale_in_control" {
+ for_each = local.as_scaling.in == null ? [] : [""]
+ content {
+ time_window_sec = local.as_scaling.in.time_window_sec
+ dynamic "max_scaled_in_replicas" {
+ for_each = (
+ local.as_scaling.in.max_replicas_fixed == null &&
+ local.as_scaling.in.max_replicas_percent == null
+ ? []
+ : [""]
+ )
+ content {
+ fixed = local.as_scaling.in.max_replicas_fixed
+ percent = local.as_scaling.in.max_replicas_percent
+ }
+ }
+ }
+ }
+
+ dynamic "cpu_utilization" {
+ for_each = local.as_signals.cpu_utilization == null ? [] : [""]
+ content {
+ target = local.as_signals.cpu_utilization.target
+ predictive_method = (
+ local.as_signals.cpu_utilization.optimize_availability == true
+ ? "OPTIMIZE_AVAILABILITY"
+ : null
+ )
+ }
+ }
+
+ dynamic "load_balancing_utilization" {
+ for_each = local.as_signals.load_balancing_utilization == null ? [] : [""]
+ content {
+ target = local.as_signals.load_balancing_utilization.target
+ }
+ }
+
+ dynamic "metric" {
+ for_each = toset(
+ local.as_signals.metrics == null ? [] : local.as_signals.metrics
+ )
+ content {
+ name = metric.value.name
+ type = metric.value.type
+ target = metric.value.target_value
+ single_instance_assignment = metric.value.single_instance_assignment
+ filter = metric.value.time_series_filter
+ }
+ }
+
+ dynamic "scaling_schedules" {
+ for_each = toset(
+ local.as_signals.schedules == null ? [] : local.as_signals.schedules
+ )
+ iterator = schedule
+ content {
+ duration_sec = schedule.value.duration_sec
+ min_required_replicas = schedule.value.min_required_replicas
+ name = schedule.value.name
+ schedule = schedule.cron_schedule
+ description = schedule.value.description
+ disabled = schedule.value.disabled
+ time_zone = schedule.value.timezone
+ }
+ }
+
+ }
+}
diff --git a/modules/compute-mig/health-check.tf b/modules/compute-mig/health-check.tf
new file mode 100644
index 0000000000..4a4ed40def
--- /dev/null
+++ b/modules/compute-mig/health-check.tf
@@ -0,0 +1,119 @@
+/**
+ * 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:file:description Health check resource.
+
+locals {
+ hc = var.health_check_config
+ hc_grpc = try(local.hc.grpc, null) != null
+ hc_http = try(local.hc.http, null) != null
+ hc_http2 = try(local.hc.http2, null) != null
+ hc_https = try(local.hc.https, null) != null
+ hc_ssl = try(local.hc.ssl, null) != null
+ hc_tcp = try(local.hc.tcp, null) != null
+}
+
+resource "google_compute_health_check" "default" {
+ provider = google-beta
+ count = local.hc != null ? 1 : 0
+ project = var.project_id
+ name = var.name
+ description = local.hc.description
+ check_interval_sec = local.hc.check_interval_sec
+ healthy_threshold = local.hc.healthy_threshold
+ timeout_sec = local.hc.timeout_sec
+ unhealthy_threshold = local.hc.unhealthy_threshold
+
+ dynamic "grpc_health_check" {
+ for_each = local.hc_grpc ? [""] : []
+ content {
+ port = local.hc.grpc.port
+ port_name = local.hc.grpc.port_name
+ port_specification = local.hc.grpc.port_specification
+ grpc_service_name = local.hc.grpc.service_name
+ }
+ }
+
+ dynamic "http_health_check" {
+ for_each = local.hc_http ? [""] : []
+ content {
+ host = local.hc.http.host
+ port = local.hc.http.port
+ port_name = local.hc.http.port_name
+ port_specification = local.hc.http.port_specification
+ proxy_header = local.hc.http.proxy_header
+ request_path = local.hc.http.request_path
+ response = local.hc.http.response
+ }
+ }
+
+ dynamic "http2_health_check" {
+ for_each = local.hc_http2 ? [""] : []
+ content {
+ host = local.hc.http.host
+ port = local.hc.http.port
+ port_name = local.hc.http.port_name
+ port_specification = local.hc.http.port_specification
+ proxy_header = local.hc.http.proxy_header
+ request_path = local.hc.http.request_path
+ response = local.hc.http.response
+ }
+ }
+
+ dynamic "https_health_check" {
+ for_each = local.hc_https ? [""] : []
+ content {
+ host = local.hc.http.host
+ port = local.hc.http.port
+ port_name = local.hc.http.port_name
+ port_specification = local.hc.http.port_specification
+ proxy_header = local.hc.http.proxy_header
+ request_path = local.hc.http.request_path
+ response = local.hc.http.response
+ }
+ }
+
+ dynamic "ssl_health_check" {
+ for_each = local.hc_ssl ? [""] : []
+ content {
+ port = local.hc.tcp.port
+ port_name = local.hc.tcp.port_name
+ port_specification = local.hc.tcp.port_specification
+ proxy_header = local.hc.tcp.proxy_header
+ request = local.hc.tcp.request
+ response = local.hc.tcp.response
+ }
+ }
+
+ dynamic "tcp_health_check" {
+ for_each = local.hc_tcp ? [""] : []
+ content {
+ port = local.hc.tcp.port
+ port_name = local.hc.tcp.port_name
+ port_specification = local.hc.tcp.port_specification
+ proxy_header = local.hc.tcp.proxy_header
+ request = local.hc.tcp.request
+ response = local.hc.tcp.response
+ }
+ }
+
+ dynamic "log_config" {
+ for_each = try(local.hc.enable_logging, null) == true ? [""] : []
+ content {
+ enable = true
+ }
+ }
+}
diff --git a/modules/compute-mig/main.tf b/modules/compute-mig/main.tf
index 7b2253599e..65ce55b8e9 100644
--- a/modules/compute-mig/main.tf
+++ b/modules/compute-mig/main.tf
@@ -14,105 +14,50 @@
* limitations under the License.
*/
-resource "google_compute_autoscaler" "default" {
- provider = google-beta
- count = var.regional || var.autoscaler_config == null ? 0 : 1
- project = var.project_id
- name = var.name
- description = "Terraform managed."
- zone = var.location
- target = google_compute_instance_group_manager.default.0.id
-
- autoscaling_policy {
- max_replicas = var.autoscaler_config.max_replicas
- min_replicas = var.autoscaler_config.min_replicas
- cooldown_period = var.autoscaler_config.cooldown_period
-
- dynamic "cpu_utilization" {
- for_each = (
- var.autoscaler_config.cpu_utilization_target == null ? [] : [""]
- )
- content {
- target = var.autoscaler_config.cpu_utilization_target
- }
- }
-
- dynamic "load_balancing_utilization" {
- for_each = (
- var.autoscaler_config.load_balancing_utilization_target == null ? [] : [""]
- )
- content {
- target = var.autoscaler_config.load_balancing_utilization_target
- }
- }
-
- dynamic "metric" {
- for_each = (
- var.autoscaler_config.metric == null
- ? []
- : [var.autoscaler_config.metric]
- )
- iterator = config
- content {
- name = config.value.name
- single_instance_assignment = config.value.single_instance_assignment
- target = config.value.target
- type = config.value.type
- filter = config.value.filter
- }
- }
- }
+locals {
+ health_check = (
+ try(var.auto_healing_policies.health_check, null) == null
+ ? try(google_compute_health_check.default.0.self_link, null)
+ : try(var.auto_healing_policies.health_check, null)
+ )
+ instance_group_manager = (
+ local.is_regional ?
+ google_compute_region_instance_group_manager.default :
+ google_compute_instance_group_manager.default
+ )
+ is_regional = length(split("-", var.location)) == 2
}
-
resource "google_compute_instance_group_manager" "default" {
- provider = google-beta
- count = var.regional ? 0 : 1
- project = var.project_id
- zone = var.location
- name = var.name
- base_instance_name = var.name
- description = "Terraform-managed."
- target_size = var.target_size
- target_pools = var.target_pools
- wait_for_instances = var.wait_for_instances
- dynamic "auto_healing_policies" {
- for_each = var.auto_healing_policies == null ? [] : [var.auto_healing_policies]
- iterator = config
+ provider = google-beta
+ count = local.is_regional ? 0 : 1
+ project = var.project_id
+ zone = var.location
+ name = var.name
+ base_instance_name = var.name
+ description = var.description
+ target_size = var.target_size
+ target_pools = var.target_pools
+ wait_for_instances = try(var.wait_for_instances.enabled, null)
+ wait_for_instances_status = try(var.wait_for_instances.status, null)
+
+ dynamic "all_instances_config" {
+ for_each = var.all_instances_config == null ? [] : [""]
content {
- health_check = config.value.health_check
- initial_delay_sec = config.value.initial_delay_sec
+ labels = try(var.all_instances_config.labels, null)
+ metadata = try(var.all_instances_config.metadata, null)
}
}
- dynamic "stateful_disk" {
- for_each = try(var.stateful_config.mig_config.stateful_disks, {})
- iterator = config
- content {
- device_name = config.key
- delete_rule = config.value.delete_rule
- }
- }
- dynamic "update_policy" {
- for_each = var.update_policy == null ? [] : [var.update_policy]
+
+ dynamic "auto_healing_policies" {
+ for_each = var.auto_healing_policies == null ? [] : [""]
iterator = config
content {
- type = config.value.type
- minimal_action = config.value.minimal_action
- min_ready_sec = config.value.min_ready_sec
- max_surge_fixed = (
- config.value.max_surge_type == "fixed" ? config.value.max_surge : null
- )
- max_surge_percent = (
- config.value.max_surge_type == "percent" ? config.value.max_surge : null
- )
- max_unavailable_fixed = (
- config.value.max_unavailable_type == "fixed" ? config.value.max_unavailable : null
- )
- max_unavailable_percent = (
- config.value.max_unavailable_type == "percent" ? config.value.max_unavailable : null
- )
+ health_check = local.health_check
+ initial_delay_sec = var.auto_healing_policies.initial_delay_sec
}
}
+
dynamic "named_port" {
for_each = var.named_ports == null ? {} : var.named_ports
iterator = config
@@ -121,166 +66,88 @@ resource "google_compute_instance_group_manager" "default" {
port = config.value
}
}
- version {
- instance_template = var.default_version.instance_template
- name = var.default_version.name
- }
- dynamic "version" {
- for_each = var.versions == null ? {} : var.versions
- iterator = version
+
+ dynamic "stateful_disk" {
+ for_each = var.stateful_disks
content {
- name = version.key
- instance_template = version.value.instance_template
- target_size {
- fixed = (
- version.value.target_type == "fixed" ? version.value.target_size : null
- )
- percent = (
- version.value.target_type == "percent" ? version.value.target_size : null
- )
- }
+ device_name = stateful_disk.key
+ delete_rule = stateful_disk.value ? "ON_PERMANENT_INSTANCE_DELETION" : "NEVER"
}
}
-}
-
-locals {
- instance_group_manager = (
- var.regional ?
- google_compute_region_instance_group_manager.default :
- google_compute_instance_group_manager.default
- )
-}
-
-resource "google_compute_per_instance_config" "default" {
- for_each = try(var.stateful_config.per_instance_config, {})
- #for_each = var.stateful_config && var.stateful_config.per_instance_config == null ? {} : length(var.stateful_config.per_instance_config)
- zone = var.location
- # terraform error, solved with locals
- #instance_group_manager = var.regional ? google_compute_region_instance_group_manager.default : google_compute_instance_group_manager.default
- instance_group_manager = local.instance_group_manager[0].id
- name = each.key
- project = var.project_id
- minimal_action = try(each.value.update_config.minimal_action, null)
- most_disruptive_allowed_action = try(each.value.update_config.most_disruptive_allowed_action, null)
- remove_instance_state_on_destroy = try(each.value.update_config.remove_instance_state_on_destroy, null)
- preserved_state {
- metadata = each.value.metadata
-
- dynamic "disk" {
- for_each = try(each.value.stateful_disks, {})
- #for_each = var.stateful_config.mig_config.stateful_disks == null ? {} : var.stateful_config.mig_config.stateful_disks
- iterator = config
- content {
- device_name = config.key
- source = config.value.source
- mode = config.value.mode
- delete_rule = config.value.delete_rule
- }
+ dynamic "update_policy" {
+ for_each = var.update_policy == null ? [] : [var.update_policy]
+ iterator = p
+ content {
+ minimal_action = p.value.minimal_action
+ type = p.value.type
+ max_surge_fixed = try(p.value.max_surge.fixed, null)
+ max_surge_percent = try(p.value.max_surge.percent, null)
+ max_unavailable_fixed = try(p.value.max_unavailable.fixed, null)
+ max_unavailable_percent = try(p.value.max_unavailable.percent, null)
+ min_ready_sec = p.value.min_ready_sec
+ most_disruptive_allowed_action = p.value.most_disruptive_action
+ replacement_method = p.value.replacement_method
}
}
-}
-
-resource "google_compute_region_autoscaler" "default" {
- provider = google-beta
- count = var.regional && var.autoscaler_config != null ? 1 : 0
- project = var.project_id
- name = var.name
- description = "Terraform managed."
- region = var.location
- target = google_compute_region_instance_group_manager.default.0.id
-
- autoscaling_policy {
- max_replicas = var.autoscaler_config.max_replicas
- min_replicas = var.autoscaler_config.min_replicas
- cooldown_period = var.autoscaler_config.cooldown_period
- dynamic "cpu_utilization" {
- for_each = (
- var.autoscaler_config.cpu_utilization_target == null ? [] : [""]
- )
- content {
- target = var.autoscaler_config.cpu_utilization_target
- }
- }
-
- dynamic "load_balancing_utilization" {
- for_each = (
- var.autoscaler_config.load_balancing_utilization_target == null ? [] : [""]
- )
- content {
- target = var.autoscaler_config.load_balancing_utilization_target
- }
- }
+ version {
+ instance_template = var.instance_template
+ name = var.default_version_name
+ }
- dynamic "metric" {
- for_each = (
- var.autoscaler_config.metric == null
- ? []
- : [var.autoscaler_config.metric]
- )
- iterator = config
- content {
- name = config.value.name
- single_instance_assignment = config.value.single_instance_assignment
- target = config.value.target
- type = config.value.type
- filter = config.value.filter
+ dynamic "version" {
+ for_each = var.versions
+ content {
+ name = version.key
+ instance_template = version.value.instance_template
+ dynamic "target_size" {
+ for_each = version.value.target_size == null ? [] : [""]
+ content {
+ fixed = version.value.target_size.fixed
+ percent = version.value.target_size.percent
+ }
}
}
}
}
-
resource "google_compute_region_instance_group_manager" "default" {
provider = google-beta
- count = var.regional ? 1 : 0
+ count = local.is_regional ? 1 : 0
project = var.project_id
region = var.location
name = var.name
base_instance_name = var.name
- description = "Terraform-managed."
- target_size = var.target_size
- target_pools = var.target_pools
- wait_for_instances = var.wait_for_instances
- dynamic "auto_healing_policies" {
- for_each = var.auto_healing_policies == null ? [] : [var.auto_healing_policies]
- iterator = config
- content {
- health_check = config.value.health_check
- initial_delay_sec = config.value.initial_delay_sec
- }
- }
- dynamic "stateful_disk" {
- for_each = try(var.stateful_config.mig_config.stateful_disks, {})
- iterator = config
+ description = var.description
+ distribution_policy_target_shape = try(
+ var.distribution_policy.target_shape, null
+ )
+ distribution_policy_zones = try(
+ var.distribution_policy.zones, null
+ )
+ target_size = var.target_size
+ target_pools = var.target_pools
+ wait_for_instances = try(var.wait_for_instances.enabled, null)
+ wait_for_instances_status = try(var.wait_for_instances.status, null)
+
+ dynamic "all_instances_config" {
+ for_each = var.all_instances_config == null ? [] : [""]
content {
- device_name = config.key
- delete_rule = config.value.delete_rule
+ labels = try(var.all_instances_config.labels, null)
+ metadata = try(var.all_instances_config.metadata, null)
}
}
- dynamic "update_policy" {
- for_each = var.update_policy == null ? [] : [var.update_policy]
+ dynamic "auto_healing_policies" {
+ for_each = var.auto_healing_policies == null ? [] : [""]
iterator = config
content {
- type = config.value.type
- minimal_action = config.value.minimal_action
- min_ready_sec = config.value.min_ready_sec
- max_surge_fixed = (
- config.value.max_surge_type == "fixed" ? config.value.max_surge : null
- )
- max_surge_percent = (
- config.value.max_surge_type == "percent" ? config.value.max_surge : null
- )
- max_unavailable_fixed = (
- config.value.max_unavailable_type == "fixed" ? config.value.max_unavailable : null
- )
- max_unavailable_percent = (
- config.value.max_unavailable_type == "percent" ? config.value.max_unavailable : null
- )
+ health_check = local.health_check
+ initial_delay_sec = var.auto_healing_policies.initial_delay_sec
}
}
+
dynamic "named_port" {
for_each = var.named_ports == null ? {} : var.named_ports
iterator = config
@@ -289,172 +156,49 @@ resource "google_compute_region_instance_group_manager" "default" {
port = config.value
}
}
- version {
- instance_template = var.default_version.instance_template
- name = var.default_version.name
- }
- dynamic "version" {
- for_each = var.versions == null ? {} : var.versions
- iterator = version
- content {
- name = version.key
- instance_template = version.value.instance_template
- target_size {
- fixed = (
- version.value.target_type == "fixed" ? version.value.target_size : null
- )
- percent = (
- version.value.target_type == "percent" ? version.value.target_size : null
- )
- }
- }
- }
-}
-
-resource "google_compute_health_check" "http" {
- provider = google-beta
- count = try(var.health_check_config.type, null) == "http" ? 1 : 0
- project = var.project_id
- name = var.name
- description = "Terraform managed."
-
- check_interval_sec = try(var.health_check_config.config.check_interval_sec, null)
- healthy_threshold = try(var.health_check_config.config.healthy_threshold, null)
- timeout_sec = try(var.health_check_config.config.timeout_sec, null)
- unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null)
-
- http_health_check {
- host = try(var.health_check_config.check.host, null)
- port = try(var.health_check_config.check.port, null)
- port_name = try(var.health_check_config.check.port_name, null)
- port_specification = try(var.health_check_config.check.port_specification, null)
- proxy_header = try(var.health_check_config.check.proxy_header, null)
- request_path = try(var.health_check_config.check.request_path, null)
- response = try(var.health_check_config.check.response, null)
- }
-
- dynamic "log_config" {
- for_each = try(var.health_check_config.logging, false) ? [""] : []
- content {
- enable = true
- }
- }
-}
-
-resource "google_compute_health_check" "https" {
- provider = google-beta
- count = try(var.health_check_config.type, null) == "https" ? 1 : 0
- project = var.project_id
- name = var.name
- description = "Terraform managed."
-
- check_interval_sec = try(var.health_check_config.config.check_interval_sec, null)
- healthy_threshold = try(var.health_check_config.config.healthy_threshold, null)
- timeout_sec = try(var.health_check_config.config.timeout_sec, null)
- unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null)
-
- https_health_check {
- host = try(var.health_check_config.check.host, null)
- port = try(var.health_check_config.check.port, null)
- port_name = try(var.health_check_config.check.port_name, null)
- port_specification = try(var.health_check_config.check.port_specification, null)
- proxy_header = try(var.health_check_config.check.proxy_header, null)
- request_path = try(var.health_check_config.check.request_path, null)
- response = try(var.health_check_config.check.response, null)
- }
-
- dynamic "log_config" {
- for_each = try(var.health_check_config.logging, false) ? [""] : []
- content {
- enable = true
- }
- }
-}
-
-resource "google_compute_health_check" "tcp" {
- provider = google-beta
- count = try(var.health_check_config.type, null) == "tcp" ? 1 : 0
- project = var.project_id
- name = var.name
- description = "Terraform managed."
-
- check_interval_sec = try(var.health_check_config.config.check_interval_sec, null)
- healthy_threshold = try(var.health_check_config.config.healthy_threshold, null)
- timeout_sec = try(var.health_check_config.config.timeout_sec, null)
- unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null)
-
- tcp_health_check {
- port = try(var.health_check_config.check.port, null)
- port_name = try(var.health_check_config.check.port_name, null)
- port_specification = try(var.health_check_config.check.port_specification, null)
- proxy_header = try(var.health_check_config.check.proxy_header, null)
- request = try(var.health_check_config.check.request, null)
- response = try(var.health_check_config.check.response, null)
- }
- dynamic "log_config" {
- for_each = try(var.health_check_config.logging, false) ? [""] : []
+ dynamic "stateful_disk" {
+ for_each = var.stateful_disks
content {
- enable = true
+ device_name = stateful_disk.key
+ delete_rule = stateful_disk.value ? "ON_PERMANENT_INSTANCE_DELETION" : "NEVER"
}
}
-}
-
-resource "google_compute_health_check" "ssl" {
- provider = google-beta
- count = try(var.health_check_config.type, null) == "ssl" ? 1 : 0
- project = var.project_id
- name = var.name
- description = "Terraform managed."
-
- check_interval_sec = try(var.health_check_config.config.check_interval_sec, null)
- healthy_threshold = try(var.health_check_config.config.healthy_threshold, null)
- timeout_sec = try(var.health_check_config.config.timeout_sec, null)
- unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null)
-
- ssl_health_check {
- port = try(var.health_check_config.check.port, null)
- port_name = try(var.health_check_config.check.port_name, null)
- port_specification = try(var.health_check_config.check.port_specification, null)
- proxy_header = try(var.health_check_config.check.proxy_header, null)
- request = try(var.health_check_config.check.request, null)
- response = try(var.health_check_config.check.response, null)
- }
- dynamic "log_config" {
- for_each = try(var.health_check_config.logging, false) ? [""] : []
+ dynamic "update_policy" {
+ for_each = var.update_policy == null ? [] : [var.update_policy]
+ iterator = p
content {
- enable = true
+ minimal_action = p.value.minimal_action
+ type = p.value.type
+ instance_redistribution_type = p.value.regional_redistribution_type
+ max_surge_fixed = try(p.value.max_surge.fixed, null)
+ max_surge_percent = try(p.value.max_surge.percent, null)
+ max_unavailable_fixed = try(p.value.max_unavailable.fixed, null)
+ max_unavailable_percent = try(p.value.max_unavailable.percent, null)
+ min_ready_sec = p.value.min_ready_sec
+ most_disruptive_allowed_action = p.value.most_disruptive_action
+ replacement_method = p.value.replacement_method
}
}
-}
-
-resource "google_compute_health_check" "http2" {
- provider = google-beta
- count = try(var.health_check_config.type, null) == "http2" ? 1 : 0
- project = var.project_id
- name = var.name
- description = "Terraform managed."
- check_interval_sec = try(var.health_check_config.config.check_interval_sec, null)
- healthy_threshold = try(var.health_check_config.config.healthy_threshold, null)
- timeout_sec = try(var.health_check_config.config.timeout_sec, null)
- unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null)
-
- http2_health_check {
- host = try(var.health_check_config.check.host, null)
- port = try(var.health_check_config.check.port, null)
- port_name = try(var.health_check_config.check.port_name, null)
- port_specification = try(var.health_check_config.check.port_specification, null)
- proxy_header = try(var.health_check_config.check.proxy_header, null)
- request_path = try(var.health_check_config.check.request_path, null)
- response = try(var.health_check_config.check.response, null)
+ version {
+ instance_template = var.instance_template
+ name = var.default_version_name
}
- dynamic "log_config" {
- for_each = try(var.health_check_config.logging, false) ? [""] : []
+ dynamic "version" {
+ for_each = var.versions
content {
- enable = true
+ name = version.key
+ instance_template = version.value.instance_template
+ dynamic "target_size" {
+ for_each = version.value.target_size == null ? [] : [""]
+ content {
+ fixed = version.value.target_size.fixed
+ percent = version.value.target_size.percent
+ }
+ }
}
}
}
diff --git a/modules/compute-mig/outputs.tf b/modules/compute-mig/outputs.tf
index 93de9223d3..41b20c1fa2 100644
--- a/modules/compute-mig/outputs.tf
+++ b/modules/compute-mig/outputs.tf
@@ -37,13 +37,6 @@ output "health_check" {
value = (
var.health_check_config == null
? null
- : try(
- google_compute_health_check.http.0,
- google_compute_health_check.https.0,
- google_compute_health_check.tcp.0,
- google_compute_health_check.ssl.0,
- google_compute_health_check.http2.0,
- {}
- )
+ : google_compute_health_check.default.0
)
}
diff --git a/modules/compute-mig/stateful-config.tf b/modules/compute-mig/stateful-config.tf
new file mode 100644
index 0000000000..1e9e056e56
--- /dev/null
+++ b/modules/compute-mig/stateful-config.tf
@@ -0,0 +1,91 @@
+/**
+ * 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:file:description Instance-level stateful configuration resources.
+
+resource "google_compute_per_instance_config" "default" {
+ for_each = local.is_regional ? {} : var.stateful_config
+ project = var.project_id
+ zone = var.location
+ name = each.key
+ instance_group_manager = try(
+ google_compute_instance_group_manager.default.0.id, null
+ )
+ minimal_action = each.value.minimal_action
+ most_disruptive_allowed_action = each.value.most_disruptive_action
+ remove_instance_state_on_destroy = each.value.remove_state_on_destroy
+
+ dynamic "preserved_state" {
+ for_each = each.value.preserved_state == null ? [] : [""]
+ content {
+ metadata = each.value.preserved_state.metadata
+ dynamic "disk" {
+ for_each = (
+ each.value.preserved_state.disks == null
+ ? {}
+ : each.value.preserved_state.disks
+ )
+ content {
+ device_name = disk.key
+ source = disk.value.source
+ delete_rule = (
+ disk.value.delete_on_instance_deletion == true
+ ? "ON_PERMANENT_INSTANCE_DELETION"
+ : "NEVER"
+ )
+ mode = disk.value.read_only == true ? "READ_ONLY" : "READ_WRITE"
+ }
+ }
+ }
+ }
+}
+
+resource "google_compute_region_per_instance_config" "default" {
+ for_each = local.is_regional ? var.stateful_config : {}
+ project = var.project_id
+ region = var.location
+ name = each.key
+ region_instance_group_manager = try(
+ google_compute_region_instance_group_manager.default.0.id, null
+ )
+ minimal_action = each.value.minimal_action
+ most_disruptive_allowed_action = each.value.most_disruptive_action
+ remove_instance_state_on_destroy = each.value.remove_state_on_destroy
+
+ dynamic "preserved_state" {
+ for_each = each.value.preserved_state == null ? [] : [""]
+ content {
+ metadata = each.value.preserved_state.metadata
+ dynamic "disk" {
+ for_each = (
+ each.value.preserved_state.disks == null
+ ? {}
+ : each.value.preserved_state.disks
+ )
+ content {
+ device_name = disk.key
+ source = disk.value.source
+ delete_rule = (
+ disk.value.delete_on_instance_deletion == true
+ ? "ON_PERMANENT_INSTANCE_DELETION"
+ : "NEVER"
+ )
+ mode = disk.value.read_only == true ? "READ_ONLY" : "READ_WRITE"
+ }
+ }
+ }
+ }
+}
diff --git a/modules/compute-mig/variables.tf b/modules/compute-mig/variables.tf
index 274fb2354d..30f2ce9651 100644
--- a/modules/compute-mig/variables.tf
+++ b/modules/compute-mig/variables.tf
@@ -14,57 +14,176 @@
* limitations under the License.
*/
+variable "all_instances_config" {
+ description = "Metadata and labels set to all instances in the group."
+ type = object({
+ labels = optional(map(string))
+ metadata = optional(map(string))
+ })
+ default = null
+}
+
variable "auto_healing_policies" {
description = "Auto-healing policies for this group."
type = object({
- health_check = string
+ health_check = optional(string)
initial_delay_sec = number
})
default = null
}
variable "autoscaler_config" {
- description = "Optional autoscaler configuration. Only one of 'cpu_utilization_target' 'load_balancing_utilization_target' or 'metric' can be not null."
+ description = "Optional autoscaler configuration."
type = object({
- max_replicas = number
- min_replicas = number
- cooldown_period = number
- cpu_utilization_target = number
- load_balancing_utilization_target = number
- metric = object({
- name = string
- single_instance_assignment = number
- target = number
- type = string # GAUGE, DELTA_PER_SECOND, DELTA_PER_MINUTE
- filter = string
- })
+ max_replicas = number
+ min_replicas = number
+ cooldown_period = optional(number)
+ mode = optional(string) # OFF, ONLY_UP, ON
+ scaling_control = optional(object({
+ down = optional(object({
+ max_replicas_fixed = optional(number)
+ max_replicas_percent = optional(number)
+ time_window_sec = optional(number)
+ }))
+ in = optional(object({
+ max_replicas_fixed = optional(number)
+ max_replicas_percent = optional(number)
+ time_window_sec = optional(number)
+ }))
+ }), {})
+ scaling_signals = optional(object({
+ cpu_utilization = optional(object({
+ target = number
+ optimize_availability = optional(bool)
+ }))
+ load_balancing_utilization = optional(object({
+ target = number
+ }))
+ metrics = optional(list(object({
+ name = string
+ type = string # GAUGE, DELTA_PER_SECOND, DELTA_PER_MINUTE
+ target_value = number
+ single_instance_assignment = optional(number)
+ time_series_filter = optional(string)
+ })))
+ schedules = optional(list(object({
+ duration_sec = number
+ name = string
+ min_required_replicas = number
+ cron_schedule = string
+ description = optional(bool)
+ timezone = optional(string)
+ disabled = optional(bool)
+ })))
+ }), {})
})
default = null
}
-variable "default_version" {
- description = "Default application version template. Additional versions can be specified via the `versions` variable."
+variable "default_version_name" {
+ description = "Name used for the default version."
+ type = string
+ default = "default"
+}
+
+variable "description" {
+ description = "Optional description used for all resources managed by this module."
+ type = string
+ default = "Terraform managed."
+}
+
+variable "distribution_policy" {
+ description = "DIstribution policy for regional MIG."
type = object({
- instance_template = string
- name = string
+ target_shape = optional(string)
+ zones = optional(list(string))
})
+ default = null
}
variable "health_check_config" {
description = "Optional auto-created health check configuration, use the output self-link to set it in the auto healing policy. Refer to examples for usage."
type = object({
- type = string # http https tcp ssl http2
- check = map(any) # actual health check block attributes
- config = map(number) # interval, thresholds, timeout
- logging = bool
+ check_interval_sec = optional(number)
+ description = optional(string, "Terraform managed.")
+ enable_logging = optional(bool, false)
+ healthy_threshold = optional(number)
+ timeout_sec = optional(number)
+ unhealthy_threshold = optional(number)
+ grpc = optional(object({
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ service_name = optional(string)
+ }))
+ http = optional(object({
+ host = optional(string)
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request_path = optional(string)
+ response = optional(string)
+ }))
+ http2 = optional(object({
+ host = optional(string)
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request_path = optional(string)
+ response = optional(string)
+ }))
+ https = optional(object({
+ host = optional(string)
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request_path = optional(string)
+ response = optional(string)
+ }))
+ tcp = optional(object({
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request = optional(string)
+ response = optional(string)
+ }))
+ ssl = optional(object({
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request = optional(string)
+ response = optional(string)
+ }))
})
default = null
+ validation {
+ 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.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."
+ }
+}
+
+variable "instance_template" {
+ description = "Instance template for the default version."
+ type = string
}
variable "location" {
- description = "Compute zone, or region if `regional` is set to true."
+ description = "Compute zone or region."
type = string
}
+
variable "name" {
description = "Managed group name."
type = string
@@ -81,41 +200,30 @@ variable "project_id" {
type = string
}
-variable "regional" {
- description = "Use regional instance group. When set, `location` should be set to the region."
- type = bool
- default = false
-}
-
variable "stateful_config" {
- description = "Stateful configuration can be done by individual instances or for all instances in the MIG. They key in per_instance_config is the name of the specific instance. The key of the stateful_disks is the 'device_name' field of the resource. Please note that device_name is defined at the OS mount level, unlike the disk name."
- type = object({
- per_instance_config = map(object({
- #name is the key
- #name = string
- stateful_disks = map(object({
- #device_name is the key
- source = string
- mode = string # READ_WRITE | READ_ONLY
- delete_rule = string # NEVER | ON_PERMANENT_INSTANCE_DELETION
- }))
- metadata = map(string)
- update_config = object({
- minimal_action = string # NONE | REPLACE | RESTART | REFRESH
- most_disruptive_allowed_action = string # REPLACE | RESTART | REFRESH | NONE
- remove_instance_state_on_destroy = bool
- })
+ description = "Stateful configuration for individual instances."
+ type = map(object({
+ minimal_action = optional(string)
+ most_disruptive_action = optional(string)
+ remove_state_on_destroy = optional(bool)
+ preserved_state = optional(object({
+ disks = optional(map(object({
+ source = string
+ delete_on_instance_deletion = optional(bool)
+ read_only = optional(bool)
+ })))
+ metadata = optional(map(string))
}))
+ }))
+ default = {}
+ nullable = false
+}
- mig_config = object({
- stateful_disks = map(object({
- #device_name is the key
- delete_rule = string # NEVER | ON_PERMANENT_INSTANCE_DELETION
- }))
- })
-
- })
- default = null
+variable "stateful_disks" {
+ description = "Stateful disk configuration applied at the MIG level to all instances, in device name => on permanent instance delete rule as boolean."
+ type = map(bool)
+ default = {}
+ nullable = false
}
variable "target_pools" {
@@ -131,31 +239,44 @@ variable "target_size" {
}
variable "update_policy" {
- description = "Update policy. Type can be 'OPPORTUNISTIC' or 'PROACTIVE', action 'REPLACE' or 'restart', surge type 'fixed' or 'percent'."
+ description = "Update policy. Minimal action and type are required."
type = object({
- type = string # OPPORTUNISTIC | PROACTIVE
- minimal_action = string # REPLACE | RESTART
- min_ready_sec = number
- max_surge_type = string # fixed | percent
- max_surge = number
- max_unavailable_type = string
- max_unavailable = number
+ minimal_action = string
+ type = string
+ max_surge = optional(object({
+ fixed = optional(number)
+ percent = optional(number)
+ }))
+ max_unavailable = optional(object({
+ fixed = optional(number)
+ percent = optional(number)
+ }))
+ min_ready_sec = optional(number)
+ most_disruptive_action = optional(string)
+ regional_redistribution_type = optional(string)
+ replacement_method = optional(string)
})
default = null
}
variable "versions" {
- description = "Additional application versions, target_type is either 'fixed' or 'percent'."
+ description = "Additional application versions, target_size is optional."
type = map(object({
instance_template = string
- target_type = string # fixed | percent
- target_size = number
+ target_size = optional(object({
+ fixed = optional(number)
+ percent = optional(number)
+ }))
}))
- default = null
+ default = {}
+ nullable = false
}
variable "wait_for_instances" {
description = "Wait for all instances to be created/updated before returning."
- type = bool
- default = null
+ type = object({
+ enabled = bool
+ status = optional(string)
+ })
+ default = null
}
diff --git a/modules/compute-mig/versions.tf b/modules/compute-mig/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/compute-mig/versions.tf
+++ b/modules/compute-mig/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/compute-vm/README.md b/modules/compute-vm/README.md
index 81cbee2c4e..2f715555a2 100644
--- a/modules/compute-vm/README.md
+++ b/modules/compute-vm/README.md
@@ -15,15 +15,37 @@ The simplest example leverages defaults for the boot disk image and size, and us
```hcl
module "simple-vm-example" {
- source = "./modules/compute-vm"
+ source = "./fabric/modules/compute-vm"
project_id = var.project_id
- zone = "europe-west1-b"
+ zone = "europe-west1-b"
+ name = "test"
+ network_interfaces = [{
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ }]
+ service_account_create = true
+}
+# tftest modules=1 resources=2
+
+```
+
+### 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
- nat = false
- addresses = null
}]
service_account_create = true
}
@@ -44,25 +66,21 @@ This is an example of attaching a pre-existing regional PD to a new instance:
```hcl
module "simple-vm-example" {
- source = "./modules/compute-vm"
+ source = "./fabric/modules/compute-vm"
project_id = var.project_id
- zone = "${var.region}-b"
+ zone = "${var.region}-b"
name = "test"
network_interfaces = [{
network = var.vpc.self_link
subnetwork = var.subnet.self_link
- nat = false
- addresses = null
}]
attached_disks = [{
name = "repd-1"
- size = null
+ size = 10
source_type = "attach"
source = "regions/${var.region}/disks/repd-test-1"
options = {
- mode = null
replica_zone = "${var.region}-c"
- type = null
}
}]
service_account_create = true
@@ -74,29 +92,25 @@ And the same example for an instance template (where not using the full self lin
```hcl
module "simple-vm-example" {
- source = "./modules/compute-vm"
+ source = "./fabric/modules/compute-vm"
project_id = var.project_id
- zone = "${var.region}-b"
+ zone = "${var.region}-b"
name = "test"
network_interfaces = [{
network = var.vpc.self_link
subnetwork = var.subnet.self_link
- nat = false
- addresses = null
}]
attached_disks = [{
name = "repd"
- size = null
+ size = 10
source_type = "attach"
source = "https://www.googleapis.com/compute/v1/projects/${var.project_id}/regions/${var.region}/disks/repd-test-1"
options = {
- mode = null
replica_zone = "${var.region}-c"
- type = null
}
}]
service_account_create = true
- create_template = true
+ create_template = true
}
# tftest modules=1 resources=2
```
@@ -107,35 +121,27 @@ This example shows how to control disk encryption via the the `encryption` varia
```hcl
module "kms-vm-example" {
- source = "./modules/compute-vm"
+ 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
- nat = false
- addresses = null
}]
attached_disks = [
{
- name = "attached-disk"
- size = 10
- source = null
- source_type = null
- options = null
+ name = "attached-disk"
+ size = 10
}
]
service_account_create = true
boot_disk = {
- image = "projects/debian-cloud/global/images/family/debian-10"
- type = "pd-ssd"
- size = 10
+ image = "projects/debian-cloud/global/images/family/debian-10"
}
encryption = {
- encrypt_boot = true
- disk_encryption_key_raw = null
- kms_key_self_link = var.kms_key.self_link
+ encrypt_boot = true
+ kms_key_self_link = var.kms_key.self_link
}
}
# tftest modules=1 resources=3
@@ -147,24 +153,17 @@ This example shows how to add additional [Alias IPs](https://cloud.google.com/vp
```hcl
module "vm-with-alias-ips" {
- source = "./modules/compute-vm"
+ source = "./fabric/modules/compute-vm"
project_id = "my-project"
zone = "europe-west1-b"
name = "test"
network_interfaces = [{
network = var.vpc.self_link
subnetwork = var.subnet.self_link
- nat = false
- addresses = null
- }]
- network_interface_options = {
- 0 = {
- alias_ips = {
- alias1 = "10.16.0.10/32"
- }
- nic_type = null
+ alias_ips = {
+ alias1 = "10.16.0.10/32"
}
- }
+ }]
service_account_create = true
}
# tftest modules=1 resources=2
@@ -177,9 +176,9 @@ This example shows how to enable [gVNIC](https://cloud.google.com/compute/docs/n
```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"
@@ -196,30 +195,22 @@ resource "google_compute_image" "cos-gvnic" {
}
module "vm-with-gvnic" {
- source = "./modules/compute-vm"
+ source = "./fabric/modules/compute-vm"
project_id = "my-project"
zone = "europe-west1-b"
name = "test"
- boot_disk = {
- image = google_compute_image.cos-gvnic.self_link
- type = "pd-ssd"
- size = 10
+ boot_disk = {
+ image = google_compute_image.cos-gvnic.self_link
+ type = "pd-ssd"
}
network_interfaces = [{
network = var.vpc.self_link
subnetwork = var.subnet.self_link
- nat = false
- addresses = null
+ nic_type = "GVNIC"
}]
- network_interface_options = {
- 0 = {
- alias_ips = null
- nic_type = "GVNIC"
- }
- }
service_account_create = true
}
-# tftest modules=1 resources=2
+# tftest modules=1 resources=3
```
### Instance template
@@ -228,32 +219,25 @@ This example shows how to use the module to manage an instance template that def
```hcl
module "cos-test" {
- source = "./modules/compute-vm"
+ source = "./fabric/modules/compute-vm"
project_id = "my-project"
- zone = "europe-west1-b"
+ zone = "europe-west1-b"
name = "test"
network_interfaces = [{
network = var.vpc.self_link
subnetwork = var.subnet.self_link
- nat = false
- addresses = null
}]
- boot_disk = {
+ boot_disk = {
image = "projects/cos-cloud/global/images/family/cos-stable"
- type = "pd-ssd"
- size = 10
}
attached_disks = [
{
- name = "disk-1"
- size = 10
- source = null
- source_type = null
- options = null
+ name = "disk-1"
+ size = 10
}
]
- service_account = "vm-default@my-project.iam.gserviceaccount.com"
- create_template = true
+ service_account = "vm-default@my-project.iam.gserviceaccount.com"
+ create_template = true
}
# tftest modules=1 resources=1
```
@@ -268,20 +252,16 @@ locals {
}
module "instance-group" {
- source = "./modules/compute-vm"
+ source = "./fabric/modules/compute-vm"
project_id = "my-project"
- zone = "europe-west1-b"
+ zone = "europe-west1-b"
name = "ilb-test"
network_interfaces = [{
network = var.vpc.self_link
subnetwork = var.subnet.self_link
- nat = false
- addresses = null
}]
boot_disk = {
image = "projects/cos-cloud/global/images/family/cos-stable"
- type = "pd-ssd"
- size = 10
}
service_account = var.service_account.email
service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
@@ -298,36 +278,34 @@ module "instance-group" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [name](variables.tf#L160) | Instance name. | string
| ✓ | |
-| [network_interfaces](variables.tf#L174) | Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed. | list(object({…}))
| ✓ | |
-| [project_id](variables.tf#L201) | Project id. | string
| ✓ | |
-| [zone](variables.tf#L260) | Compute zone. | string
| ✓ | |
-| [attached_disk_defaults](variables.tf#L17) | Defaults for attached disks options. | object({…})
| | {…}
|
-| [attached_disks](variables.tf#L32) | 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#L58) | Boot disk properties. | object({…})
| | {…}
|
-| [boot_disk_delete](variables.tf#L72) | Auto delete boot disk. | bool
| | true
|
-| [can_ip_forward](variables.tf#L78) | Enable IP forwarding. | bool
| | false
|
-| [confidential_compute](variables.tf#L84) | Enable Confidential Compute for these instances. | bool
| | false
|
-| [create_template](variables.tf#L90) | Create instance template instead of instances. | bool
| | false
|
-| [description](variables.tf#L95) | Description of a Compute Instance. | string
| | "Managed by the compute-vm Terraform module."
|
-| [enable_display](variables.tf#L100) | Enable virtual display on the instances. | bool
| | false
|
-| [encryption](variables.tf#L106) | 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#L116) | Define this variable to create an instance group for instances. Disabled for template use. | object({…})
| | null
|
-| [hostname](variables.tf#L124) | Instance FQDN name. | string
| | null
|
-| [iam](variables.tf#L130) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
-| [instance_type](variables.tf#L136) | Instance type. | string
| | "f1-micro"
|
-| [labels](variables.tf#L142) | Instance labels. | map(string)
| | {}
|
-| [metadata](variables.tf#L148) | Instance metadata. | map(string)
| | {}
|
-| [min_cpu_platform](variables.tf#L154) | Minimum CPU platform. | string
| | null
|
-| [network_interface_options](variables.tf#L165) | Network interfaces extended options. The key is the index of the inteface to configure. The value is an object with alias_ips and nic_type. Set alias_ips or nic_type to null if you need only one of them. | map(object({…}))
| | {}
|
-| [options](variables.tf#L187) | Instance options. | object({…})
| | {…}
|
-| [scratch_disks](variables.tf#L206) | Scratch disks configuration. | object({…})
| | {…}
|
-| [service_account](variables.tf#L218) | Service account email. Unused if service account is auto-created. | string
| | null
|
-| [service_account_create](variables.tf#L224) | Auto-create service account. | bool
| | false
|
-| [service_account_scopes](variables.tf#L232) | Scopes applied to service account. | list(string)
| | []
|
-| [shielded_config](variables.tf#L238) | Shielded VM configuration of the instances. | object({…})
| | null
|
-| [tag_bindings](variables.tf#L248) | Tag bindings for this instance, in key => tag value id format. | map(string)
| | null
|
-| [tags](variables.tf#L254) | Instance network tags for firewall rule targets. | list(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#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
@@ -337,12 +315,13 @@ module "instance-group" {
| [group](outputs.tf#L26) | Instance group resource. | |
| [instance](outputs.tf#L31) | Instance resource. | |
| [internal_ip](outputs.tf#L36) | Instance main interface internal IP address. | |
-| [self_link](outputs.tf#L44) | Instance self links. | |
-| [service_account](outputs.tf#L49) | Service account resource. | |
-| [service_account_email](outputs.tf#L56) | Service account email. | |
-| [service_account_iam_email](outputs.tf#L61) | Service account email. | |
-| [template](outputs.tf#L69) | Template resource. | |
-| [template_name](outputs.tf#L74) | Template name. | |
+| [internal_ips](outputs.tf#L44) | Instance interfaces internal IP addresses. | |
+| [self_link](outputs.tf#L52) | Instance self links. | |
+| [service_account](outputs.tf#L57) | Service account resource. | |
+| [service_account_email](outputs.tf#L64) | Service account email. | |
+| [service_account_iam_email](outputs.tf#L69) | Service account email. | |
+| [template](outputs.tf#L77) | Template resource. | |
+| [template_name](outputs.tf#L82) | Template name. | |
## TODO
diff --git a/modules/compute-vm/main.tf b/modules/compute-vm/main.tf
index 50c1fea421..31e3706727 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
})
}
@@ -30,7 +30,7 @@ locals {
k => v if try(v.options.replica_zone, null) == null
}
on_host_maintenance = (
- var.options.preemptible || var.confidential_compute
+ var.options.spot || var.confidential_compute
? "TERMINATE"
: "MIGRATE"
)
@@ -60,13 +60,7 @@ locals {
]
)
)
-
- network_interface_options = {
- for i, v in var.network_interfaces : i => lookup(var.network_interface_options, i, {
- alias_ips = null,
- nic_type = null
- })
- }
+ termination_action = var.options.spot ? coalesce(var.options.termination_action, "STOP") : null
}
resource "google_compute_disk" "disks" {
@@ -144,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"
@@ -158,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"
@@ -169,7 +163,7 @@ resource "google_compute_instance" "default" {
}
boot_disk {
- auto_delete = var.boot_disk_delete
+ auto_delete = var.boot_disk.auto_delete
initialize_params {
type = var.boot_disk.type
image = var.boot_disk.image
@@ -200,21 +194,23 @@ resource "google_compute_instance" "default" {
}
}
dynamic "alias_ip_range" {
- for_each = local.network_interface_options[config.key].alias_ips != null ? local.network_interface_options[config.key].alias_ips : {}
+ for_each = config.value.alias_ips
iterator = config_alias
content {
subnetwork_range_name = config_alias.key
ip_cidr_range = config_alias.value
}
}
- nic_type = local.network_interface_options[config.key].nic_type
+ nic_type = config.value.nic_type
}
}
scheduling {
- automatic_restart = !var.options.preemptible
- on_host_maintenance = local.on_host_maintenance
- preemptible = var.options.preemptible
+ automatic_restart = !var.options.spot
+ instance_termination_action = local.termination_action
+ on_host_maintenance = local.on_host_maintenance
+ preemptible = var.options.spot
+ provisioning_model = var.options.spot ? "SPOT" : "STANDARD"
}
dynamic "scratch_disk" {
@@ -270,7 +266,7 @@ resource "google_compute_instance_template" "default" {
labels = var.labels
disk {
- auto_delete = var.boot_disk_delete
+ auto_delete = var.boot_disk.auto_delete
boot = true
disk_size_gb = var.boot_disk.size
disk_type = var.boot_disk.type
@@ -288,8 +284,8 @@ resource "google_compute_instance_template" "default" {
for_each = local.attached_disks
iterator = config
content {
- # auto_delete = config.value.options.auto_delete
- device_name = config.value.name
+ auto_delete = config.value.options.auto_delete
+ 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 = (
@@ -309,6 +305,12 @@ resource "google_compute_instance_template" "default" {
config.value.source_type != "attach" ? config.value.name : null
)
type = "PERSISTENT"
+ dynamic "disk_encryption_key" {
+ for_each = var.encryption != null ? [""] : []
+ content {
+ kms_key_self_link = var.encryption.kms_key_self_link
+ }
+ }
}
}
@@ -326,21 +328,23 @@ resource "google_compute_instance_template" "default" {
}
}
dynamic "alias_ip_range" {
- for_each = local.network_interface_options[config.key].alias_ips != null ? local.network_interface_options[config.key].alias_ips : {}
+ for_each = config.value.alias_ips
iterator = config_alias
content {
subnetwork_range_name = config_alias.key
ip_cidr_range = config_alias.value
}
}
- nic_type = local.network_interface_options[config.key].nic_type
+ nic_type = config.value.nic_type
}
}
scheduling {
- automatic_restart = !var.options.preemptible
- on_host_maintenance = local.on_host_maintenance
- preemptible = var.options.preemptible
+ automatic_restart = !var.options.spot
+ instance_termination_action = local.termination_action
+ on_host_maintenance = local.on_host_maintenance
+ preemptible = var.options.spot
+ provisioning_model = var.options.spot ? "SPOT" : "STANDARD"
}
service_account {
@@ -348,6 +352,16 @@ resource "google_compute_instance_template" "default" {
scopes = local.service_account_scopes
}
+ dynamic "shielded_instance_config" {
+ for_each = var.shielded_config != null ? [var.shielded_config] : []
+ iterator = config
+ content {
+ enable_secure_boot = config.value.enable_secure_boot
+ enable_vtpm = config.value.enable_vtpm
+ enable_integrity_monitoring = config.value.enable_integrity_monitoring
+ }
+ }
+
lifecycle {
create_before_destroy = true
}
diff --git a/modules/compute-vm/outputs.tf b/modules/compute-vm/outputs.tf
index 523924f93b..32c9ef9cba 100644
--- a/modules/compute-vm/outputs.tf
+++ b/modules/compute-vm/outputs.tf
@@ -41,6 +41,14 @@ output "internal_ip" {
)
}
+output "internal_ips" {
+ description = "Instance interfaces internal IP addresses."
+ value = [
+ for nic in try(google_compute_instance.default.0.network_interface, [])
+ : nic.network_ip
+ ]
+}
+
output "self_link" {
description = "Instance self links."
value = try(google_compute_instance.default.0.self_link, null)
diff --git a/modules/compute-vm/variables.tf b/modules/compute-vm/variables.tf
index 653c66daf6..dc5651775d 100644
--- a/modules/compute-vm/variables.tf
+++ b/modules/compute-vm/variables.tf
@@ -17,6 +17,7 @@
variable "attached_disk_defaults" {
description = "Defaults for attached disks options."
type = object({
+ auto_delete = optional(bool, false)
mode = string
replica_zone = string
type = string
@@ -27,20 +28,35 @@ variable "attached_disk_defaults" {
replica_zone = null
type = "pd-balanced"
}
+
+ validation {
+ condition = var.attached_disk_defaults.mode == "READ_WRITE" || !var.attached_disk_defaults.auto_delete
+ error_message = "auto_delete can only be specified on READ_WRITE disks."
+ }
}
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 = string
- source_type = string
- options = object({
- mode = string
- replica_zone = string
- type = string
- })
+ source = optional(string)
+ source_type = optional(string)
+ options = optional(
+ object({
+ auto_delete = optional(bool, false)
+ mode = optional(string, "READ_WRITE")
+ replica_zone = optional(string)
+ type = optional(string, "pd-balanced")
+ }),
+ {
+ auto_delete = true
+ mode = "READ_WRITE"
+ replica_zone = null
+ type = "pd-balanced"
+ }
+ )
}))
default = []
validation {
@@ -53,28 +69,32 @@ variable "attached_disks" {
]) == length(var.attached_disks)
error_message = "Source type must be one of 'image', 'snapshot', 'attach', null."
}
+
+ validation {
+ condition = length([
+ for d in var.attached_disks : d if d.options == null ||
+ d.options.mode == "READ_WRITE" || !d.options.auto_delete
+ ]) == length(var.attached_disks)
+ error_message = "auto_delete can only be specified on READ_WRITE disks."
+ }
}
variable "boot_disk" {
description = "Boot disk properties."
type = object({
- image = string
- size = number
- type = string
+ auto_delete = optional(bool, true)
+ image = optional(string, "projects/debian-cloud/global/images/family/debian-11")
+ size = optional(number, 10)
+ type = optional(string, "pd-balanced")
})
default = {
- image = "projects/debian-cloud/global/images/family/debian-11"
- type = "pd-balanced"
- size = 10
+ auto_delete = true
+ image = "projects/debian-cloud/global/images/family/debian-11"
+ type = "pd-balanced"
+ size = 10
}
}
-variable "boot_disk_delete" {
- description = "Auto delete boot disk."
- type = bool
- default = true
-}
-
variable "can_ip_forward" {
description = "Enable IP forwarding."
type = bool
@@ -97,6 +117,7 @@ variable "description" {
type = string
default = "Managed by the compute-vm Terraform module."
}
+
variable "enable_display" {
description = "Enable virtual display on the instances."
type = bool
@@ -106,9 +127,9 @@ variable "enable_display" {
variable "encryption" {
description = "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."
type = object({
- encrypt_boot = bool
- disk_encryption_key_raw = string
- kms_key_self_link = string
+ encrypt_boot = optional(bool, false)
+ disk_encryption_key_raw = optional(string)
+ kms_key_self_link = optional(string)
})
default = null
}
@@ -162,39 +183,40 @@ variable "name" {
type = string
}
-variable "network_interface_options" {
- description = "Network interfaces extended options. The key is the index of the inteface to configure. The value is an object with alias_ips and nic_type. Set alias_ips or nic_type to null if you need only one of them."
- type = map(object({
- alias_ips = map(string)
- nic_type = string
- }))
- default = {}
-}
-
variable "network_interfaces" {
description = "Network interfaces configuration. Use self links for Shared VPC, set addresses to null if not needed."
type = list(object({
- nat = bool
+ nat = optional(bool, false)
network = string
subnetwork = string
- addresses = object({
+ addresses = optional(object({
internal = string
external = string
- })
+ }), null)
+ alias_ips = optional(map(string), {})
+ nic_type = optional(string)
}))
}
variable "options" {
description = "Instance options."
type = object({
- allow_stopping_for_update = bool
- deletion_protection = bool
- preemptible = bool
+ allow_stopping_for_update = optional(bool, true)
+ deletion_protection = optional(bool, false)
+ spot = optional(bool, false)
+ termination_action = optional(string)
})
default = {
allow_stopping_for_update = true
deletion_protection = false
- preemptible = false
+ spot = false
+ termination_action = null
+ }
+ validation {
+ condition = (var.options.termination_action == null
+ ||
+ contains(["STOP", "DELETE"], coalesce(var.options.termination_action, "1")))
+ error_message = "Allowed values for options.termination_action are 'STOP', 'DELETE' and null."
}
}
@@ -261,5 +283,3 @@ variable "zone" {
description = "Compute zone."
type = string
}
-
-
diff --git a/modules/compute-vm/versions.tf b/modules/compute-vm/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/compute-vm/versions.tf
+++ b/modules/compute-vm/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/container-registry/README.md b/modules/container-registry/README.md
index c4baff7636..bf04997024 100644
--- a/modules/container-registry/README.md
+++ b/modules/container-registry/README.md
@@ -6,7 +6,7 @@ This module simplifies the creation of GCS buckets used by Google Container Regi
```hcl
module "container_registry" {
- source = "./modules/container-registry"
+ source = "./fabric/modules/container-registry"
project_id = "myproject"
location = "EU"
iam = {
diff --git a/modules/container-registry/versions.tf b/modules/container-registry/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/container-registry/versions.tf
+++ b/modules/container-registry/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/data-catalog-policy-tag/README.md b/modules/data-catalog-policy-tag/README.md
index 5d41f72319..3cf4aaafce 100644
--- a/modules/data-catalog-policy-tag/README.md
+++ b/modules/data-catalog-policy-tag/README.md
@@ -9,10 +9,10 @@ Note: Data Catalog is still in beta, hence this module currently uses the beta p
```hcl
module "cmn-dc" {
- source = "./modules/data-catalog-policy-tag"
+ source = "./fabric/modules/data-catalog-policy-tag"
name = "my-datacatalog-policy-tags"
project_id = "my-project"
- tags = {
+ tags = {
low = null, medium = null, high = null
}
}
@@ -23,13 +23,13 @@ module "cmn-dc" {
```hcl
module "cmn-dc" {
- source = "./modules/data-catalog-policy-tag"
+ 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"]
@@ -44,7 +44,7 @@ module "cmn-dc" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [name](variables.tf#L59) | Name of this taxonomy. | string
| ✓ | |
-| [project_id](variables.tf#L70) | GCP project id. |
| ✓ | |
+| [project_id](variables.tf#L74) | GCP project id. |
| ✓ | |
| [activated_policy_types](variables.tf#L17) | A list of policy types that are activated for this taxonomy. | list(string)
| | ["FINE_GRAINED_ACCESS_CONTROL"]
|
| [description](variables.tf#L23) | Description of this taxonomy. | string
| | "Taxonomy - Terraform managed"
|
| [group_iam](variables.tf#L29) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string))
| | {}
|
@@ -52,8 +52,8 @@ module "cmn-dc" {
| [iam_additive](variables.tf#L41) | IAM additive bindings in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
| [iam_additive_members](variables.tf#L47) | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | map(list(string))
| | {}
|
| [location](variables.tf#L53) | Data Catalog Taxonomy location. | string
| | "eu"
|
-| [prefix](variables.tf#L64) | Prefix used to generate project id and name. | string
| | null
|
-| [tags](variables.tf#L74) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(map(list(string)))
| | {}
|
+| [prefix](variables.tf#L64) | Optional prefix used to generate project id and name. | string
| | null
|
+| [tags](variables.tf#L78) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(map(list(string)))
| | {}
|
## Outputs
@@ -65,4 +65,4 @@ module "cmn-dc" {
## TODO
- Support IAM at tag level.
-- Support Child policy tags
\ No newline at end of file
+- Support Child policy tags
diff --git a/modules/data-catalog-policy-tag/outputs.tf b/modules/data-catalog-policy-tag/outputs.tf
index fcd5cc292b..1f0bb2420f 100644
--- a/modules/data-catalog-policy-tag/outputs.tf
+++ b/modules/data-catalog-policy-tag/outputs.tf
@@ -16,7 +16,7 @@
output "tags" {
description = "Policy Tags."
- value = { for k, v in google_data_catalog_policy_tag.default : v.id => v.name }
+ value = { for k, v in google_data_catalog_policy_tag.default : k => v.id }
}
output "taxonomy_id" {
diff --git a/modules/data-catalog-policy-tag/variables.tf b/modules/data-catalog-policy-tag/variables.tf
index ad1be567fe..2342e94747 100644
--- a/modules/data-catalog-policy-tag/variables.tf
+++ b/modules/data-catalog-policy-tag/variables.tf
@@ -62,9 +62,13 @@ variable "name" {
}
variable "prefix" {
- description = "Prefix used to generate project id and name."
+ description = "Optional prefix used to generate project id and name."
type = string
default = null
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty, please use null instead."
+ }
}
variable "project_id" {
diff --git a/modules/data-catalog-policy-tag/versions.tf b/modules/data-catalog-policy-tag/versions.tf
index e72a78007a..90b632f6d4 100644
--- a/modules/data-catalog-policy-tag/versions.tf
+++ b/modules/data-catalog-policy-tag/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.1.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/datafusion/README.md b/modules/datafusion/README.md
index 20414c6b79..377e81452a 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 = "./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
}
@@ -23,7 +23,7 @@ module "datafusion" {
```hcl
module "datafusion" {
- source = "./modules/datafusion"
+ source = "./fabric/modules/datafusion"
name = "my-datafusion"
region = "europe-west1"
project_id = "my-project"
diff --git a/modules/datafusion/versions.tf b/modules/datafusion/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/datafusion/versions.tf
+++ b/modules/datafusion/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/dns/README.md b/modules/dns/README.md
index ae91dbbcdc..9e461f0e51 100644
--- a/modules/dns/README.md
+++ b/modules/dns/README.md
@@ -1,6 +1,6 @@
# Google Cloud DNS Module
-This module allows simple management of Google Cloud DNS zones and records. It supports creating public, private, forwarding, peering and service directory based zones.
+This module allows simple management of Google Cloud DNS zones and records. It supports creating public, private, forwarding, peering, service directory and reverse-managed based zones.
For DNSSEC configuration, refer to the [`dns_managed_zone` documentation](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/dns_managed_zone#dnssec_config).
@@ -10,24 +10,25 @@ For DNSSEC configuration, refer to the [`dns_managed_zone` documentation](https:
```hcl
module "private-dns" {
- source = "./modules/dns"
+ source = "./fabric/modules/dns"
project_id = "myproject"
type = "private"
name = "test-example"
domain = "test.example."
client_networks = [var.vpc.self_link]
recordsets = {
- "A localhost" = { ttl = 300, records = ["127.0.0.1"] }
+ "A localhost" = { records = ["127.0.0.1"] }
+ "A myhost" = { ttl = 600, records = ["10.0.0.120"] }
}
}
-# tftest modules=1 resources=2
+# tftest modules=1 resources=3
```
### Forwarding Zone
```hcl
module "private-dns" {
- source = "./modules/dns"
+ source = "./fabric/modules/dns"
project_id = "myproject"
type = "forwarding"
name = "test-example"
@@ -42,7 +43,7 @@ module "private-dns" {
```hcl
module "private-dns" {
- source = "./modules/dns"
+ source = "./fabric/modules/dns"
project_id = "myproject"
type = "peering"
name = "test-example"
@@ -52,26 +53,72 @@ module "private-dns" {
}
# tftest modules=1 resources=1
```
+
+### Routing Policies
+
+```hcl
+module "private-dns" {
+ source = "./fabric/modules/dns"
+ project_id = "myproject"
+ type = "private"
+ name = "test-example"
+ domain = "test.example."
+ client_networks = [var.vpc.self_link]
+ recordsets = {
+ "A regular" = { records = ["10.20.0.1"] }
+ "A geo" = {
+ geo_routing = [
+ { location = "europe-west1", records = ["10.0.0.1"] },
+ { location = "europe-west2", records = ["10.0.0.2"] },
+ { location = "europe-west3", records = ["10.0.0.3"] }
+ ]
+ }
+
+ "A wrr" = {
+ ttl = 600
+ wrr_routing = [
+ { weight = 0.6, records = ["10.10.0.1"] },
+ { weight = 0.2, records = ["10.10.0.2"] },
+ { weight = 0.2, records = ["10.10.0.3"] }
+ ]
+ }
+ }
+}
+# tftest modules=1 resources=4
+```
+
+### Reverse Lookup Zone
+
+```hcl
+module "private-dns" {
+ source = "./fabric/modules/dns"
+ project_id = "myproject"
+ type = "reverse-managed"
+ name = "test-example"
+ domain = "0.0.10.in-addr.arpa."
+ client_networks = [var.vpc.self_link]
+}
+# tftest modules=1 resources=1
+```
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [domain](variables.tf#L51) | Zone domain, must end with a period. | string
| ✓ | |
-| [name](variables.tf#L62) | Zone name, must be unique within the project. | string
| ✓ | |
-| [project_id](variables.tf#L73) | Project id for the zone. | string
| ✓ | |
+| [domain](variables.tf#L54) | Zone domain, must end with a period. | string
| ✓ | |
+| [name](variables.tf#L72) | Zone name, must be unique within the project. | string
| ✓ | |
+| [project_id](variables.tf#L83) | Project id for the zone. | string
| ✓ | |
| [client_networks](variables.tf#L21) | List of VPC self links that can see this zone. | list(string)
| | []
|
-| [default_key_specs_key](variables.tf#L27) | DNSSEC default key signing specifications: algorithm, key_length, key_type, kind. | any
| | {}
|
-| [default_key_specs_zone](variables.tf#L33) | DNSSEC default zone signing specifications: algorithm, key_length, key_type, kind. | any
| | {}
|
-| [description](variables.tf#L39) | Domain description. | string
| | "Terraform managed."
|
-| [dnssec_config](variables.tf#L45) | DNSSEC configuration: kind, non_existence, state. | any
| | {}
|
-| [forwarders](variables.tf#L56) | Map of {IPV4_ADDRESS => FORWARDING_PATH} for 'forwarding' zone types. Path can be 'default', 'private', or null for provider default. | map(string)
| | {}
|
-| [peer_network](variables.tf#L67) | Peering network self link, only valid for 'peering' zone types. | string
| | null
|
-| [recordsets](variables.tf#L78) | Map of DNS recordsets in \"type name\" => {ttl, [records]} format. | map(object({…}))
| | {}
|
-| [service_directory_namespace](variables.tf#L94) | Service directory namespace id (URL), only valid for 'service-directory' zone types. | string
| | null
|
-| [type](variables.tf#L100) | Type of zone to create, valid values are 'public', 'private', 'forwarding', 'peering', 'service-directory'. | string
| | "private"
|
-| [zone_create](variables.tf#L110) | Create zone. When set to false, uses a data source to reference existing zone. | bool
| | true
|
+| [description](variables.tf#L28) | Domain description. | string
| | "Terraform managed."
|
+| [dnssec_config](variables.tf#L34) | DNSSEC configuration for this zone. | object({…})
| | {…}
|
+| [enable_logging](variables.tf#L59) | Enable query logging for this zone. Only valid for public zones. | bool
| | false
|
+| [forwarders](variables.tf#L66) | Map of {IPV4_ADDRESS => FORWARDING_PATH} for 'forwarding' zone types. Path can be 'default', 'private', or null for provider default. | map(string)
| | {}
|
+| [peer_network](variables.tf#L77) | Peering network self link, only valid for 'peering' zone types. | string
| | null
|
+| [recordsets](variables.tf#L88) | Map of DNS recordsets in \"type name\" => {ttl, [records]} format. | map(object({…}))
| | {}
|
+| [service_directory_namespace](variables.tf#L123) | Service directory namespace id (URL), only valid for 'service-directory' zone types. | string
| | null
|
+| [type](variables.tf#L129) | Type of zone to create, valid values are 'public', 'private', 'forwarding', 'peering', 'service-directory','reverse-managed'. | string
| | "private"
|
+| [zone_create](variables.tf#L139) | Create zone. When set to false, uses a data source to reference existing zone. | bool
| | true
|
## Outputs
diff --git a/modules/dns/main.tf b/modules/dns/main.tf
index c2b567d2b3..ca30c7d0c7 100644
--- a/modules/dns/main.tf
+++ b/modules/dns/main.tf
@@ -15,11 +15,42 @@
*/
locals {
- _recordsets = var.recordsets == null ? {} : var.recordsets
- recordsets = {
- for key, attrs in local._recordsets :
+ # split record name and type and set as keys in a map
+ _recordsets_0 = {
+ for key, attrs in var.recordsets :
key => merge(attrs, zipmap(["type", "name"], split(" ", key)))
}
+ # compute the final resource name for the recordset
+ _recordsets = {
+ for key, attrs in local._recordsets_0 :
+ key => merge(attrs, {
+ resource_name = (
+ attrs.name == ""
+ ? var.domain
+ : (
+ substr(attrs.name, -1, 1) == "."
+ ? attrs.name
+ : "${attrs.name}.${var.domain}"
+ )
+ )
+ })
+ }
+ # split recordsets between regular, geo and wrr
+ geo_recordsets = {
+ for k, v in local._recordsets :
+ k => v
+ if v.geo_routing != null
+ }
+ regular_recordsets = {
+ for k, v in local._recordsets :
+ k => v
+ if v.records != null
+ }
+ wrr_recordsets = {
+ for k, v in local._recordsets :
+ k => v
+ if v.wrr_routing != null
+ }
zone = (
var.zone_create
? try(
@@ -35,13 +66,14 @@ locals {
}
resource "google_dns_managed_zone" "non-public" {
- count = (var.zone_create && var.type != "public") ? 1 : 0
- provider = google-beta
- project = var.project_id
- name = var.name
- dns_name = var.domain
- description = var.description
- visibility = "private"
+ count = (var.zone_create && var.type != "public") ? 1 : 0
+ provider = google-beta
+ project = var.project_id
+ name = var.name
+ dns_name = var.domain
+ description = var.description
+ visibility = "private"
+ reverse_lookup = (var.type == "reverse-managed")
dynamic "forwarding_config" {
for_each = (
@@ -103,8 +135,9 @@ resource "google_dns_managed_zone" "non-public" {
}
data "google_dns_managed_zone" "public" {
- count = var.zone_create ? 0 : 1
- name = var.name
+ count = var.zone_create ? 0 : 1
+ project = var.project_id
+ name = var.name
}
resource "google_dns_managed_zone" "public" {
@@ -116,24 +149,25 @@ resource "google_dns_managed_zone" "public" {
visibility = "public"
dynamic "dnssec_config" {
- for_each = var.dnssec_config == {} ? [] : tolist([var.dnssec_config])
+ for_each = var.dnssec_config == null ? [] : [1]
iterator = config
content {
- kind = lookup(config.value, "kind", "dns#managedZoneDnsSecConfig")
- non_existence = lookup(config.value, "non_existence", "nsec3")
- state = lookup(config.value, "state", "off")
+ kind = "dns#managedZoneDnsSecConfig"
+ non_existence = var.dnssec_config.non_existence
+ state = var.dnssec_config.state
default_key_specs {
- algorithm = lookup(var.default_key_specs_key, "algorithm", "rsasha256")
- key_length = lookup(var.default_key_specs_key, "key_length", 2048)
- key_type = lookup(var.default_key_specs_key, "key_type", "keySigning")
- kind = lookup(var.default_key_specs_key, "kind", "dns#dnsKeySpec")
+ algorithm = var.dnssec_config.key_signing_key.algorithm
+ key_length = var.dnssec_config.key_signing_key.key_length
+ key_type = "keySigning"
+ kind = "dns#dnsKeySpec"
}
+
default_key_specs {
- algorithm = lookup(var.default_key_specs_zone, "algorithm", "rsasha256")
- key_length = lookup(var.default_key_specs_zone, "key_length", 1024)
- key_type = lookup(var.default_key_specs_zone, "key_type", "zoneSigning")
- kind = lookup(var.default_key_specs_zone, "kind", "dns#dnsKeySpec")
+ algorithm = var.dnssec_config.zone_signing_key.algorithm
+ key_length = var.dnssec_config.zone_signing_key.key_length
+ key_type = "zoneSigning"
+ kind = "dns#dnsKeySpec"
}
}
}
@@ -148,23 +182,72 @@ data "google_dns_keys" "dns_keys" {
resource "google_dns_record_set" "cloud-static-records" {
for_each = (
var.type == "public" || var.type == "private"
- ? local.recordsets
+ ? local.regular_recordsets
: {}
)
project = var.project_id
managed_zone = var.name
- name = (
- each.value.name == ""
- ? var.domain
- : (
- substr(each.value.name, -1, 1) == "."
- ? each.value.name
- : "${each.value.name}.${var.domain}"
- )
+ name = each.value.resource_name
+ type = each.value.type
+ ttl = each.value.ttl
+ rrdatas = each.value.records
+
+ depends_on = [
+ google_dns_managed_zone.non-public, google_dns_managed_zone.public
+ ]
+}
+
+resource "google_dns_record_set" "cloud-geo-records" {
+ for_each = (
+ var.type == "public" || var.type == "private"
+ ? local.geo_recordsets
+ : {}
)
- type = each.value.type
- ttl = each.value.ttl
- rrdatas = each.value.records
+ project = var.project_id
+ managed_zone = var.name
+ name = each.value.resource_name
+ type = each.value.type
+ ttl = each.value.ttl
+
+ routing_policy {
+ dynamic "geo" {
+ for_each = each.value.geo_routing
+ iterator = policy
+ content {
+ location = policy.value.location
+ rrdatas = policy.value.records
+ }
+ }
+ }
+
+ depends_on = [
+ google_dns_managed_zone.non-public, google_dns_managed_zone.public
+ ]
+}
+
+resource "google_dns_record_set" "cloud-wrr-records" {
+ for_each = (
+ var.type == "public" || var.type == "private"
+ ? local.wrr_recordsets
+ : {}
+ )
+ project = var.project_id
+ managed_zone = var.name
+ name = each.value.resource_name
+ type = each.value.type
+ ttl = each.value.ttl
+
+ routing_policy {
+ dynamic "wrr" {
+ for_each = each.value.wrr_routing
+ iterator = policy
+ content {
+ weight = policy.value.weight
+ rrdatas = policy.value.records
+ }
+ }
+ }
+
depends_on = [
google_dns_managed_zone.non-public, google_dns_managed_zone.public
]
diff --git a/modules/dns/variables.tf b/modules/dns/variables.tf
index ba44c7d84f..df80976e32 100644
--- a/modules/dns/variables.tf
+++ b/modules/dns/variables.tf
@@ -22,18 +22,7 @@ variable "client_networks" {
description = "List of VPC self links that can see this zone."
type = list(string)
default = []
-}
-
-variable "default_key_specs_key" {
- description = "DNSSEC default key signing specifications: algorithm, key_length, key_type, kind."
- type = any
- default = {}
-}
-
-variable "default_key_specs_zone" {
- description = "DNSSEC default zone signing specifications: algorithm, key_length, key_type, kind."
- type = any
- default = {}
+ nullable = false
}
variable "description" {
@@ -43,9 +32,23 @@ variable "description" {
}
variable "dnssec_config" {
- description = "DNSSEC configuration: kind, non_existence, state."
- type = any
- default = {}
+ description = "DNSSEC configuration for this zone."
+ type = object({
+ non_existence = optional(string, "nsec3")
+ state = string
+ key_signing_key = optional(object(
+ { algorithm = string, key_length = number }),
+ { algorithm = "rsasha256", key_length = 2048 }
+ )
+ zone_signing_key = optional(object(
+ { algorithm = string, key_length = number }),
+ { algorithm = "rsasha256", key_length = 1024 }
+ )
+ })
+ default = {
+ state = "off"
+ }
+ nullable = false
}
variable "domain" {
@@ -53,6 +56,13 @@ variable "domain" {
type = string
}
+variable "enable_logging" {
+ description = "Enable query logging for this zone. Only valid for public zones."
+ type = bool
+ default = false
+ nullable = false
+}
+
variable "forwarders" {
description = "Map of {IPV4_ADDRESS => FORWARDING_PATH} for 'forwarding' zone types. Path can be 'default', 'private', or null for provider default."
type = map(string)
@@ -78,17 +88,36 @@ variable "project_id" {
variable "recordsets" {
description = "Map of DNS recordsets in \"type name\" => {ttl, [records]} format."
type = map(object({
- ttl = number
- records = list(string)
+ ttl = optional(number, 300)
+ records = optional(list(string))
+ geo_routing = optional(list(object({
+ location = string
+ records = list(string)
+ })))
+ wrr_routing = optional(list(object({
+ weight = number
+ records = list(string)
+ })))
}))
- default = {}
+ default = {}
+ nullable = false
validation {
condition = alltrue([
- for k, v in var.recordsets == null ? {} : var.recordsets :
+ for k, v in coalesce(var.recordsets, {}) :
length(split(" ", k)) == 2
])
error_message = "Recordsets must have keys in the format \"type name\"."
}
+ validation {
+ condition = alltrue([
+ for k, v in coalesce(var.recordsets, {}) : (
+ (v.records != null && v.wrr_routing == null && v.geo_routing == null) ||
+ (v.records == null && v.wrr_routing != null && v.geo_routing == null) ||
+ (v.records == null && v.wrr_routing == null && v.geo_routing != null)
+ )
+ ])
+ error_message = "Only one of records, wrr_routing or geo_routing can be defined for each recordset."
+ }
}
variable "service_directory_namespace" {
@@ -98,12 +127,12 @@ variable "service_directory_namespace" {
}
variable "type" {
- description = "Type of zone to create, valid values are 'public', 'private', 'forwarding', 'peering', 'service-directory'."
+ description = "Type of zone to create, valid values are 'public', 'private', 'forwarding', 'peering', 'service-directory','reverse-managed'."
type = string
default = "private"
validation {
- condition = contains(["public", "private", "forwarding", "peering", "service-directory"], var.type)
- error_message = "Zone must be one of 'public', 'private', 'forwarding', 'peering', 'service-directory'."
+ condition = contains(["public", "private", "forwarding", "peering", "service-directory", "reverse-managed"], var.type)
+ error_message = "Zone must be one of 'public', 'private', 'forwarding', 'peering', 'service-directory','reverse-managed'."
}
}
diff --git a/modules/dns/versions.tf b/modules/dns/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/dns/versions.tf
+++ b/modules/dns/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/endpoints/README.md b/modules/endpoints/README.md
index a82d14db55..3b9e317db0 100644
--- a/modules/endpoints/README.md
+++ b/modules/endpoints/README.md
@@ -8,17 +8,27 @@ This module allows simple management of ['Google Cloud Endpoints'](https://cloud
```hcl
module "endpoint" {
- source = "./modules/endpoints"
+ source = "./fabric/modules/endpoints"
project_id = "my-project"
service_name = "YOUR-API.endpoints.YOUR-PROJECT-ID.cloud.goog"
- openapi_config = { "yaml_path" = "openapi.yaml" }
+ openapi_config = { "yaml_path" = "configs/endpoints/openapi.yaml" }
iam = {
"servicemanagement.serviceController" = [
"serviceAccount:123456890-compute@developer.gserviceaccount.com"
]
}
}
-# tftest skip
+# tftest modules=1 resources=2 files=openapi
+```
+
+```yaml
+# tftest-file id=openapi path=configs/endpoints/openapi.yaml
+swagger: "2.0"
+info:
+ description: "A simple Google Cloud Endpoints API example."
+ title: "Endpoints Example"
+ version: "1.0.0"
+host: "echo-api.endpoints.YOUR-PROJECT-ID.cloud.goog"
```
[Here](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/endpoints/getting-started/openapi.yaml) you can find an example of an openapi.yaml file. Once created the endpoint, remember to activate the service at project level.
diff --git a/modules/endpoints/versions.tf b/modules/endpoints/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/endpoints/versions.tf
+++ b/modules/endpoints/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/folder/README.md b/modules/folder/README.md
index 2e5b8b5aa8..e1ad6809e2 100644
--- a/modules/folder/README.md
+++ b/modules/folder/README.md
@@ -2,191 +2,185 @@
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 = "./modules/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"]
}
-}
-# tftest modules=1 resources=3
-```
-
-### Organization policies
-
-```hcl
-module "folder" {
- source = "./modules/folder"
- parent = "organizations/1234567890"
- name = "Folder name"
- policy_boolean = {
- "constraints/compute.disableGuestAttributesAccess" = true
- "constraints/compute.skipDefaultNetworkCreation" = true
+ iam_additive = {
+ "roles/compute.admin" = ["user:a1@example.org", "user:a2@example.org"]
+ "roles/compute.viewer" = ["user:a2@example.org"]
}
- policy_list = {
- "constraints/compute.trustedImageProjects" = {
- inherit_from_parent = null
- suggested_value = null
- status = true
- values = ["projects/my-project"]
- }
+ iam_additive_members = {
+ "user:am1@example.org" = ["roles/storage.admin"]
+ "user:am2@example.org" = ["roles/storage.objectViewer"]
}
}
-# tftest modules=1 resources=4
+# tftest modules=1 resources=9 inventory=iam.yaml
```
-### Firewall policy factory
+## Organization policies
-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`).
+To manage organization policies, the `orgpolicy.googleapis.com` service should be enabled in the quota project.
```hcl
module "folder" {
- source = "./modules/folder"
+ source = "./fabric/modules/folder"
parent = "organizations/1234567890"
- name = "Folder name"
- firewall_policy_factory = {
- cidr_file = "data/cidrs.yaml"
- policy_name = null
- rules_file = "data/rules.yaml"
- }
- firewall_policy_association = {
- factory-policy = module.folder.firewall_policy_id["factory"]
+ name = "Folder name"
+ org_policies = {
+ "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 = "somewhere"
+ }
+ enforce = true
+ }
+ ]
+ }
+ "constraints/iam.allowedPolicyMemberDomains" = {
+ allow = {
+ values = ["C0xxxxxxx", "C0yyyyyyy"]
+ }
+ }
+ "constraints/compute.trustedImageProjects" = {
+ allow = {
+ values = ["projects/my-project"]
+ }
+ }
+ "constraints/compute.vmExternalIpAccess" = {
+ deny = { all = true }
+ }
}
}
-# tftest skip
-```
-
-```yaml
-# cidrs.yaml
-
-rfc1918:
- - 10.0.0.0/8
- - 172.16.0.0/12
- - 192.168.0.0/16
+# tftest modules=1 resources=8 inventory=org-policies.yaml
```
-```yaml
-# 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
+### Organization policy factory
-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
-```
+See the [organization policy factory in the project module](../project#organization-policy-factory).
-### Logging Sinks
+## Logging Sinks
```hcl
module "gcs" {
- source = "./modules/gcs"
+ source = "./fabric/modules/gcs"
project_id = "my-project"
name = "gcs_sink"
force_destroy = true
}
module "dataset" {
- source = "./modules/bigquery-dataset"
+ source = "./fabric/modules/bigquery-dataset"
project_id = "my-project"
id = "bq_sink"
}
module "pubsub" {
- source = "./modules/pubsub"
+ source = "./fabric/modules/pubsub"
project_id = "my-project"
name = "pubsub_sink"
}
module "bucket" {
- source = "./modules/logging-bucket"
+ source = "./fabric/modules/logging-bucket"
parent_type = "project"
parent = "my-project"
id = "bucket"
}
module "folder-sink" {
- source = "./modules/folder"
+ source = "./fabric/modules/folder"
parent = "folders/657104291943"
name = "my-folder"
logging_sinks = {
warnings = {
- type = "storage"
- destination = module.gcs.name
- filter = "severity=WARNING"
- include_children = true
- exclusions = {}
+ destination = module.gcs.id
+ filter = "severity=WARNING"
+ type = "storage"
}
info = {
- type = "bigquery"
- destination = module.dataset.id
- filter = "severity=INFO"
- include_children = true
- exclusions = {}
+ destination = module.dataset.id
+ filter = "severity=INFO"
+ type = "bigquery"
}
notice = {
- type = "pubsub"
- destination = module.pubsub.id
- filter = "severity=NOTICE"
- include_children = true
- exclusions = {}
+ destination = module.pubsub.id
+ filter = "severity=NOTICE"
+ type = "pubsub"
}
debug = {
- type = "logging"
- destination = module.bucket.id
- filter = "severity=DEBUG"
- include_children = true
+ destination = module.bucket.id
+ filter = "severity=DEBUG"
exclusions = {
no-compute = "logName:compute"
}
+ type = "logging"
}
}
logging_exclusions = {
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" {
- source = "./modules/folder"
+ source = "./fabric/modules/folder"
parent = var.organization_id
name = "policy-container"
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"
@@ -206,14 +200,78 @@ module "folder1" {
}
module "folder2" {
- source = "./modules/folder"
+ 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 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=6
+# 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
@@ -222,12 +280,12 @@ Refer to the [Creating and managing tags](https://cloud.google.com/resource-mana
```hcl
module "org" {
- source = "./modules/organization"
+ source = "./fabric/modules/organization"
organization_id = var.organization_id
tags = {
environment = {
- description = "Environment specification."
- iam = null
+ description = "Environment specification."
+ iam = null
values = {
dev = null
prod = null
@@ -237,7 +295,7 @@ module "org" {
}
module "folder" {
- source = "./modules/folder"
+ source = "./fabric/modules/folder"
name = "Test"
parent = module.org.organization_id
tag_bindings = {
@@ -245,7 +303,7 @@ module "folder" {
foo = "tagValues/12345678"
}
}
-# tftest modules=2 resources=6
+# tftest modules=2 resources=6 inventory=tags.yaml
```
@@ -259,7 +317,7 @@ module "folder" {
| [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | google_folder_iam_binding
· google_folder_iam_member
|
| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member
· google_logging_folder_exclusion
· google_logging_folder_sink
· google_project_iam_member
· google_pubsub_topic_iam_member
· google_storage_bucket_iam_member
|
| [main.tf](./main.tf) | Module-level locals and resources. | google_essential_contacts_contact
· google_folder
|
-| [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | google_folder_organization_policy
|
+| [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | google_org_policy_policy
|
| [outputs.tf](./outputs.tf) | Module outputs. | |
| [tags.tf](./tags.tf) | None | google_tags_tag_binding
|
| [variables.tf](./variables.tf) | Module variables. | |
@@ -280,12 +338,12 @@ module "folder" {
| [iam_additive_members](variables.tf#L85) | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | map(list(string))
| | {}
|
| [id](variables.tf#L92) | Folder ID in case you use folder_create=false. | string
| | null
|
| [logging_exclusions](variables.tf#L98) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string)
| | {}
|
-| [logging_sinks](variables.tf#L105) | Logging sinks to create for this folder. | map(object({…}))
| | {}
|
-| [name](variables.tf#L126) | Folder name. | string
| | null
|
-| [parent](variables.tf#L132) | Parent in folders/folder_id or organizations/org_id format. | string
| | null
|
-| [policy_boolean](variables.tf#L142) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool)
| | {}
|
-| [policy_list](variables.tf#L149) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…}))
| | {}
|
-| [tag_bindings](variables.tf#L161) | Tag bindings for this folder, in key => tag value id format. | map(string)
| | null
|
+| [logging_sinks](variables.tf#L105) | Logging sinks to create for the organization. | map(object({…}))
| | {}
|
+| [name](variables.tf#L135) | Folder name. | string
| | null
|
+| [org_policies](variables.tf#L141) | Organization policies applied to this folder keyed by policy name. | map(object({…}))
| | {}
|
+| [org_policies_data_path](variables.tf#L181) | Path containing org policies in YAML format. | string
| | null
|
+| [parent](variables.tf#L187) | Parent in folders/folder_id or organizations/org_id format. | string
| | null
|
+| [tag_bindings](variables.tf#L197) | Tag bindings for this folder, in key => tag value id format. | map(string)
| | null
|
## Outputs
@@ -295,7 +353,7 @@ module "folder" {
| [firewall_policy_id](outputs.tf#L21) | Map of firewall policy ids created in this folder. | |
| [folder](outputs.tf#L26) | Folder resource. | |
| [id](outputs.tf#L31) | Folder id. | |
-| [name](outputs.tf#L41) | Folder name. | |
-| [sink_writer_identities](outputs.tf#L46) | Writer identities created for each sink. | |
+| [name](outputs.tf#L40) | Folder name. | |
+| [sink_writer_identities](outputs.tf#L45) | Writer identities created for each sink. | |
diff --git a/modules/folder/logging.tf b/modules/folder/logging.tf
index d6a195e1ec..2b5a73fffa 100644
--- a/modules/folder/logging.tf
+++ b/modules/folder/logging.tf
@@ -28,13 +28,21 @@ locals {
}
resource "google_logging_folder_sink" "sink" {
- for_each = var.logging_sinks
- name = each.key
- #description = "${each.key} (Terraform-managed)."
+ for_each = var.logging_sinks
+ name = each.key
+ description = coalesce(each.value.description, "${each.key} (Terraform-managed).")
folder = local.folder.name
destination = "${each.value.type}.googleapis.com/${each.value.destination}"
filter = each.value.filter
include_children = each.value.include_children
+ disabled = each.value.disabled
+
+ dynamic "bigquery_options" {
+ for_each = each.value.type == "biquery" && each.value.bq_partitioned_table != false ? [""] : []
+ content {
+ use_partitioned_tables = each.value.bq_partitioned_table
+ }
+ }
dynamic "exclusions" {
for_each = each.value.exclusions
@@ -78,8 +86,12 @@ resource "google_project_iam_member" "bucket-sinks-binding" {
project = split("/", each.value.destination)[1]
role = "roles/logging.bucketWriter"
member = google_logging_folder_sink.sink[each.key].writer_identity
- # TODO(jccb): use a condition to limit writer-identity only to this
- # bucket
+
+ condition {
+ title = "${each.key} bucket writer"
+ description = "Grants bucketWriter to ${google_logging_folder_sink.sink[each.key].writer_identity} used by log sink ${each.key} on ${local.folder.id}"
+ expression = "resource.name.endsWith('${each.value.destination}')"
+ }
}
resource "google_logging_folder_exclusion" "logging-exclusion" {
diff --git a/modules/folder/organization-policies.tf b/modules/folder/organization-policies.tf
index 177a3d8041..47532f21be 100644
--- a/modules/folder/organization-policies.tf
+++ b/modules/folder/organization-policies.tf
@@ -16,75 +16,127 @@
# tfdoc:file:description Folder-level organization policies.
-resource "google_folder_organization_policy" "boolean" {
- for_each = var.policy_boolean
- folder = local.folder.name
- constraint = each.key
+locals {
+ _factory_data_raw = merge([
+ for f in try(fileset(var.org_policies_data_path, "*.yaml"), []) :
+ yamldecode(file("${var.org_policies_data_path}/${f}"))
+ ]...)
- dynamic "boolean_policy" {
- for_each = each.value == null ? [] : [each.value]
- iterator = policy
- content {
- enforced = policy.value
+ # simulate applying defaults to data coming from yaml files
+ _factory_data = {
+ for k, v in local._factory_data_raw :
+ k => {
+ inherit_from_parent = try(v.inherit_from_parent, null)
+ reset = try(v.reset, null)
+ allow = can(v.allow) ? {
+ all = try(v.allow.all, null)
+ values = try(v.allow.values, null)
+ } : null
+ deny = can(v.deny) ? {
+ all = try(v.deny.all, null)
+ values = try(v.deny.values, null)
+ } : null
+ enforce = try(v.enforce, true)
+
+ rules = [
+ for r in try(v.rules, []) : {
+ allow = can(r.allow) ? {
+ all = try(r.allow.all, null)
+ values = try(r.allow.values, null)
+ } : null
+ deny = can(r.deny) ? {
+ all = try(r.deny.all, null)
+ values = try(r.deny.values, null)
+ } : null
+ enforce = try(r.enforce, true)
+ condition = {
+ description = try(r.condition.description, null)
+ expression = try(r.condition.expression, null)
+ location = try(r.condition.location, null)
+ title = try(r.condition.title, null)
+ }
+ }
+ ]
}
}
- dynamic "restore_policy" {
- for_each = each.value == null ? [""] : []
- content {
- default = true
- }
+ _org_policies = merge(local._factory_data, var.org_policies)
+
+ org_policies = {
+ for k, v in local._org_policies :
+ k => merge(v, {
+ name = "${local.folder.name}/policies/${k}"
+ parent = local.folder.name
+
+ is_boolean_policy = v.allow == null && v.deny == null
+ has_values = (
+ length(coalesce(try(v.allow.values, []), [])) > 0 ||
+ length(coalesce(try(v.deny.values, []), [])) > 0
+ )
+ rules = [
+ for r in v.rules :
+ merge(r, {
+ has_values = (
+ length(coalesce(try(r.allow.values, []), [])) > 0 ||
+ length(coalesce(try(r.deny.values, []), [])) > 0
+ )
+ })
+ ]
+ })
}
}
-resource "google_folder_organization_policy" "list" {
- for_each = var.policy_list
- folder = local.folder.name
- constraint = each.key
+resource "google_org_policy_policy" "default" {
+ for_each = local.org_policies
+ name = each.value.name
+ parent = each.value.parent
- dynamic "list_policy" {
- for_each = each.value.status == null ? [] : [each.value]
- iterator = policy
- content {
- inherit_from_parent = policy.value.inherit_from_parent
- suggested_value = policy.value.suggested_value
- dynamic "allow" {
- for_each = policy.value.status ? [""] : []
- content {
- values = (
- try(length(policy.value.values) > 0, false)
- ? policy.value.values
- : null
- )
- all = (
- try(length(policy.value.values) > 0, false)
- ? null
- : true
- )
+ spec {
+ inherit_from_parent = each.value.inherit_from_parent
+ reset = each.value.reset
+
+ dynamic "rules" {
+ for_each = each.value.rules
+ iterator = rule
+ content {
+ allow_all = try(rule.value.allow.all, false) == true ? "TRUE" : null
+ deny_all = try(rule.value.deny.all, false) == true ? "TRUE" : null
+ enforce = (
+ each.value.is_boolean_policy && rule.value.enforce != null
+ ? upper(tostring(rule.value.enforce))
+ : null
+ )
+ condition {
+ description = rule.value.condition.description
+ expression = rule.value.condition.expression
+ location = rule.value.condition.location
+ title = rule.value.condition.title
}
- }
- dynamic "deny" {
- for_each = policy.value.status ? [] : [""]
- content {
- values = (
- try(length(policy.value.values) > 0, false)
- ? policy.value.values
- : null
- )
- all = (
- try(length(policy.value.values) > 0, false)
- ? null
- : true
- )
+ dynamic "values" {
+ for_each = rule.value.has_values ? [1] : []
+ content {
+ allowed_values = try(rule.value.allow.values, null)
+ denied_values = try(rule.value.deny.values, null)
+ }
}
}
}
- }
- dynamic "restore_policy" {
- for_each = each.value.status == null ? [true] : []
- content {
- default = true
+ 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/outputs.tf b/modules/folder/outputs.tf
index 37babc6f6c..8073951bfa 100644
--- a/modules/folder/outputs.tf
+++ b/modules/folder/outputs.tf
@@ -33,8 +33,7 @@ output "id" {
value = local.folder.name
depends_on = [
google_folder_iam_binding.authoritative,
- google_folder_organization_policy.boolean,
- google_folder_organization_policy.list
+ google_org_policy_policy.default,
]
}
diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf
index 19ed18f3de..a93ea1aae9 100644
--- a/modules/folder/variables.tf
+++ b/modules/folder/variables.tf
@@ -103,24 +103,33 @@ variable "logging_exclusions" {
}
variable "logging_sinks" {
- description = "Logging sinks to create for this folder."
+ description = "Logging sinks to create for the organization."
type = map(object({
- destination = string
- type = string
- filter = string
- include_children = bool
- # TODO exclusions also support description and disabled
- exclusions = map(string)
+ bq_partitioned_table = optional(bool)
+ description = optional(string)
+ destination = string
+ disabled = optional(bool, false)
+ exclusions = optional(map(string), {})
+ filter = string
+ include_children = optional(bool, true)
+ type = string
}))
+ default = {}
+ nullable = false
validation {
condition = alltrue([
- for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) :
+ for k, v in var.logging_sinks :
contains(["bigquery", "logging", "pubsub", "storage"], v.type)
])
error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'."
}
- default = {}
- nullable = false
+ validation {
+ condition = alltrue([
+ for k, v in var.logging_sinks :
+ v.bq_partitioned_table != true || v.type == "bigquery"
+ ])
+ error_message = "Can only set bq_partitioned_table when type is `bigquery`."
+ }
}
variable "name" {
@@ -129,6 +138,52 @@ variable "name" {
default = null
}
+variable "org_policies" {
+ description = "Organization policies applied to this folder keyed by policy name."
+ type = map(object({
+ inherit_from_parent = optional(bool) # for list policies only.
+ reset = optional(bool)
+
+ # default (unconditional) values
+ allow = optional(object({
+ all = optional(bool)
+ values = optional(list(string))
+ }))
+ deny = optional(object({
+ all = optional(bool)
+ values = optional(list(string))
+ }))
+ enforce = optional(bool, true) # for boolean policies only.
+
+ # conditional values
+ rules = optional(list(object({
+ allow = optional(object({
+ all = optional(bool)
+ values = optional(list(string))
+ }))
+ deny = optional(object({
+ all = optional(bool)
+ values = optional(list(string))
+ }))
+ enforce = optional(bool, true) # for boolean policies only.
+ condition = object({
+ description = optional(string)
+ expression = optional(string)
+ location = optional(string)
+ title = optional(string)
+ })
+ })), [])
+ }))
+ default = {}
+ nullable = false
+}
+
+variable "org_policies_data_path" {
+ description = "Path containing org policies in YAML format."
+ type = string
+ default = null
+}
+
variable "parent" {
description = "Parent in folders/folder_id or organizations/org_id format."
type = string
@@ -139,25 +194,6 @@ variable "parent" {
}
}
-variable "policy_boolean" {
- description = "Map of boolean org policies and enforcement value, set value to null for policy restore."
- type = map(bool)
- default = {}
- nullable = false
-}
-
-variable "policy_list" {
- description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny."
- type = map(object({
- inherit_from_parent = bool
- suggested_value = string
- status = bool
- values = list(string)
- }))
- default = {}
- nullable = false
-}
-
variable "tag_bindings" {
description = "Tag bindings for this folder, in key => tag value id format."
type = map(string)
diff --git a/modules/folder/versions.tf b/modules/folder/versions.tf
index e72a78007a..90b632f6d4 100644
--- a/modules/folder/versions.tf
+++ b/modules/folder/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.1.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/folders-unit/README.md b/modules/folders-unit/README.md
deleted file mode 100644
index 4515853362..0000000000
--- a/modules/folders-unit/README.md
+++ /dev/null
@@ -1,57 +0,0 @@
-# Google Cloud Unit Folders Module
-
-This module allows creation and management of an organizational hierarchy "unit" composed of a parent folder (usually mapped to a business unit or team), and a set of child folders (usually mapped to environments) each with a corresponding set of service accounts, IAM bindings and GCS buckets.
-
-## Example
-
-```hcl
-module "folders-unit" {
- source = "./modules/folders-unit"
- name = "Business Intelligence"
- short_name = "bi"
- automation_project_id = "automation-project-394yr923811"
- billing_account_id = "015617-16GHBC-AF02D9"
- organization_id = "506128240800"
- root_node = "folders/93469270123701"
- prefix = "unique-prefix"
- environments = {
- dev = "Development",
- test = "Testing",
- prod = "Production"
- }
- service_account_keys = true
-}
-# tftest modules=1 resources=37
-```
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [automation_project_id](variables.tf#L17) | Project id used for automation service accounts. | string
| ✓ | |
-| [billing_account_id](variables.tf#L22) | Country billing account account. | string
| ✓ | |
-| [name](variables.tf#L86) | Top folder name. | string
| ✓ | |
-| [organization_id](variables.tf#L91) | Organization id in organizations/nnnnnn format. | string
| ✓ | |
-| [root_node](variables.tf#L102) | Root node in folders/folder_id or organizations/org_id format. | string
| ✓ | |
-| [short_name](variables.tf#L113) | Short name used as GCS bucket and service account prefixes, do not use capital letters or spaces. | string
| ✓ | |
-| [environments](variables.tf#L27) | Unit environments short names. | map(string)
| | {…}
|
-| [gcs_defaults](variables.tf#L36) | Defaults use for the state GCS buckets. | map(string)
| | {…}
|
-| [iam](variables.tf#L45) | IAM bindings for the top-level folder in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
-| [iam_billing_config](variables.tf#L51) | Grant billing user role to service accounts, defaults to granting on the billing account. | object({…})
| | {…}
|
-| [iam_enviroment_roles](variables.tf#L63) | IAM roles granted to the environment service account on the environment sub-folder. | list(string)
| | […]
|
-| [iam_xpn_config](variables.tf#L74) | Grant Shared VPC creation roles to service accounts, defaults to granting at folder level. | object({…})
| | {…}
|
-| [prefix](variables.tf#L96) | Optional prefix used for GCS bucket names to ensure uniqueness. | string
| | null
|
-| [service_account_keys](variables.tf#L107) | Generate and store service account keys in the state file. | bool
| | false
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [env_folders](outputs.tf#L17) | Unit environments folders. | |
-| [env_gcs_buckets](outputs.tf#L28) | Unit environments tfstate gcs buckets. | |
-| [env_sa_keys](outputs.tf#L36) | Unit environments service account keys. | ✓ |
-| [env_service_accounts](outputs.tf#L45) | Unit environments service accounts. | |
-| [unit_folder](outputs.tf#L53) | Unit top level folder. | |
-
-
diff --git a/modules/folders-unit/locals.tf b/modules/folders-unit/locals.tf
deleted file mode 100644
index bd5135092b..0000000000
--- a/modules/folders-unit/locals.tf
+++ /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.
- */
-
-locals {
- folder_roles = concat(var.iam_enviroment_roles, local.sa_xpn_folder_roles)
- iam = var.iam == null ? {} : var.iam
- folder_iam_service_account_bindings = {
- for pair in setproduct(keys(var.environments), local.folder_roles) :
- "${pair.0}-${pair.1}" => { environment = pair.0, role = pair.1 }
- }
- org_iam_service_account_bindings = {
- for pair in setproduct(keys(var.environments), concat(
- local.sa_xpn_org_roles,
- local.sa_billing_org_roles,
- local.sa_billing_org_roles)) :
- "${pair.0}-${pair.1}" => { environment = pair.0, role = pair.1 }
- }
- billing_iam_service_account_bindings = {
- for pair in setproduct(keys(var.environments), local.sa_billing_account_roles) :
- "${pair.0}-${pair.1}" => { environment = pair.0, role = pair.1 }
- }
- service_accounts = {
- for key, sa in google_service_account.environment :
- key => "serviceAccount:${sa.email}"
- }
- sa_billing_account_roles = (
- var.iam_billing_config.target_org ? [] : ["roles/billing.user"]
- )
- sa_billing_org_roles = (
- !var.iam_billing_config.target_org ? [] : ["roles/billing.user"]
- )
- sa_xpn_folder_roles = (
- local.sa_xpn_target_org ? [] : ["roles/compute.xpnAdmin"]
- )
- sa_xpn_org_roles = (
- local.sa_xpn_target_org
- ? ["roles/compute.xpnAdmin", "roles/resourcemanager.organizationViewer"]
- : ["roles/resourcemanager.organizationViewer"]
- )
- sa_xpn_target_org = (
- var.iam_xpn_config.target_org
- ||
- substr(var.root_node, 0, 13) == "organizations"
- )
-}
diff --git a/modules/folders-unit/main.tf b/modules/folders-unit/main.tf
deleted file mode 100644
index f609fffc1d..0000000000
--- a/modules/folders-unit/main.tf
+++ /dev/null
@@ -1,109 +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 {
- organization_id = element(split("/", var.organization_id), 1)
-}
-
-###############################################################################
-# Folders and folder IAM #
-###############################################################################
-
-resource "google_folder" "unit" {
- display_name = var.name
- parent = var.root_node
-}
-
-resource "google_folder" "environment" {
- for_each = var.environments
- display_name = each.value
- parent = google_folder.unit.name
-}
-
-resource "google_folder_iam_binding" "unit" {
- for_each = var.iam
- folder = google_folder.unit.name
- role = each.key
- members = each.value
-}
-
-resource "google_folder_iam_binding" "environment" {
- for_each = local.folder_iam_service_account_bindings
- folder = google_folder.environment[each.value.environment].name
- role = each.value.role
- members = [local.service_accounts[each.value.environment]]
-}
-
-###############################################################################
-# Billing account and org IAM #
-###############################################################################
-
-resource "google_organization_iam_member" "org_iam_member" {
- for_each = local.org_iam_service_account_bindings
- org_id = local.organization_id
- role = each.value.role
- member = local.service_accounts[each.value.environment]
-}
-
-resource "google_billing_account_iam_member" "billing_iam_member" {
- for_each = var.iam_billing_config.grant ? local.billing_iam_service_account_bindings : {}
- billing_account_id = var.billing_account_id
- role = each.value.role
- member = local.service_accounts[each.value.environment]
-}
-
-################################################################################
-# Service Accounts #
-################################################################################
-
-resource "google_service_account" "environment" {
- for_each = var.environments
- project = var.automation_project_id
- account_id = "${var.short_name}-${each.key}"
- display_name = "${var.short_name} ${each.key} (Terraform managed)."
-}
-
-resource "google_service_account_key" "keys" {
- for_each = var.service_account_keys ? var.environments : {}
- service_account_id = google_service_account.environment[each.key].email
-}
-
-################################################################################
-# GCS and GCS IAM #
-################################################################################
-
-resource "google_storage_bucket" "tfstate" {
- for_each = var.environments
- project = var.automation_project_id
- name = join("", [
- var.prefix == null ? "" : "${var.prefix}-",
- "${var.short_name}-${each.key}-tf"
- ])
- location = var.gcs_defaults.location
- storage_class = var.gcs_defaults.storage_class
- force_destroy = false
- uniform_bucket_level_access = true
- versioning {
- enabled = true
- }
-}
-
-resource "google_storage_bucket_iam_binding" "bindings" {
- for_each = var.environments
- bucket = google_storage_bucket.tfstate[each.key].name
- role = "roles/storage.objectAdmin"
- members = [local.service_accounts[each.key]]
-}
diff --git a/modules/folders-unit/outputs.tf b/modules/folders-unit/outputs.tf
deleted file mode 100644
index 8e4d1066fc..0000000000
--- a/modules/folders-unit/outputs.tf
+++ /dev/null
@@ -1,59 +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 "env_folders" {
- description = "Unit environments folders."
- value = {
- for key, folder in google_folder.environment
- : key => {
- id = folder.name,
- name = folder.display_name
- }
- }
-}
-
-output "env_gcs_buckets" {
- description = "Unit environments tfstate gcs buckets."
- value = {
- for key, bucket in google_storage_bucket.tfstate
- : key => bucket.name
- }
-}
-
-output "env_sa_keys" {
- description = "Unit environments service account keys."
- sensitive = true
- value = {
- for key, sa_key in google_service_account_key.keys :
- key => sa_key.private_key
- }
-}
-
-output "env_service_accounts" {
- description = "Unit environments service accounts."
- value = {
- for key, sa in google_service_account.environment
- : key => sa.email
- }
-}
-
-output "unit_folder" {
- description = "Unit top level folder."
- value = {
- id = google_folder.unit.name,
- name = google_folder.unit.display_name
- }
-}
diff --git a/modules/folders-unit/variables.tf b/modules/folders-unit/variables.tf
deleted file mode 100644
index 10a167d470..0000000000
--- a/modules/folders-unit/variables.tf
+++ /dev/null
@@ -1,116 +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 "automation_project_id" {
- description = "Project id used for automation service accounts."
- type = string
-}
-
-variable "billing_account_id" {
- description = "Country billing account account."
- type = string
-}
-
-variable "environments" {
- description = "Unit environments short names."
- type = map(string)
- default = {
- non-prod = "Non production"
- prod = "Production"
- }
-}
-
-variable "gcs_defaults" {
- description = "Defaults use for the state GCS buckets."
- type = map(string)
- default = {
- location = "EU"
- storage_class = "MULTI_REGIONAL"
- }
-}
-
-variable "iam" {
- description = "IAM bindings for the top-level folder in {ROLE => [MEMBERS]} format."
- type = map(list(string))
- default = {}
-}
-
-variable "iam_billing_config" {
- description = "Grant billing user role to service accounts, defaults to granting on the billing account."
- type = object({
- grant = bool
- target_org = bool
- })
- default = {
- grant = true
- target_org = false
- }
-}
-
-variable "iam_enviroment_roles" {
- description = "IAM roles granted to the environment service account on the environment sub-folder."
- type = list(string)
- default = [
- "roles/compute.networkAdmin",
- "roles/owner",
- "roles/resourcemanager.folderAdmin",
- "roles/resourcemanager.projectCreator",
- ]
-}
-
-variable "iam_xpn_config" {
- description = "Grant Shared VPC creation roles to service accounts, defaults to granting at folder level."
- type = object({
- grant = bool
- target_org = bool
- })
- default = {
- grant = true
- target_org = false
- }
-}
-
-variable "name" {
- description = "Top folder name."
- type = string
-}
-
-variable "organization_id" {
- description = "Organization id in organizations/nnnnnn format."
- type = string
-}
-
-variable "prefix" {
- description = "Optional prefix used for GCS bucket names to ensure uniqueness."
- type = string
- default = null
-}
-
-variable "root_node" {
- description = "Root node in folders/folder_id or organizations/org_id format."
- type = string
-}
-
-variable "service_account_keys" {
- description = "Generate and store service account keys in the state file."
- type = bool
- default = false
-}
-
-variable "short_name" {
- description = "Short name used as GCS bucket and service account prefixes, do not use capital letters or spaces."
- type = string
-}
diff --git a/modules/folders-unit/versions.tf b/modules/folders-unit/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/modules/folders-unit/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/modules/gcs/README.md b/modules/gcs/README.md
index 322a4c2779..439b4522d9 100644
--- a/modules/gcs/README.md
+++ b/modules/gcs/README.md
@@ -1,9 +1,10 @@
# Google Cloud Storage Module
+
## Example
```hcl
module "bucket" {
- source = "./modules/gcs"
+ source = "./fabric/modules/gcs"
project_id = "myproject"
prefix = "test"
name = "my-bucket"
@@ -18,7 +19,7 @@ module "bucket" {
```hcl
module "bucket" {
- source = "./modules/gcs"
+ source = "./fabric/modules/gcs"
project_id = "myproject"
prefix = "test"
name = "my-bucket"
@@ -34,19 +35,17 @@ module "bucket" {
```hcl
module "bucket" {
- source = "./modules/gcs"
+ 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_object_prefix = null
@@ -59,39 +58,33 @@ module "bucket" {
```hcl
module "bucket" {
- source = "./modules/gcs"
+ source = "./fabric/modules/gcs"
project_id = "myproject"
prefix = "test"
- name = "my-bucket"
-
+ 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
+ lifecycle_rules = {
+ lr-0 = {
+ action = {
+ type = "SetStorageClass"
+ storage_class = "STANDARD"
+ }
+ condition = {
+ age = 30
+ }
}
}
}
# tftest modules=1 resources=2
```
+
### Minimal example with GCS notifications
+
```hcl
module "bucket-gcs-notification" {
- source = "./modules/gcs"
+ source = "./fabric/modules/gcs"
project_id = "myproject"
prefix = "test"
name = "my-bucket"
@@ -112,32 +105,33 @@ module "bucket-gcs-notification" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [name](variables.tf#L89) | Bucket name suffix. | string
| ✓ | |
-| [project_id](variables.tf#L112) | 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#L106) | Prefix used to generate the bucket name. | string
| | null
|
-| [retention_policy](variables.tf#L117) | Bucket retention policy. | object({…})
| | null
|
-| [storage_class](variables.tf#L126) | Bucket storage class. | string
| | "MULTI_REGIONAL"
|
-| [uniform_bucket_level_access](variables.tf#L136) | 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#L142) | Enable versioning, defaults to false. | bool
| | false
|
-| [website](variables.tf#L148) | 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
| name | description | sensitive |
|---|---|:---:|
| [bucket](outputs.tf#L17) | Bucket resource. | |
-| [name](outputs.tf#L22) | Bucket name. | |
-| [notification](outputs.tf#L26) | GCS Notification self link. | |
-| [topic](outputs.tf#L30) | Topic ID used by GCS. | |
-| [url](outputs.tf#L34) | Bucket URL. | |
+| [id](outputs.tf#L28) | Bucket ID (same as name). | |
+| [name](outputs.tf#L37) | Bucket name. | |
+| [notification](outputs.tf#L46) | GCS Notification self link. | |
+| [topic](outputs.tf#L51) | Topic ID used by GCS. | |
+| [url](outputs.tf#L56) | Bucket URL. | |
diff --git a/modules/gcs/main.tf b/modules/gcs/main.tf
index 020d235954..68b77077d1 100644
--- a/modules/gcs/main.tf
+++ b/modules/gcs/main.tf
@@ -15,11 +15,7 @@
*/
locals {
- prefix = (
- var.prefix == null || var.prefix == "" # keep "" for backward compatibility
- ? ""
- : "${var.prefix}-"
- )
+ prefix = var.prefix == null ? "" : "${var.prefix}-"
notification = try(var.notification_config.enabled, false)
}
@@ -79,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
}
}
}
@@ -108,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/outputs.tf b/modules/gcs/outputs.tf
index 415b94639d..a00c04cf7a 100644
--- a/modules/gcs/outputs.tf
+++ b/modules/gcs/outputs.tf
@@ -19,18 +19,40 @@ output "bucket" {
value = google_storage_bucket.bucket
}
+# We add `id` as an alias to `name` to simplify log sink handling.
+# Since all other log destinations (pubsub, logging-bucket, bigquery)
+# have an id output, it is convenient to have in this module too to
+# handle all log destination as homogeneous objects (i.e. you can
+# assume any valid log destination has an `id` output).
+
+output "id" {
+ description = "Bucket ID (same as name)."
+ value = "${local.prefix}${lower(var.name)}"
+ depends_on = [
+ google_storage_bucket.bucket,
+ google_storage_bucket_iam_binding.bindings
+ ]
+}
+
output "name" {
description = "Bucket name."
- value = google_storage_bucket.bucket.name
+ value = "${local.prefix}${lower(var.name)}"
+ depends_on = [
+ google_storage_bucket.bucket,
+ google_storage_bucket_iam_binding.bindings
+ ]
}
+
output "notification" {
description = "GCS Notification self link."
value = local.notification ? google_storage_notification.notification[0].self_link : null
}
+
output "topic" {
description = "Topic ID used by GCS."
value = local.notification ? google_pubsub_topic.topic[0].id : null
}
+
output "url" {
description = "Bucket URL."
value = google_storage_bucket.bucket.url
diff --git a/modules/gcs/variables.tf b/modules/gcs/variables.tf
index d8885bbb81..58aec4dfb8 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,19 +121,25 @@ 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
}
+
variable "prefix" {
- description = "Prefix used to generate the bucket name."
+ description = "Optional prefix used to generate the bucket name."
type = string
default = null
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty, please use null instead."
+ }
}
variable "project_id" {
@@ -118,7 +151,7 @@ variable "retention_policy" {
description = "Bucket retention policy."
type = object({
retention_period = number
- is_locked = bool
+ is_locked = optional(bool)
})
default = null
}
@@ -148,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 290412687a..90b632f6d4 100644
--- a/modules/gcs/versions.tf
+++ b/modules/gcs/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/gke-cluster/README.md b/modules/gke-cluster/README.md
index 19a15a54a6..fc2e3b3745 100644
--- a/modules/gke-cluster/README.md
+++ b/modules/gke-cluster/README.md
@@ -8,22 +8,25 @@ This module allows simplified creation and management of GKE clusters and should
```hcl
module "cluster-1" {
- source = "./modules/gke-cluster"
- project_id = "myproject"
- name = "cluster-1"
- location = "europe-west1-b"
- network = var.vpc.self_link
- subnetwork = var.subnet.self_link
- secondary_range_pods = "pods"
- secondary_range_services = "services"
- default_max_pods_per_node = 32
- master_authorized_ranges = {
- internal-vms = "10.0.0.0/8"
+ source = "./fabric/modules/gke-cluster"
+ project_id = "myproject"
+ name = "cluster-1"
+ location = "europe-west1-b"
+ vpc_config = {
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ secondary_range_names = {
+ pods = "pods"
+ services = "services"
+ }
+ master_authorized_ranges = {
+ internal-vms = "10.0.0.0/8"
+ }
+ master_ipv4_cidr_block = "192.168.0.0/28"
}
+ max_pods_per_node = 32
private_cluster_config = {
- enable_private_nodes = true
enable_private_endpoint = true
- master_ipv4_cidr_block = "192.168.0.0/28"
master_global_access = false
}
labels = {
@@ -37,25 +40,30 @@ module "cluster-1" {
```hcl
module "cluster-1" {
- source = "./modules/gke-cluster"
- project_id = "myproject"
- name = "cluster-1"
- location = "europe-west1-b"
- network = var.vpc.self_link
- subnetwork = var.subnet.self_link
- secondary_range_pods = "pods"
- secondary_range_services = "services"
- default_max_pods_per_node = 32
- enable_dataplane_v2 = true
- master_authorized_ranges = {
- internal-vms = "10.0.0.0/8"
+ source = "./fabric/modules/gke-cluster"
+ project_id = "myproject"
+ name = "cluster-1"
+ location = "europe-west1-b"
+ vpc_config = {
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ secondary_range_names = {
+ pods = "pods"
+ services = "services"
+ }
+ master_authorized_ranges = {
+ internal-vms = "10.0.0.0/8"
+ }
+ master_ipv4_cidr_block = "192.168.0.0/28"
}
private_cluster_config = {
- enable_private_nodes = true
enable_private_endpoint = true
- master_ipv4_cidr_block = "192.168.0.0/28"
master_global_access = false
}
+ enable_features = {
+ dataplane_v2 = true
+ workload_identity = true
+ }
labels = {
environment = "dev"
}
@@ -68,44 +76,24 @@ module "cluster-1" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [location](variables.tf#L163) | Cluster zone or region. | string
| ✓ | |
-| [name](variables.tf#L230) | Cluster name. | string
| ✓ | |
-| [network](variables.tf#L235) | Name or self link of the VPC used for the cluster. Use the self link for Shared VPC. | string
| ✓ | |
-| [project_id](variables.tf#L279) | Cluster project id. | string
| ✓ | |
-| [secondary_range_pods](variables.tf#L302) | Subnet secondary range name used for pods. | string
| ✓ | |
-| [secondary_range_services](variables.tf#L307) | Subnet secondary range name used for services. | string
| ✓ | |
-| [subnetwork](variables.tf#L312) | VPC subnetwork name or self link. | string
| ✓ | |
-| [addons](variables.tf#L17) | Addons enabled in the cluster (true means enabled). | object({…})
| | {…}
|
-| [authenticator_security_group](variables.tf#L51) | RBAC security group for Google Groups for GKE, format is gke-security-groups@yourdomain.com. | string
| | null
|
-| [cluster_autoscaling](variables.tf#L57) | Enable and configure limits for Node Auto-Provisioning with Cluster Autoscaler. | object({…})
| | {…}
|
-| [database_encryption](variables.tf#L75) | Enable and configure GKE application-layer secrets encryption. | object({…})
| | {…}
|
-| [default_max_pods_per_node](variables.tf#L89) | Maximum number of pods per node in this cluster. | number
| | 110
|
-| [description](variables.tf#L95) | Cluster description. | string
| | null
|
-| [dns_config](variables.tf#L101) | Configuration for Using Cloud DNS for GKE. | object({…})
| | {…}
|
-| [enable_autopilot](variables.tf#L115) | Create cluster in autopilot mode. With autopilot there's no need to create node-pools and some features are not supported (e.g. setting default_max_pods_per_node). | bool
| | false
|
-| [enable_binary_authorization](variables.tf#L121) | Enable Google Binary Authorization. | bool
| | null
|
-| [enable_dataplane_v2](variables.tf#L127) | Enable Dataplane V2 on the cluster, will disable network_policy addons config. | bool
| | false
|
-| [enable_intranode_visibility](variables.tf#L133) | Enable intra-node visibility to make same node pod to pod traffic visible. | bool
| | null
|
-| [enable_l4_ilb_subsetting](variables.tf#L139) | Enable L4ILB Subsetting. | bool
| | null
|
-| [enable_shielded_nodes](variables.tf#L145) | Enable Shielded Nodes features on all nodes in this cluster. | bool
| | null
|
-| [enable_tpu](variables.tf#L151) | Enable Cloud TPU resources in this cluster. | bool
| | null
|
-| [labels](variables.tf#L157) | Cluster resource labels. | map(string)
| | null
|
-| [logging_config](variables.tf#L168) | Logging configuration (enabled components). | list(string)
| | null
|
-| [logging_service](variables.tf#L174) | Logging service (disable with an empty string). | string
| | "logging.googleapis.com/kubernetes"
|
-| [maintenance_config](variables.tf#L180) | Maintenance window configuration. | object({…})
| | {…}
|
-| [master_authorized_ranges](variables.tf#L206) | External Ip address ranges that can access the Kubernetes cluster master through HTTPS. | map(string)
| | {}
|
-| [min_master_version](variables.tf#L212) | Minimum version of the master, defaults to the version of the most recent official release. | string
| | null
|
-| [monitoring_config](variables.tf#L218) | Monitoring configuration (enabled components). | list(string)
| | null
|
-| [monitoring_service](variables.tf#L224) | Monitoring service (disable with an empty string). | string
| | "monitoring.googleapis.com/kubernetes"
|
-| [node_locations](variables.tf#L240) | Zones in which the cluster's nodes are located. | list(string)
| | []
|
-| [notification_config](variables.tf#L246) | GKE Cluster upgrade notifications via PubSub. | bool
| | false
|
-| [peering_config](variables.tf#L252) | Configure peering with the master VPC for private clusters. | object({…})
| | null
|
-| [pod_security_policy](variables.tf#L262) | Enable the PodSecurityPolicy feature. | bool
| | null
|
-| [private_cluster_config](variables.tf#L268) | Enable and configure private cluster, private nodes must be true if used. | object({…})
| | null
|
-| [release_channel](variables.tf#L284) | Release channel for GKE upgrades. | string
| | null
|
-| [resource_usage_export_config](variables.tf#L290) | Configure the ResourceUsageExportConfig feature. | object({…})
| | {…}
|
-| [vertical_pod_autoscaling](variables.tf#L317) | Enable the Vertical Pod Autoscaling feature. | bool
| | null
|
-| [workload_identity](variables.tf#L323) | Enable the Workload Identity feature. | bool
| | true
|
+| [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({…})
| ✓ | |
+| [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
|
## Outputs
@@ -114,11 +102,11 @@ module "cluster-1" {
| [ca_certificate](outputs.tf#L17) | Public certificate of the cluster (base64-encoded). | ✓ |
| [cluster](outputs.tf#L23) | Cluster resource. | ✓ |
| [endpoint](outputs.tf#L29) | Cluster endpoint. | |
-| [id](outputs.tf#L34) | Cluster ID. | ✓ |
-| [location](outputs.tf#L40) | Cluster location. | |
-| [master_version](outputs.tf#L45) | Master version. | |
-| [name](outputs.tf#L50) | Cluster name. | |
-| [notifications](outputs.tf#L55) | GKE PubSub notifications topic. | |
-| [self_link](outputs.tf#L60) | Cluster self link. | ✓ |
+| [id](outputs.tf#L34) | Cluster ID. | |
+| [location](outputs.tf#L39) | Cluster location. | |
+| [master_version](outputs.tf#L44) | Master version. | |
+| [name](outputs.tf#L49) | Cluster name. | |
+| [notifications](outputs.tf#L54) | GKE PubSub notifications topic. | |
+| [self_link](outputs.tf#L59) | Cluster self link. | ✓ |
-
\ No newline at end of file
+
diff --git a/modules/gke-cluster/main.tf b/modules/gke-cluster/main.tf
index d8b5aff391..5b5cd95f3a 100644
--- a/modules/gke-cluster/main.tf
+++ b/modules/gke-cluster/main.tf
@@ -14,144 +14,237 @@
* limitations under the License.
*/
-locals {
- # The Google provider is unable to validate certain configurations of
- # private_cluster_config when enable_private_nodes is false (provider docs)
- is_private = try(var.private_cluster_config.enable_private_nodes, false)
- peering = try(
- google_container_cluster.cluster.private_cluster_config.0.peering_name,
- null
+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
+ description = var.description
+ location = var.location
+ node_locations = (
+ length(var.node_locations) == 0 ? null : var.node_locations
)
- peering_project_id = (
- try(var.peering_config.project_id, null) == null
- ? var.project_id
- : var.peering_config.project_id
+ min_master_version = var.min_master_version
+ network = var.vpc_config.network
+ subnetwork = var.vpc_config.subnetwork
+ resource_labels = var.labels
+ default_max_pods_per_node = (
+ var.enable_features.autopilot ? null : var.max_pods_per_node
)
-}
+ enable_intranode_visibility = (
+ var.enable_features.autopilot ? null : var.enable_features.intranode_visibility
+ )
+ enable_l4_ilb_subsetting = var.enable_features.l4_ilb_subsetting
+ enable_shielded_nodes = (
+ var.enable_features.autopilot ? null : var.enable_features.shielded_nodes
+ )
+ enable_tpu = var.enable_features.tpu
+ initial_node_count = 1
+ remove_default_node_pool = var.enable_features.autopilot ? null : true
+ datapath_provider = (
+ var.enable_features.dataplane_v2 || var.enable_features.autopilot
+ ? "ADVANCED_DATAPATH"
+ : "DATAPATH_PROVIDER_UNSPECIFIED"
+ )
+ enable_autopilot = var.enable_features.autopilot ? true : null
+
+ # 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
+ }
+ }
+ }
-resource "google_container_cluster" "cluster" {
- provider = google-beta
- project = var.project_id
- name = var.name
- description = var.description
- location = var.location
- node_locations = length(var.node_locations) == 0 ? null : var.node_locations
- min_master_version = var.min_master_version
- network = var.network
- subnetwork = var.subnetwork
- logging_service = var.logging_config == null ? var.logging_service : null
- monitoring_service = var.monitoring_config == null ? var.monitoring_service : null
- resource_labels = var.labels
- default_max_pods_per_node = var.enable_autopilot ? null : var.default_max_pods_per_node
- enable_binary_authorization = var.enable_binary_authorization
- enable_intranode_visibility = var.enable_intranode_visibility
- enable_l4_ilb_subsetting = var.enable_l4_ilb_subsetting
- enable_shielded_nodes = var.enable_shielded_nodes
- enable_tpu = var.enable_tpu
- initial_node_count = 1
- remove_default_node_pool = var.enable_autopilot ? null : true
- datapath_provider = var.enable_dataplane_v2 ? "ADVANCED_DATAPATH" : "DATAPATH_PROVIDER_UNSPECIFIED"
- enable_autopilot = var.enable_autopilot == true ? true : null
- # node_config {}
- # NOTE: Default node_pool is deleted, so node_config (here) is extranneous.
- # Specify that node_config as an parameter to gke-nodepool module instead.
- # TODO(ludomagno): compute addons map in locals and use a single dynamic block
addons_config {
dynamic "dns_cache_config" {
- for_each = var.enable_autopilot ? [] : [""]
+ for_each = !var.enable_features.autopilot ? [""] : []
content {
- enabled = var.addons.dns_cache_config
+ enabled = var.enable_addons.dns_cache
}
}
http_load_balancing {
- disabled = !var.addons.http_load_balancing
+ disabled = !var.enable_addons.http_load_balancing
}
horizontal_pod_autoscaling {
- disabled = !var.addons.horizontal_pod_autoscaling
+ disabled = !var.enable_addons.horizontal_pod_autoscaling
}
dynamic "network_policy_config" {
- for_each = !var.enable_autopilot ? [""] : []
+ for_each = !var.enable_features.autopilot ? [""] : []
content {
- disabled = !var.addons.network_policy_config
+ disabled = !var.enable_addons.network_policy
}
}
cloudrun_config {
- disabled = !var.addons.cloudrun_config
+ disabled = !var.enable_addons.cloudrun
}
istio_config {
- disabled = !var.addons.istio_config.enabled
- auth = var.addons.istio_config.tls ? "AUTH_MUTUAL_TLS" : "AUTH_NONE"
+ disabled = var.enable_addons.istio == null
+ auth = (
+ try(var.enable_addons.istio.enable_tls, false) ? "AUTH_MUTUAL_TLS" : "AUTH_NONE"
+ )
}
gce_persistent_disk_csi_driver_config {
- enabled = var.addons.gce_persistent_disk_csi_driver_config
+ enabled = (
+ var.enable_features.autopilot
+ ? true
+ : var.enable_addons.gce_persistent_disk_csi_driver
+ )
}
- gcp_filestore_csi_driver_config {
- enabled = var.addons.gcp_filestore_csi_driver_config
+ dynamic "gcp_filestore_csi_driver_config" {
+ for_each = !var.enable_features.autopilot ? [""] : []
+ content {
+ enabled = var.enable_addons.gcp_filestore_csi_driver
+ }
}
kalm_config {
- enabled = var.addons.kalm_config
+ enabled = var.enable_addons.kalm
}
config_connector_config {
- enabled = var.addons.config_connector_config
+ enabled = var.enable_addons.config_connector
+ }
+ gke_backup_agent_config {
+ enabled = var.enable_addons.gke_backup_agent
+ }
+ }
+
+ dynamic "authenticator_groups_config" {
+ for_each = var.enable_features.groups_for_rbac != null ? [""] : []
+ content {
+ security_group = var.enable_features.groups_for_rbac
+ }
+ }
+
+ dynamic "binary_authorization" {
+ for_each = var.enable_features.binary_authorization ? [""] : []
+ content {
+ evaluation_mode = "PROJECT_SINGLETON_POLICY_ENFORCE"
+ }
+ }
+
+ dynamic "cluster_autoscaling" {
+ for_each = var.cluster_autoscaling == null ? [] : [""]
+ content {
+ enabled = true
+ dynamic "resource_limits" {
+ for_each = var.cluster_autoscaling.cpu_limits != null ? [""] : []
+ content {
+ resource_type = "cpu"
+ minimum = var.cluster_autoscaling.cpu_limits.min
+ maximum = var.cluster_autoscaling.cpu_limits.max
+ }
+ }
+ dynamic "resource_limits" {
+ for_each = var.cluster_autoscaling.mem_limits != null ? [""] : []
+ content {
+ resource_type = "memory"
+ minimum = var.cluster_autoscaling.mem_limits.min
+ maximum = var.cluster_autoscaling.mem_limits.max
+ }
+ }
+ // TODO: support GPUs too
+ }
+ }
+
+ dynamic "database_encryption" {
+ for_each = var.enable_features.database_encryption != null ? [""] : []
+ content {
+ state = var.enable_features.database_encryption.state
+ key_name = var.enable_features.database_encryption.key_name
}
}
- # TODO(ludomagno): support setting address ranges instead of range names
- # https://www.terraform.io/docs/providers/google/r/container_cluster.html#cluster_ipv4_cidr_block
- ip_allocation_policy {
- cluster_secondary_range_name = var.secondary_range_pods
- services_secondary_range_name = var.secondary_range_services
+ dynamic "dns_config" {
+ for_each = var.enable_features.cloud_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
+ }
+ }
+
+ dynamic "ip_allocation_policy" {
+ for_each = var.vpc_config.secondary_range_blocks != null ? [""] : []
+ content {
+ cluster_ipv4_cidr_block = var.vpc_config.secondary_range_blocks.pods
+ services_ipv4_cidr_block = var.vpc_config.secondary_range_blocks.services
+ }
+ }
+ dynamic "ip_allocation_policy" {
+ for_each = var.vpc_config.secondary_range_names != null ? [""] : []
+ content {
+ cluster_secondary_range_name = var.vpc_config.secondary_range_names.pods
+ services_secondary_range_name = var.vpc_config.secondary_range_names.services
+ }
+ }
+
+ dynamic "logging_config" {
+ for_each = var.logging_config != null && !var.enable_features.autopilot ? [""] : []
+ content {
+ enable_components = var.logging_config
+ }
}
- # https://www.terraform.io/docs/providers/google/r/container_cluster.html#daily_maintenance_window
maintenance_policy {
dynamic "daily_maintenance_window" {
- for_each = var.maintenance_config != null && lookup(var.maintenance_config, "daily_maintenance_window", null) != null ? [var.maintenance_config.daily_maintenance_window] : []
- iterator = config
+ for_each = (
+ try(var.maintenance_config.daily_window_start_time, null) != null
+ ? [""]
+ : []
+ )
content {
- start_time = config.value.start_time
+ start_time = var.maintenance_config.daily_window_start_time
}
}
-
dynamic "recurring_window" {
- for_each = var.maintenance_config != null && lookup(var.maintenance_config, "recurring_window", null) != null ? [var.maintenance_config.recurring_window] : []
- iterator = config
+ for_each = (
+ try(var.maintenance_config.recurring_window, null) != null
+ ? [""]
+ : []
+ )
content {
- start_time = config.value.start_time
- end_time = config.value.end_time
- recurrence = config.value.recurrence
+ start_time = var.maintenance_config.recurring_window.start_time
+ end_time = var.maintenance_config.recurring_window.end_time
+ recurrence = var.maintenance_config.recurring_window.recurrence
}
}
-
dynamic "maintenance_exclusion" {
- for_each = var.maintenance_config != null && lookup(var.maintenance_config, "maintenance_exclusion", null) != null ? var.maintenance_config.maintenance_exclusion : []
- iterator = config
+ for_each = (
+ try(var.maintenance_config.maintenance_exclusions, null) == null
+ ? []
+ : var.maintenance_config.maintenance_exclusions
+ )
+ iterator = exclusion
content {
- exclusion_name = config.value.exclusion_name
- start_time = config.value.start_time
- end_time = config.value.end_time
+ exclusion_name = exclusion.value.name
+ start_time = exclusion.value.start_time
+ end_time = exclusion.value.end_time
}
}
}
master_auth {
client_certificate_config {
- issue_client_certificate = false
+ issue_client_certificate = var.issue_client_certificate
}
}
dynamic "master_authorized_networks_config" {
- for_each = (
- length(var.master_authorized_ranges) == 0
- ? []
- : [var.master_authorized_ranges]
- )
- iterator = ranges
+ for_each = var.vpc_config.master_authorized_ranges != null ? [""] : []
content {
dynamic "cidr_blocks" {
- for_each = ranges.value
+ for_each = var.vpc_config.master_authorized_ranges
iterator = range
content {
cidr_block = range.value
@@ -161,69 +254,66 @@ resource "google_container_cluster" "cluster" {
}
}
- #the network_policy block is enabled if network_policy_config and network_dataplane_v2 is set to false. Dataplane V2 has built-in network policies.
- dynamic "network_policy" {
- for_each = var.addons.network_policy_config ? [""] : []
- content {
- enabled = var.enable_dataplane_v2 ? false : true
- provider = var.enable_dataplane_v2 ? "PROVIDER_UNSPECIFIED" : "CALICO"
- }
- }
-
- dynamic "private_cluster_config" {
- for_each = local.is_private ? [var.private_cluster_config] : []
- iterator = config
+ dynamic "monitoring_config" {
+ for_each = var.monitoring_config != null && !var.enable_features.autopilot ? [""] : []
content {
- enable_private_nodes = config.value.enable_private_nodes
- enable_private_endpoint = config.value.enable_private_endpoint
- master_ipv4_cidr_block = config.value.master_ipv4_cidr_block
- master_global_access_config {
- enabled = config.value.master_global_access
+ enable_components = var.monitoring_config.enable_components
+ dynamic "managed_prometheus" {
+ for_each = (
+ try(var.monitoring_config.managed_prometheus, null) == true ? [""] : []
+ )
+ content {
+ enabled = true
+ }
}
}
}
- # beta features
-
- dynamic "authenticator_groups_config" {
- for_each = var.authenticator_security_group == null ? [] : [""]
+ # dataplane v2 has bult-in network policies
+ dynamic "network_policy" {
+ for_each = (
+ var.enable_addons.network_policy && !var.enable_features.dataplane_v2
+ ? [""]
+ : []
+ )
content {
- security_group = var.authenticator_security_group
+ enabled = true
+ provider = "CALICO"
}
}
- dynamic "cluster_autoscaling" {
- for_each = var.cluster_autoscaling.enabled ? [var.cluster_autoscaling] : []
- iterator = config
+ dynamic "notification_config" {
+ for_each = var.enable_features.upgrade_notifications != null ? [""] : []
content {
- enabled = true
- resource_limits {
- resource_type = "cpu"
- minimum = config.value.cpu_min
- maximum = config.value.cpu_max
- }
- resource_limits {
- resource_type = "memory"
- minimum = config.value.memory_min
- maximum = config.value.memory_max
+ pubsub {
+ enabled = true
+ topic = (
+ try(var.enable_features.upgrade_notifications.topic_id, null) != null
+ ? var.enable_features.upgrade_notifications.topic_id
+ : google_pubsub_topic.notifications[0].id
+ )
}
- // TODO: support GPUs too
}
}
- dynamic "database_encryption" {
- for_each = var.database_encryption.enabled ? [var.database_encryption] : []
- iterator = config
+ dynamic "private_cluster_config" {
+ for_each = (
+ var.private_cluster_config != null ? [""] : []
+ )
content {
- state = config.value.state
- key_name = config.value.key_name
+ enable_private_nodes = true
+ enable_private_endpoint = var.private_cluster_config.enable_private_endpoint
+ master_ipv4_cidr_block = try(var.vpc_config.master_ipv4_cidr_block, null)
+ master_global_access_config {
+ enabled = var.private_cluster_config.master_global_access
+ }
}
}
dynamic "pod_security_policy_config" {
- for_each = var.pod_security_policy != null ? [""] : []
+ for_each = var.enable_features.pod_security_policy ? [""] : []
content {
- enabled = var.pod_security_policy
+ enabled = var.enable_features.pod_security_policy
}
}
@@ -236,80 +326,61 @@ resource "google_container_cluster" "cluster" {
dynamic "resource_usage_export_config" {
for_each = (
- var.resource_usage_export_config.enabled != null
- &&
- var.resource_usage_export_config.dataset != null
- ? [""] : []
+ try(var.enable_features.resource_usage_export.dataset, null) != null
+ ? [""]
+ : []
)
content {
- enable_network_egress_metering = var.resource_usage_export_config.enabled
+ enable_network_egress_metering = (
+ var.enable_features.resource_usage_export.enable_network_egress_metering
+ )
+ enable_resource_consumption_metering = (
+ var.enable_features.resource_usage_export.enable_resource_consumption_metering
+ )
bigquery_destination {
- dataset_id = var.resource_usage_export_config.dataset
+ dataset_id = var.enable_features.resource_usage_export.dataset
}
}
}
dynamic "vertical_pod_autoscaling" {
- for_each = var.vertical_pod_autoscaling == null ? [] : [""]
+ for_each = var.enable_features.vertical_pod_autoscaling ? [""] : []
content {
- enabled = var.vertical_pod_autoscaling
+ enabled = var.enable_features.vertical_pod_autoscaling
}
}
dynamic "workload_identity_config" {
- for_each = var.workload_identity && !var.enable_autopilot ? [""] : []
+ for_each = var.enable_features.workload_identity ? [""] : []
content {
workload_pool = "${var.project_id}.svc.id.goog"
}
}
-
- dynamic "monitoring_config" {
- for_each = var.monitoring_config != null ? [""] : []
- content {
- enable_components = var.monitoring_config
- }
- }
-
- dynamic "logging_config" {
- for_each = var.logging_config != null ? [""] : []
- content {
- enable_components = var.logging_config
- }
- }
-
- dynamic "dns_config" {
- for_each = var.dns_config != null ? [var.dns_config] : []
- iterator = config
- content {
- cluster_dns = config.value.cluster_dns
- cluster_dns_scope = config.value.cluster_dns_scope
- cluster_dns_domain = config.value.cluster_dns_domain
- }
- }
-
- dynamic "notification_config" {
- for_each = var.notification_config ? [""] : []
- content {
- pubsub {
- enabled = var.notification_config
- topic = var.notification_config ? google_pubsub_topic.notifications[0].id : null
- }
- }
- }
}
resource "google_compute_network_peering_routes_config" "gke_master" {
- count = local.is_private && var.peering_config != null ? 1 : 0
- project = local.peering_project_id
- peering = local.peering
- network = element(reverse(split("/", var.network)), 0)
- import_custom_routes = var.peering_config.import_routes
- export_custom_routes = var.peering_config.export_routes
+ count = (
+ try(var.private_cluster_config.peering_config, null) != null ? 1 : 0
+ )
+ project = (
+ try(var.private_cluster_config.peering_config, null) == null
+ ? var.project_id
+ : var.private_cluster_config.peering_config.project_id
+ )
+ peering = try(
+ google_container_cluster.cluster.private_cluster_config.0.peering_name,
+ null
+ )
+ network = element(reverse(split("/", var.vpc_config.network)), 0)
+ import_custom_routes = var.private_cluster_config.peering_config.import_routes
+ export_custom_routes = var.private_cluster_config.peering_config.export_routes
}
resource "google_pubsub_topic" "notifications" {
- count = var.notification_config ? 1 : 0
- name = "gke-pubsub-notifications"
+ count = (
+ try(var.enable_features.upgrade_notifications.topic_id, null) == null ? 0 : 1
+ )
+ name = "gke-pubsub-notifications"
labels = {
content = "gke-notifications"
}
diff --git a/modules/gke-cluster/outputs.tf b/modules/gke-cluster/outputs.tf
index e4abecbd3c..f98f4f54c7 100644
--- a/modules/gke-cluster/outputs.tf
+++ b/modules/gke-cluster/outputs.tf
@@ -33,7 +33,6 @@ output "endpoint" {
output "id" {
description = "Cluster ID."
- sensitive = true
value = google_container_cluster.cluster.id
}
@@ -54,7 +53,7 @@ output "name" {
output "notifications" {
description = "GKE PubSub notifications topic."
- value = var.notification_config ? google_pubsub_topic.notifications[0].id : null
+ value = try(google_pubsub_topic.notifications[0].id, null)
}
output "self_link" {
diff --git a/modules/gke-cluster/variables.tf b/modules/gke-cluster/variables.tf
index d3043bda72..62d871e977 100644
--- a/modules/gke-cluster/variables.tf
+++ b/modules/gke-cluster/variables.tf
@@ -14,82 +14,25 @@
* limitations under the License.
*/
-variable "addons" {
- description = "Addons enabled in the cluster (true means enabled)."
- type = object({
- cloudrun_config = bool
- dns_cache_config = bool
- horizontal_pod_autoscaling = bool
- http_load_balancing = bool
- istio_config = object({
- enabled = bool
- tls = bool
- })
- network_policy_config = bool
- gce_persistent_disk_csi_driver_config = bool
- gcp_filestore_csi_driver_config = bool
- config_connector_config = bool
- kalm_config = bool
- })
- default = {
- cloudrun_config = false
- dns_cache_config = false
- horizontal_pod_autoscaling = true
- http_load_balancing = true
- istio_config = {
- enabled = false
- tls = false
- }
- network_policy_config = false
- gce_persistent_disk_csi_driver_config = false
- gcp_filestore_csi_driver_config = false
- config_connector_config = false
- kalm_config = false
- }
-}
-
-variable "authenticator_security_group" {
- description = "RBAC security group for Google Groups for GKE, format is gke-security-groups@yourdomain.com."
- type = string
- default = null
-}
-
variable "cluster_autoscaling" {
description = "Enable and configure limits for Node Auto-Provisioning with Cluster Autoscaler."
type = object({
- enabled = bool
- cpu_min = number
- cpu_max = number
- memory_min = number
- memory_max = number
- })
- default = {
- enabled = false
- cpu_min = 0
- cpu_max = 0
- memory_min = 0
- memory_max = 0
- }
-}
-
-variable "database_encryption" {
- description = "Enable and configure GKE application-layer secrets encryption."
- type = object({
- enabled = bool
- state = string
- key_name = string
+ auto_provisioning_defaults = optional(object({
+ boot_disk_kms_key = optional(string)
+ image_type = optional(string)
+ oauth_scopes = optional(list(string))
+ service_account = optional(string)
+ }))
+ cpu_limits = optional(object({
+ min = number
+ max = number
+ }))
+ mem_limits = optional(object({
+ min = number
+ max = number
+ }))
})
- default = {
- enabled = false
- state = "DECRYPTED"
- key_name = null
- }
-}
-
-variable "default_max_pods_per_node" {
- description = "Maximum number of pods per node in this cluster."
- type = number
- default = 110
+ default = null
}
variable "description" {
@@ -98,62 +41,73 @@ variable "description" {
default = null
}
-variable "dns_config" {
- description = "Configuration for Using Cloud DNS for GKE."
+variable "enable_addons" {
+ description = "Addons enabled in the cluster (true means enabled)."
type = object({
- cluster_dns = string
- cluster_dns_scope = string
- cluster_dns_domain = string
+ cloudrun = optional(bool, false)
+ config_connector = optional(bool, false)
+ dns_cache = optional(bool, false)
+ gce_persistent_disk_csi_driver = optional(bool, false)
+ gcp_filestore_csi_driver = optional(bool, false)
+ gke_backup_agent = optional(bool, false)
+ horizontal_pod_autoscaling = optional(bool, false)
+ http_load_balancing = optional(bool, false)
+ istio = optional(object({
+ enable_tls = bool
+ }))
+ kalm = optional(bool, false)
+ network_policy = optional(bool, false)
})
default = {
- cluster_dns = "PROVIDER_UNSPECIFIED"
- cluster_dns_scope = "DNS_SCOPE_UNSPECIFIED"
- cluster_dns_domain = ""
+ horizontal_pod_autoscaling = true
+ http_load_balancing = true
}
+ nullable = false
}
-variable "enable_autopilot" {
- description = "Create cluster in autopilot mode. With autopilot there's no need to create node-pools and some features are not supported (e.g. setting default_max_pods_per_node)."
- type = bool
- default = false
-}
-
-variable "enable_binary_authorization" {
- description = "Enable Google Binary Authorization."
- type = bool
- default = null
+variable "enable_features" {
+ description = "Enable cluster-level features. Certain features allow configuration."
+ type = object({
+ autopilot = optional(bool, false)
+ binary_authorization = optional(bool, false)
+ cloud_dns = optional(object({
+ provider = optional(string)
+ scope = optional(string)
+ domain = optional(string)
+ }))
+ database_encryption = optional(object({
+ state = string
+ key_name = string
+ }))
+ dataplane_v2 = optional(bool, false)
+ groups_for_rbac = optional(string)
+ intranode_visibility = optional(bool, false)
+ l4_ilb_subsetting = optional(bool, false)
+ pod_security_policy = optional(bool, false)
+ resource_usage_export = optional(object({
+ dataset = string
+ enable_network_egress_metering = optional(bool)
+ enable_resource_consumption_metering = optional(bool)
+ }))
+ shielded_nodes = optional(bool, false)
+ tpu = optional(bool, false)
+ upgrade_notifications = optional(object({
+ topic_id = optional(string)
+ }))
+ vertical_pod_autoscaling = optional(bool, false)
+ workload_identity = optional(bool, false)
+ })
+ default = {
+ workload_identity = true
+ }
}
-variable "enable_dataplane_v2" {
- description = "Enable Dataplane V2 on the cluster, will disable network_policy addons config."
+variable "issue_client_certificate" {
+ description = "Enable issuing client certificate."
type = bool
default = false
}
-variable "enable_intranode_visibility" {
- description = "Enable intra-node visibility to make same node pod to pod traffic visible."
- type = bool
- default = null
-}
-
-variable "enable_l4_ilb_subsetting" {
- description = "Enable L4ILB Subsetting."
- type = bool
- default = null
-}
-
-variable "enable_shielded_nodes" {
- description = "Enable Shielded Nodes features on all nodes in this cluster."
- type = bool
- default = null
-}
-
-variable "enable_tpu" {
- description = "Enable Cloud TPU resources in this cluster."
- type = bool
- default = null
-}
-
variable "labels" {
description = "Cluster resource labels."
type = map(string)
@@ -166,47 +120,38 @@ variable "location" {
}
variable "logging_config" {
- description = "Logging configuration (enabled components)."
+ description = "Logging configuration."
type = list(string)
- default = null
-}
-
-variable "logging_service" {
- description = "Logging service (disable with an empty string)."
- type = string
- default = "logging.googleapis.com/kubernetes"
+ default = ["SYSTEM_COMPONENTS"]
}
variable "maintenance_config" {
description = "Maintenance window configuration."
type = object({
- daily_maintenance_window = object({
- start_time = string
- })
- recurring_window = object({
+ daily_window_start_time = optional(string)
+ recurring_window = optional(object({
start_time = string
end_time = string
recurrence = string
- })
- maintenance_exclusion = list(object({
- exclusion_name = string
- start_time = string
- end_time = string
}))
+ maintenance_exclusions = optional(list(object({
+ name = string
+ start_time = string
+ end_time = string
+ scope = optional(string)
+ })))
})
default = {
- daily_maintenance_window = {
- start_time = "03:00"
- }
- recurring_window = null
- maintenance_exclusion = []
+ daily_window_start_time = "03:00"
+ recurring_window = null
+ maintenance_exclusion = []
}
}
-variable "master_authorized_ranges" {
- description = "External Ip address ranges that can access the Kubernetes cluster master through HTTPS."
- type = map(string)
- default = {}
+variable "max_pods_per_node" {
+ description = "Maximum number of pods per node in this cluster."
+ type = number
+ default = 110
}
variable "min_master_version" {
@@ -216,15 +161,14 @@ variable "min_master_version" {
}
variable "monitoring_config" {
- description = "Monitoring configuration (enabled components)."
- type = list(string)
- default = null
-}
-
-variable "monitoring_service" {
- description = "Monitoring service (disable with an empty string)."
- type = string
- default = "monitoring.googleapis.com/kubernetes"
+ description = "Monitoring components."
+ type = object({
+ enable_components = optional(list(string))
+ managed_prometheus = optional(bool)
+ })
+ default = {
+ enable_components = ["SYSTEM_COMPONENTS"]
+ }
}
variable "name" {
@@ -232,46 +176,23 @@ variable "name" {
type = string
}
-variable "network" {
- description = "Name or self link of the VPC used for the cluster. Use the self link for Shared VPC."
- type = string
-}
-
variable "node_locations" {
description = "Zones in which the cluster's nodes are located."
type = list(string)
default = []
-}
-
-variable "notification_config" {
- description = "GKE Cluster upgrade notifications via PubSub."
- type = bool
- default = false
-}
-
-variable "peering_config" {
- description = "Configure peering with the master VPC for private clusters."
- type = object({
- export_routes = bool
- import_routes = bool
- project_id = string
- })
- default = null
-}
-
-variable "pod_security_policy" {
- description = "Enable the PodSecurityPolicy feature."
- type = bool
- default = null
+ nullable = false
}
variable "private_cluster_config" {
- description = "Enable and configure private cluster, private nodes must be true if used."
+ description = "Private cluster configuration."
type = object({
- enable_private_nodes = bool
- enable_private_endpoint = bool
- master_ipv4_cidr_block = string
- master_global_access = bool
+ enable_private_endpoint = optional(bool)
+ master_global_access = optional(bool)
+ peering_config = optional(object({
+ export_routes = optional(bool)
+ import_routes = optional(bool)
+ project_id = optional(string)
+ }))
})
default = null
}
@@ -287,41 +208,21 @@ variable "release_channel" {
default = null
}
-variable "resource_usage_export_config" {
- description = "Configure the ResourceUsageExportConfig feature."
+variable "vpc_config" {
+ description = "VPC-level configuration."
type = object({
- enabled = bool
- dataset = string
+ network = string
+ subnetwork = string
+ master_ipv4_cidr_block = optional(string)
+ secondary_range_blocks = optional(object({
+ pods = string
+ services = string
+ }))
+ secondary_range_names = optional(object({
+ pods = string
+ services = string
+ }), { pods = "pods", services = "services" })
+ master_authorized_ranges = optional(map(string))
})
- default = {
- enabled = null
- dataset = null
- }
-}
-
-variable "secondary_range_pods" {
- description = "Subnet secondary range name used for pods."
- type = string
-}
-
-variable "secondary_range_services" {
- description = "Subnet secondary range name used for services."
- type = string
-}
-
-variable "subnetwork" {
- description = "VPC subnetwork name or self link."
- type = string
-}
-
-variable "vertical_pod_autoscaling" {
- description = "Enable the Vertical Pod Autoscaling feature."
- type = bool
- default = null
-}
-
-variable "workload_identity" {
- description = "Enable the Workload Identity feature."
- type = bool
- default = true
-}
+ nullable = false
+}
\ No newline at end of file
diff --git a/modules/gke-cluster/versions.tf b/modules/gke-cluster/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/gke-cluster/versions.tf
+++ b/modules/gke-cluster/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/gke-hub/README.md b/modules/gke-hub/README.md
index 6b2010590a..0f4c5ae8fd 100644
--- a/modules/gke-hub/README.md
+++ b/modules/gke-hub/README.md
@@ -3,34 +3,35 @@
This module allows simplified creation and management of a GKE Hub object and its features for a given set of clusters. The given list of clusters will be registered inside the Hub and all the configured features will be activated.
To use this module you must ensure the following APIs are enabled in the target project:
-```
-"gkehub.googleapis.com"
-"gkeconnect.googleapis.com"
-"anthosconfigmanagement.googleapis.com"
-"multiclusteringress.googleapis.com"
-"multiclusterservicediscovery.googleapis.com"
-```
+
+- `gkehub.googleapis.com`
+- `gkeconnect.googleapis.com`
+- `anthosconfigmanagement.googleapis.com`
+- `multiclusteringress.googleapis.com`
+- `multiclusterservicediscovery.googleapis.com`
+- `mesh.googleapis.com`
## Full GKE Hub example
```hcl
module "project" {
- source = "./modules/project"
+ source = "./fabric/modules/project"
billing_account = var.billing_account_id
name = "gkehub-test"
parent = "folders/12345"
services = [
+ "anthosconfigmanagement.googleapis.com",
"container.googleapis.com",
- "gkehub.googleapis.com",
"gkeconnect.googleapis.com",
- "anthosconfigmanagement.googleapis.com",
+ "gkehub.googleapis.com",
"multiclusteringress.googleapis.com",
"multiclusterservicediscovery.googleapis.com",
+ "mesh.googleapis.com"
]
}
module "vpc" {
- source = "./modules/net-vpc"
+ source = "./fabric/modules/net-vpc"
project_id = module.project.project_id
name = "network"
subnets = [{
@@ -44,52 +45,257 @@ module "vpc" {
}]
}
-module "cluster-1" {
- source = "./modules/gke-cluster"
- project_id = module.project.project_id
- name = "cluster-1"
- location = "europe-west1-b"
- network = module.vpc.self_link
- subnetwork = module.vpc.subnet_self_links["europe-west1/cluster-1"]
- secondary_range_pods = "pods"
- secondary_range_services = "services"
- enable_dataplane_v2 = true
- master_authorized_ranges = { rfc1918_10_8 = "10.0.0.0/8" }
+module "cluster_1" {
+ source = "./fabric/modules/gke-cluster"
+ project_id = module.project.project_id
+ name = "cluster-1"
+ location = "europe-west1"
+ vpc_config = {
+ network = module.vpc.self_link
+ subnetwork = module.vpc.subnet_self_links["europe-west1/cluster-1"]
+ master_authorized_ranges = {
+ fc1918_10_8 = "10.0.0.0/8"
+ }
+ master_ipv4_cidr_block = "192.168.0.0/28"
+ }
+ enable_features = {
+ dataplane_v2 = true
+ workload_identity = true
+ }
private_cluster_config = {
- enable_private_nodes = true
enable_private_endpoint = true
- master_ipv4_cidr_block = "192.168.0.0/28"
master_global_access = false
}
}
module "hub" {
- source = "./modules/gke-hub"
+ source = "./fabric/modules/gke-hub"
project_id = module.project.project_id
- member_clusters = {
- cluster1 = module.cluster-1.id
+ clusters = {
+ cluster-1 = module.cluster_1.id
+ }
+ features = {
+ appdevexperience = false
+ configmanagement = true
+ identityservice = false
+ multiclusteringress = null
+ servicemesh = false
+ multiclusterservicediscovery = false
}
- member_features = {
- configmanagement = {
- binauthz = true
+ configmanagement_templates = {
+ default = {
+ binauthz = false
config_sync = {
- gcp_service_account_email = null
- https_proxy = null
- policy_dir = "configsync"
- secret_type = "none"
- source_format = "hierarchy"
- sync_branch = "main"
- sync_repo = "https://github.com/danielmarzini/configsync-platform-example"
- sync_rev = null
+ git = {
+ gcp_service_account_email = null
+ https_proxy = null
+ policy_dir = "configsync"
+ secret_type = "none"
+ source_format = "hierarchy"
+ sync_branch = "main"
+ sync_repo = "https://github.com/danielmarzini/configsync-platform-example"
+ sync_rev = null
+ sync_wait_secs = null
+ }
+ prevent_drift = false
+ source_format = "hierarchy"
+ }
+ hierarchy_controller = {
+ enable_hierarchical_resource_quota = true
+ enable_pod_tree_labels = true
+ }
+ policy_controller = {
+ audit_interval_seconds = 120
+ exemptable_namespaces = []
+ log_denies_enabled = true
+ referential_rules_enabled = true
+ template_library_installed = true
}
- hierarchy_controller = null
- policy_controller = null
- version = "1.10.2"
+ version = "v1"
}
}
+ configmanagement_clusters = {
+ "default" = ["cluster-1"]
+ }
+}
+
+# tftest modules=4 resources=15
+```
+
+## Multi-cluster mesh on GKE
+
+```hcl
+module "project" {
+ source = "./fabric/modules/project"
+ billing_account = "123-456-789"
+ name = "gkehub-test"
+ parent = "folders/12345"
+ services = [
+ "anthos.googleapis.com",
+ "container.googleapis.com",
+ "gkehub.googleapis.com",
+ "gkeconnect.googleapis.com",
+ "mesh.googleapis.com",
+ "meshconfig.googleapis.com",
+ "meshca.googleapis.com"
+ ]
+}
+
+module "vpc" {
+ source = "./fabric/modules/net-vpc"
+ project_id = module.project.project_id
+ name = "vpc"
+ mtu = 1500
+ subnets = [
+ {
+ ip_cidr_range = "10.0.1.0/24"
+ name = "subnet-cluster-1"
+ region = "europe-west1"
+ secondary_ip_range = {
+ pods = "10.1.0.0/16"
+ services = "10.2.0.0/24"
+ }
+ },
+ {
+ ip_cidr_range = "10.0.2.0/24"
+ name = "subnet-cluster-2"
+ region = "europe-west4"
+ secondary_ip_range = {
+ 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
+ }
+ ]
+}
+
+module "firewall" {
+ source = "./fabric/modules/net-vpc-firewall"
+ project_id = module.project.project_id
+ network = module.vpc.name
+ ingress_rules = {
+ allow-mesh = {
+ description = "Allow mesh"
+ priority = 900
+ source_ranges = ["10.1.0.0/16", "10.3.0.0/16"]
+ targets = ["cluster-1-node", "cluster-2-node"]
+ },
+ "allow-cluster-1-istio" = {
+ description = "Allow istio sidecar injection, istioctl version and istioctl ps"
+ source_ranges = ["192.168.1.0/28"]
+ targets = ["cluster-1-node"]
+ rules = [
+ { protocol = "tcp", ports = [8080, 15014, 15017] }
+ ]
+ },
+ "allow-cluster-2-istio" = {
+ description = "Allow istio sidecar injection, istioctl version and istioctl ps"
+ source_ranges = ["192.168.2.0/28"]
+ targets = ["cluster-2-node"]
+ rules = [
+ { protocol = "tcp", ports = [8080, 15014, 15017] }
+ ]
+ }
+ }
+}
+
+module "cluster_1" {
+ source = "./fabric/modules/gke-cluster"
+ project_id = module.project.project_id
+ name = "cluster-1"
+ location = "europe-west1"
+ vpc_config = {
+ network = module.vpc.self_link
+ subnetwork = module.vpc.subnet_self_links["europe-west1/subnet-cluster-1"]
+ master_authorized_ranges = {
+ mgmt = "10.0.0.0/28"
+ pods-cluster-1 = "10.3.0.0/16"
+ }
+ 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}"
+ }
+}
+
+module "cluster_1_nodepool" {
+ source = "./fabric/modules/gke-nodepool"
+ project_id = module.project.project_id
+ cluster_name = module.cluster_1.name
+ location = "europe-west1"
+ name = "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"
+ vpc_config = {
+ network = module.vpc.self_link
+ subnetwork = module.vpc.subnet_self_links["europe-west4/subnet-cluster-2"]
+ master_authorized_ranges = {
+ mgmt = "10.0.0.0/28"
+ pods-cluster-1 = "10.3.0.0/16"
+ }
+ master_ipv4_cidr_block = "192.168.2.0/28"
+ }
+ private_cluster_config = {
+ enable_private_endpoint = false
+ master_global_access = true
+ }
+ release_channel = "REGULAR"
+ labels = {
+ mesh_id = "proj-${module.project.number}"
+ }
+}
+
+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"
+ node_count = { initial = 1 }
+ service_account = { create = true }
+ tags = ["cluster-2-node"]
+}
+
+module "hub" {
+ source = "./fabric/modules/gke-hub"
+ project_id = module.project.project_id
+ clusters = {
+ cluster-1 = module.cluster_1.id
+ cluster-2 = module.cluster_2.id
+ }
+ features = {
+ appdevexperience = false
+ configmanagement = false
+ identityservice = false
+ multiclusteringress = null
+ servicemesh = true
+ multiclusterservicediscovery = false
+ }
+ workload_identity_clusters = [
+ "cluster-1",
+ "cluster-2"
+ ]
}
-# tftest modules=4 resources=13
+# tftest modules=8 resources=28
```
@@ -97,15 +303,17 @@ module "hub" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [project_id](variables.tf#L75) | GKE hub project ID. | string
| ✓ | |
-| [features](variables.tf#L17) | GKE hub features to enable. | object({…})
| | {…}
|
-| [member_clusters](variables.tf#L32) | List for member cluster self links. | map(string)
| | {}
|
-| [member_features](variables.tf#L39) | Member features for each cluster | object({…})
| | {…}
|
+| [project_id](variables.tf#L87) | GKE hub project ID. | string
| ✓ | |
+| [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({…})
| | {…}
|
+| [workload_identity_clusters](variables.tf#L92) | Clusters that will use Fleet Workload Identity. | list(string)
| | []
|
## Outputs
| name | description | sensitive |
|---|---|:---:|
-| [cluster_ids](outputs.tf#L17) | | |
+| [cluster_ids](outputs.tf#L17) | Ids of all the clusters created. | |
diff --git a/modules/gke-hub/main.tf b/modules/gke-hub/main.tf
index 4da1a0ce6e..f433d32274 100644
--- a/modules/gke-hub/main.tf
+++ b/modules/gke-hub/main.tf
@@ -14,126 +14,137 @@
* limitations under the License.
*/
-resource "google_gke_hub_membership" "membership" {
+locals {
+ _cluster_cm_config = flatten([
+ for template, clusters in var.configmanagement_clusters : [
+ for cluster in clusters : {
+ cluster = cluster
+ template = lookup(var.configmanagement_templates, template, null)
+ }
+ ]
+ ])
+ cluster_cm_config = {
+ for k in local._cluster_cm_config : k.cluster => k.template if(
+ k.template != null &&
+ var.features.configmanagement == true
+ )
+ }
+ hub_features = {
+ for k, v in var.features : k => v if v != null && v != false && v != ""
+ }
+}
+
+resource "google_gke_hub_membership" "default" {
provider = google-beta
- for_each = var.member_clusters
- membership_id = each.key
+ for_each = var.clusters
project = var.project_id
+ membership_id = each.key
endpoint {
gke_cluster {
resource_link = each.value
}
}
-}
-
-resource "google_gke_hub_feature" "configmanagement" {
- provider = google-beta
- for_each = var.features.configmanagement ? { 1 = 1 } : {}
- project = var.project_id
- name = "configmanagement"
- location = "global"
-}
-
-resource "google_gke_hub_feature" "mci" {
- provider = google-beta
- for_each = var.features.mc_ingress ? var.member_clusters : {}
- project = var.project_id
- name = "multiclusteringress"
- location = "global"
- spec {
- multiclusteringress {
- config_membership = google_gke_hub_membership.membership[each.key].id
+ dynamic "authority" {
+ for_each = (
+ contains(var.workload_identity_clusters, each.key) ? {} : { 1 = 1 }
+ )
+ content {
+ issuer = "https://container.googleapis.com/v1/${var.clusters[each.key]}"
}
}
}
-resource "google_gke_hub_feature" "mcs" {
+resource "google_gke_hub_feature" "default" {
provider = google-beta
- for_each = var.features.mc_servicediscovery ? { 1 = 1 } : {}
+ for_each = local.hub_features
project = var.project_id
- name = "multiclusterservicediscovery"
+ name = each.key
location = "global"
+ dynamic "spec" {
+ for_each = each.key == "multiclusteringress" && each.value != null ? { 1 = 1 } : {}
+ content {
+ multiclusteringress {
+ config_membership = google_gke_hub_membership.default[each.value].id
+ }
+ }
+ }
}
-resource "google_gke_hub_feature_membership" "feature_member" {
+resource "google_gke_hub_feature_membership" "default" {
provider = google-beta
- for_each = var.member_clusters
+ for_each = local.cluster_cm_config
project = var.project_id
location = "global"
- feature = google_gke_hub_feature.configmanagement["1"].name
- membership = google_gke_hub_membership.membership[each.key].membership_id
+ feature = google_gke_hub_feature.default["configmanagement"].name
+ membership = google_gke_hub_membership.default[each.key].membership_id
- dynamic "configmanagement" {
- for_each = (
- try(var.member_features.configmanagement, null) != null
- ? [var.member_features.configmanagement]
- : []
- )
- iterator = configmanagement
+ configmanagement {
+ version = each.value.version
- content {
- version = try(configmanagement.value.version, null)
+ dynamic "binauthz" {
+ for_each = each.value.binauthz != true ? {} : { 1 = 1 }
+ content {
+ enabled = true
+ }
+ }
- dynamic "config_sync" {
- for_each = (
- try(configmanagement.value.config_sync, null) != null
- ? [configmanagement.value.config_sync]
- : []
- )
- iterator = config_sync
- content {
- git {
- https_proxy = try(config_sync.value.https_proxy, null)
- sync_repo = try(config_sync.value.sync_repo, null)
- sync_branch = try(config_sync.value.sync_branch, null)
- sync_rev = try(config_sync.value.sync_rev, null)
- secret_type = try(config_sync.value.secret_type, null)
- gcp_service_account_email = try(config_sync.value.gcp_service_account_email, null)
- policy_dir = try(config_sync.value.policy_dir, null)
+ dynamic "config_sync" {
+ for_each = each.value.config_sync == null ? {} : { 1 = 1 }
+ content {
+ prevent_drift = each.value.config_sync.prevent_drift
+ source_format = each.value.config_sync.source_format
+ dynamic "git" {
+ for_each = (
+ try(each.value.config_sync.git, null) == null ? {} : { 1 = 1 }
+ )
+ content {
+ gcp_service_account_email = (
+ each.value.config_sync.git.gcp_service_account_email
+ )
+ https_proxy = each.value.config_sync.git.https_proxy
+ policy_dir = each.value.config_sync.git.policy_dir
+ secret_type = each.value.config_sync.git.secret_type
+ sync_branch = each.value.config_sync.git.sync_branch
+ sync_repo = each.value.config_sync.git.sync_repo
+ sync_rev = each.value.config_sync.git.sync_rev
+ sync_wait_secs = each.value.config_sync.git.sync_wait_secs
}
- source_format = try(config_sync.value.source_format, null)
}
}
+ }
- dynamic "policy_controller" {
- for_each = (
- try(configmanagement.value.policy_controller, null) != null
- ? [configmanagement.value.policy_controller]
- : []
+ dynamic "hierarchy_controller" {
+ for_each = each.value.hierarchy_controller == null ? {} : { 1 = 1 }
+ content {
+ enable_hierarchical_resource_quota = (
+ each.value.hierarchy_controller.enable_hierarchical_resource_quota
)
- iterator = policy_controller
- content {
- enabled = true
- exemptable_namespaces = try(policy_controller.value.exemptable_namespaces, null)
- log_denies_enabled = try(policy_controller.value.log_denies_enabled, null)
- referential_rules_enabled = try(policy_controller.value.referential_rules_enabled, null)
- template_library_installed = try(policy_controller.value.template_library_installed, null)
- }
- }
-
- dynamic "binauthz" {
- for_each = (
- try(configmanagement.value.binauthz, false)
- ? [1]
- : []
+ enable_pod_tree_labels = (
+ each.value.hierarchy_controller.enable_pod_tree_labels
)
- content {
- enabled = true
- }
+ enabled = true
}
+ }
- dynamic "hierarchy_controller" {
- for_each = (
- try(var.member_features.hierarchy_controller, null) != null
- ? [var.member_features.hierarchy_controller]
- : []
+ dynamic "policy_controller" {
+ for_each = each.value.policy_controller == null ? {} : { 1 = 1 }
+ content {
+ audit_interval_seconds = (
+ each.value.policy_controller.audit_interval_seconds
)
- iterator = hierarchy_controller
- content {
- enabled = true
- enable_pod_tree_labels = try(hierarchy_controller.value.enable_pod_tree_labels)
- enable_hierarchical_resource_quota = try(hierarchy_controller.value.enable_hierarchical_resource_quota)
- }
+ exemptable_namespaces = (
+ each.value.policy_controller.exemptable_namespaces
+ )
+ log_denies_enabled = (
+ each.value.policy_controller.log_denies_enabled
+ )
+ referential_rules_enabled = (
+ each.value.policy_controller.referential_rules_enabled
+ )
+ template_library_installed = (
+ each.value.policy_controller.template_library_installed
+ )
+ enabled = true
}
}
}
diff --git a/modules/gke-hub/outputs.tf b/modules/gke-hub/outputs.tf
index fcf6d4efea..b4fd3462b3 100644
--- a/modules/gke-hub/outputs.tf
+++ b/modules/gke-hub/outputs.tf
@@ -15,12 +15,13 @@
*/
output "cluster_ids" {
- value = var.member_clusters
+ description = "Ids of all the clusters created."
+ value = {
+ for k, v in google_gke_hub_membership.default : k => v.id
+ }
depends_on = [
- google_gke_hub_membership.membership,
- google_gke_hub_feature.configmanagement,
- google_gke_hub_feature.mci,
- google_gke_hub_feature.mcs,
- google_gke_hub_feature_membership.feature_member,
+ google_gke_hub_membership.default,
+ google_gke_hub_feature.default,
+ google_gke_hub_feature_membership.default,
]
}
diff --git a/modules/gke-hub/variables.tf b/modules/gke-hub/variables.tf
index d099bd762e..c7133c07fb 100644
--- a/modules/gke-hub/variables.tf
+++ b/modules/gke-hub/variables.tf
@@ -14,60 +14,72 @@
* limitations under the License.
*/
-variable "features" {
- description = "GKE hub features to enable."
- type = object({
- configmanagement = bool
- mc_ingress = bool
- mc_servicediscovery = bool
- })
- default = {
- configmanagement = true
- mc_ingress = false
- mc_servicediscovery = false
- }
- nullable = false
+variable "clusters" {
+ description = "Clusters members of this GKE Hub in name => id format."
+ type = map(string)
+ default = {}
+ nullable = false
}
-variable "member_clusters" {
- description = "List for member cluster self links."
- type = map(string)
+variable "configmanagement_clusters" {
+ description = "Config management features enabled on specific sets of member clusters, in config name => [cluster name] format."
+ type = map(list(string))
default = {}
nullable = false
}
-variable "member_features" {
- description = "Member features for each cluster"
- type = object({
- configmanagement = object({
- binauthz = bool
- config_sync = object({
+variable "configmanagement_templates" {
+ description = "Sets of config management configurations that can be applied to member clusters, in config name => {options} format."
+ type = map(object({
+ binauthz = bool
+ config_sync = object({
+ git = object({
gcp_service_account_email = string
https_proxy = string
policy_dir = string
secret_type = string
- source_format = string
sync_branch = string
sync_repo = string
sync_rev = string
+ sync_wait_secs = number
})
- hierarchy_controller = object({
- enable_hierarchical_resource_quota = bool
- enable_pod_tree_labels = bool
- })
- policy_controller = object({
- exemptable_namespaces = list(string)
- log_denies_enabled = bool
- referential_rules_enabled = bool
- template_library_installed = bool
- })
- version = string
+ prevent_drift = string
+ source_format = string
+ })
+ hierarchy_controller = object({
+ enable_hierarchical_resource_quota = bool
+ enable_pod_tree_labels = bool
+ })
+ policy_controller = object({
+ audit_interval_seconds = number
+ exemptable_namespaces = list(string)
+ log_denies_enabled = bool
+ referential_rules_enabled = bool
+ template_library_installed = bool
})
- # mc-ingress = bool
- # mc-servicediscovery = bool
+ version = string
+ }))
+ default = {}
+ nullable = false
+}
+
+variable "features" {
+ description = "Enable and configue fleet features."
+ type = object({
+ appdevexperience = bool
+ configmanagement = bool
+ identityservice = bool
+ multiclusteringress = string
+ multiclusterservicediscovery = bool
+ servicemesh = bool
})
default = {
- configmanagement = null
+ appdevexperience = false
+ configmanagement = false
+ identityservice = false
+ multiclusteringress = null
+ servicemesh = false
+ multiclusterservicediscovery = false
}
nullable = false
}
@@ -76,3 +88,10 @@ variable "project_id" {
description = "GKE hub project ID."
type = string
}
+
+variable "workload_identity_clusters" {
+ description = "Clusters that will use Fleet Workload Identity."
+ type = list(string)
+ default = []
+ nullable = false
+}
diff --git a/modules/gke-hub/versions.tf b/modules/gke-hub/versions.tf
index 764197882f..90b632f6d4 100644
--- a/modules/gke-hub/versions.tf
+++ b/modules/gke-hub/versions.tf
@@ -13,15 +13,17 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
+
+
diff --git a/modules/gke-nodepool/README.md b/modules/gke-nodepool/README.md
index 731a161d5c..50e9d08cb1 100644
--- a/modules/gke-nodepool/README.md
+++ b/modules/gke-nodepool/README.md
@@ -6,30 +6,74 @@ This module allows simplified creation and management of individual GKE nodepool
### Module defaults
-If no specific node configuration is set via variables, the module uses the provider's defaults only setting OAuth scopes to a minimal working set (devstorage read-only, logging and monitoring write) and the node machine type to `n1-standard-1`. The service account set by the provider in this case is the GCE default service account.
+If no specific node configuration is set via variables, the module uses the provider's defaults only setting OAuth scopes to a minimal working set and the node machine type to `n1-standard-1`. The service account set by the provider in this case is the GCE default service account.
```hcl
module "cluster-1-nodepool-1" {
- source = "./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
```
### Internally managed service account
-To have the module auto-create a service account for the nodes, set the `node_service_account_create` variable to `true`. When a service account is created by the module, OAuth scopes are set to `cloud-platform` by default. The service account resource and email (in both plain and IAM formats) are then available in outputs to assign IAM roles from your own code.
+There are three different approaches to defining the nodes service account, all depending on the `service_account` variable where the `create` attribute controls creation of a new service account by this module, and the `email` attribute controls the actual service account to use.
+
+If you create a new service account, its resource and email (in both plain and IAM formats) are then available in outputs to reference it in other modules or resources.
+
+#### 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
+```
+
+#### Externally defined service account
+
+To use an existing service account, pass in just the `email` attribute.
+
+```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"
+ service_account = {
+ email = "foo-bar@myproject.iam.gserviceaccount.com"
+ }
+}
+# tftest modules=1 resources=1
+```
+
+#### Auto-created service account
+
+To have the module create a service account, set the `create` attribute to `true` and optionally pass the desired account id in `email`.
```hcl
module "cluster-1-nodepool-1" {
- source = "./modules/gke-nodepool"
- project_id = "myproject"
- cluster_name = "cluster-1"
- location = "europe-west1-b"
- name = "nodepool-1"
- node_service_account_create = true
+ 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"
+ }
}
# tftest modules=1 resources=2
```
@@ -39,47 +83,31 @@ module "cluster-1-nodepool-1" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [cluster_name](variables.tf#L26) | Cluster name. | string
| ✓ | |
-| [location](variables.tf#L59) | Cluster location. | string
| ✓ | |
-| [project_id](variables.tf#L210) | Cluster project id. | string
| ✓ | |
-| [autoscaling_config](variables.tf#L17) | Optional autoscaling configuration. | object({…})
| | null
|
-| [gke_version](variables.tf#L31) | Kubernetes nodes version. Ignored if auto_upgrade is set in management_config. | string
| | null
|
-| [initial_node_count](variables.tf#L37) | Initial number of nodes for the pool. | number
| | 1
|
-| [kubelet_config](variables.tf#L43) | Kubelet configuration. | object({…})
| | null
|
-| [linux_node_config_sysctls](variables.tf#L53) | Linux node configuration. | map(string)
| | null
|
-| [management_config](variables.tf#L64) | Optional node management configuration. | object({…})
| | null
|
-| [max_pods_per_node](variables.tf#L73) | Maximum number of pods per node. | number
| | null
|
-| [name](variables.tf#L79) | Optional nodepool name. | string
| | null
|
-| [node_boot_disk_kms_key](variables.tf#L85) | Customer Managed Encryption Key used to encrypt the boot disk attached to each node. | string
| | null
|
-| [node_count](variables.tf#L91) | Number of nodes per instance group, can be updated after creation. Ignored when autoscaling is set. | number
| | null
|
-| [node_disk_size](variables.tf#L97) | Node disk size, defaults to 100GB. | number
| | 100
|
-| [node_disk_type](variables.tf#L103) | Node disk type, defaults to pd-standard. | string
| | "pd-standard"
|
-| [node_guest_accelerator](variables.tf#L109) | Map of type and count of attached accelerator cards. | map(number)
| | {}
|
-| [node_image_type](variables.tf#L115) | Nodes image type. | string
| | null
|
-| [node_labels](variables.tf#L121) | Kubernetes labels attached to nodes. | map(string)
| | {}
|
-| [node_local_ssd_count](variables.tf#L127) | Number of local SSDs attached to nodes. | number
| | 0
|
-| [node_locations](variables.tf#L132) | Optional list of zones in which nodes should be located. Uses cluster locations if unset. | list(string)
| | null
|
-| [node_machine_type](variables.tf#L138) | Nodes machine type. | string
| | "n1-standard-1"
|
-| [node_metadata](variables.tf#L144) | Metadata key/value pairs assigned to nodes. Set disable-legacy-endpoints to true when using this variable. | map(string)
| | null
|
-| [node_min_cpu_platform](variables.tf#L150) | Minimum CPU platform for nodes. | string
| | null
|
-| [node_preemptible](variables.tf#L156) | Use preemptible VMs for nodes. | bool
| | null
|
-| [node_sandbox_config](variables.tf#L162) | GKE Sandbox configuration. Needs image_type set to COS_CONTAINERD and node_version set to 1.12.7-gke.17 when using this variable. | string
| | null
|
-| [node_service_account](variables.tf#L168) | Service account email. Unused if service account is auto-created. | string
| | null
|
-| [node_service_account_create](variables.tf#L174) | Auto-create service account. | bool
| | false
|
-| [node_service_account_scopes](variables.tf#L182) | Scopes applied to service account. Default to: 'cloud-platform' when creating a service account; 'devstorage.read_only', 'logging.write', 'monitoring.write' otherwise. | list(string)
| | []
|
-| [node_shielded_instance_config](variables.tf#L188) | Shielded instance options. | object({…})
| | null
|
-| [node_tags](variables.tf#L197) | Network tags applied to nodes. | list(string)
| | null
|
-| [node_taints](variables.tf#L203) | Kubernetes taints applied to nodes. E.g. type=blue:NoSchedule. | list(string)
| | []
|
-| [upgrade_config](variables.tf#L215) | Optional node upgrade configuration. | object({…})
| | null
|
-| [workload_metadata_config](variables.tf#L224) | Metadata configuration to expose to workloads on the node pool. | string
| | "GKE_METADATA"
|
+| [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
| name | description | sensitive |
|---|---|:---:|
| [name](outputs.tf#L17) | Nodepool name. | |
-| [service_account](outputs.tf#L22) | Service account resource. | |
-| [service_account_email](outputs.tf#L31) | Service account email. | |
-| [service_account_iam_email](outputs.tf#L36) | Service account email. | |
+| [service_account_email](outputs.tf#L22) | Service account email. | |
+| [service_account_iam_email](outputs.tf#L27) | Service account email. | |
diff --git a/modules/gke-nodepool/main.tf b/modules/gke-nodepool/main.tf
index 0b7268ffdd..ad0c053f35 100644
--- a/modules/gke-nodepool/main.tf
+++ b/modules/gke-nodepool/main.tf
@@ -15,178 +15,213 @@
*/
locals {
- service_account_email = (
- var.node_service_account_create
- ? (
- length(google_service_account.service_account) > 0
- ? google_service_account.service_account[0].email
- : null
+ _image = coalesce(var.node_config.image_type, "-")
+ image = {
+ is_cos = length(regexall("COS", local._image)) > 0
+ is_cos_containerd = (
+ var.node_config.image_type == null
+ ||
+ length(regexall("COS_CONTAINERD", local._image)) > 0
)
- : var.node_service_account
+ is_win = length(regexall("WIN", local._image)) > 0
+ }
+ node_metadata = var.node_config.metadata == null ? null : merge(
+ var.node_config.metadata,
+ { disable-legacy-endpoints = "true" }
)
- service_account_scopes = (
- length(var.node_service_account_scopes) > 0
- ? var.node_service_account_scopes
- : (
- var.node_service_account_create
- ? ["https://www.googleapis.com/auth/cloud-platform"]
- : [
- "https://www.googleapis.com/auth/devstorage.read_only",
- "https://www.googleapis.com/auth/logging.write",
- "https://www.googleapis.com/auth/monitoring",
- "https://www.googleapis.com/auth/monitoring.write"
- ]
- )
+ # if no attributes passed for service account, use the GCE default
+ # if no email specified, create service account
+ service_account_email = (
+ var.service_account.create
+ ? google_service_account.service_account[0].email
+ : var.service_account.email
)
- node_taint_effect = {
- "NoExecute" = "NO_EXECUTE",
- "NoSchedule" = "NO_SCHEDULE"
- "PreferNoSchedule" = "PREFER_NO_SCHEDULE"
- }
- temp_node_pools_taints = [
- for taint in var.node_taints :
- {
- "key" = element(split("=", taint), 0),
- "value" = element(split(":", element(split("=", taint), 1)), 0),
- "effect" = lookup(local.node_taint_effect, element(split(":", taint), 1)),
- }
- ]
- # The taint is added to match the one that
- # GKE implicitly adds when Windows node pools are created.
- win_node_pools_taint = (
- var.node_image_type == null
- ? []
- : length(regexall("WINDOWS", var.node_image_type)) > 0
- ? [
- {
- "key" = "node.kubernetes.io/os"
- "value" = "windows"
- "effect" = local.node_taint_effect.NoSchedule
- }
+ service_account_scopes = (
+ var.service_account.oauth_scopes != null
+ ? var.service_account.oauth_scopes
+ : [
+ "https://www.googleapis.com/auth/devstorage.read_only",
+ "https://www.googleapis.com/auth/logging.write",
+ "https://www.googleapis.com/auth/monitoring",
+ "https://www.googleapis.com/auth/monitoring.write",
+ "https://www.googleapis.com/auth/userinfo.email"
]
+ )
+ taints_windows = (
+ local.image.is_win
+ ? [{
+ key = "node.kubernetes.io/os", value = "windows", effect = "NO_EXECUTE"
+ }]
: []
)
- node_taints = concat(local.temp_node_pools_taints, local.win_node_pools_taint)
}
resource "google_service_account" "service_account" {
- count = var.node_service_account_create ? 1 : 0
- project = var.project_id
- account_id = "tf-gke-${var.name}"
+ count = var.service_account.create ? 1 : 0
+ project = var.project_id
+ account_id = (
+ var.service_account.email != null
+ ? split("@", var.service_account.email)[0]
+ : "tf-gke-${var.name}"
+ )
display_name = "Terraform GKE ${var.cluster_name} ${var.name}."
}
resource "google_container_node_pool" "nodepool" {
- provider = google-beta
-
- project = var.project_id
- cluster = var.cluster_name
- location = var.location
- name = var.name
-
- initial_node_count = var.initial_node_count
+ provider = google-beta
+ project = var.project_id
+ cluster = coalesce(var.cluster_id, var.cluster_name)
+ location = var.location
+ name = var.name
+ version = var.gke_version
max_pods_per_node = var.max_pods_per_node
- node_count = var.autoscaling_config == null ? var.node_count : null
+ initial_node_count = var.node_count.initial
+ node_count = var.node_count.current
node_locations = var.node_locations
- version = var.gke_version
+ # placement_policy = var.nodepool_config.placement_policy
+
+ dynamic "autoscaling" {
+ for_each = (
+ try(var.nodepool_config.autoscaling, null) != null
+ &&
+ !try(var.nodepool_config.autoscaling.use_total_nodes, false)
+ ? [""] : []
+ )
+ content {
+ location_policy = try(var.nodepool_config.autoscaling.location_policy, null)
+ max_node_count = try(var.nodepool_config.autoscaling.max_node_count, null)
+ min_node_count = try(var.nodepool_config.autoscaling.min_node_count, null)
+ }
+ }
+ dynamic "autoscaling" {
+ for_each = (
+ try(var.nodepool_config.autoscaling.use_total_nodes, false) ? [""] : []
+ )
+ content {
+ location_policy = try(var.nodepool_config.autoscaling.location_policy, null)
+ total_max_node_count = try(var.nodepool_config.autoscaling.max_node_count, null)
+ total_min_node_count = try(var.nodepool_config.autoscaling.min_node_count, null)
+ }
+ }
+
+ dynamic "management" {
+ for_each = try(var.nodepool_config.management, null) != null ? [""] : []
+ content {
+ auto_repair = try(var.nodepool_config.management.auto_repair, null)
+ auto_upgrade = try(var.nodepool_config.management.auto_upgrade, null)
+ }
+ }
+
+ 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
+ }
+ }
+
+ dynamic "upgrade_settings" {
+ for_each = try(var.nodepool_config.upgrade_settings, null) != null ? [""] : []
+ content {
+ max_surge = try(var.nodepool_config.upgrade_settings.max_surge, null)
+ max_unavailable = try(var.nodepool_config.upgrade_settings.max_unavailable, null)
+ }
+ }
node_config {
- disk_size_gb = var.node_disk_size
- disk_type = var.node_disk_type
- image_type = var.node_image_type
- labels = var.node_labels
- taint = local.node_taints
- local_ssd_count = var.node_local_ssd_count
- machine_type = var.node_machine_type
- metadata = var.node_metadata
- min_cpu_platform = var.node_min_cpu_platform
+ boot_disk_kms_key = var.node_config.boot_disk_kms_key
+ disk_size_gb = var.node_config.disk_size_gb
+ disk_type = var.node_config.disk_type
+ image_type = var.node_config.image_type
+ labels = var.labels
+ local_ssd_count = var.node_config.local_ssd_count
+ machine_type = var.node_config.machine_type
+ metadata = local.node_metadata
+ min_cpu_platform = var.node_config.min_cpu_platform
+ node_group = var.sole_tenant_nodegroup
oauth_scopes = local.service_account_scopes
- preemptible = var.node_preemptible
+ preemptible = var.node_config.preemptible
service_account = local.service_account_email
- tags = var.node_tags
- boot_disk_kms_key = var.node_boot_disk_kms_key
+ spot = (
+ var.node_config.spot == true && var.node_config.preemptible != true
+ )
+ tags = var.tags
+ taint = (
+ var.taints == null ? [] : concat(var.taints, local.taints_windows)
+ )
- dynamic "guest_accelerator" {
- for_each = var.node_guest_accelerator
- iterator = config
+ dynamic "ephemeral_storage_config" {
+ for_each = var.node_config.ephemeral_ssd_count != null ? [""] : []
content {
- type = config.key
- count = config.value
+ local_ssd_count = var.node_config.ephemeral_ssd_count
}
}
-
- dynamic "sandbox_config" {
- for_each = (
- var.node_sandbox_config != null
- ? [var.node_sandbox_config]
- : []
- )
- iterator = config
+ dynamic "gcfs_config" {
+ for_each = var.node_config.gcfs && local.image.is_cos_containerd ? [""] : []
content {
- sandbox_type = config.value
+ enabled = true
}
}
-
- dynamic "shielded_instance_config" {
- for_each = (
- var.node_shielded_instance_config != null
- ? [var.node_shielded_instance_config]
- : []
- )
- iterator = config
+ dynamic "guest_accelerator" {
+ for_each = var.node_config.guest_accelerator != null ? [""] : []
content {
- enable_secure_boot = config.value.enable_secure_boot
- enable_integrity_monitoring = config.value.enable_integrity_monitoring
+ count = var.node_config.guest_accelerator.count
+ type = var.node_config.guest_accelerator.type
+ gpu_partition_size = var.node_config.guest_accelerator.gpu_partition_size
}
}
-
- workload_metadata_config {
- mode = var.workload_metadata_config
+ dynamic "gvnic" {
+ for_each = var.node_config.gvnic && local.image.is_cos ? [""] : []
+ content {
+ enabled = true
+ }
}
-
dynamic "kubelet_config" {
- for_each = var.kubelet_config != null ? [var.kubelet_config] : []
- iterator = config
+ for_each = var.node_config.kubelet_config != null ? [""] : []
content {
- cpu_manager_policy = config.value.cpu_manager_policy
- cpu_cfs_quota = config.value.cpu_cfs_quota
- cpu_cfs_quota_period = config.value.cpu_cfs_quota_period
+ cpu_manager_policy = var.node_config.kubelet_config.cpu_manager_policy
+ cpu_cfs_quota = var.node_config.kubelet_config.cpu_cfs_quota
+ cpu_cfs_quota_period = var.node_config.kubelet_config.cpu_cfs_quota_period
}
}
-
dynamic "linux_node_config" {
- for_each = var.linux_node_config_sysctls != null ? [var.linux_node_config_sysctls] : []
- iterator = config
+ for_each = var.node_config.linux_node_config_sysctls != null ? [""] : []
content {
- sysctls = config.value
+ sysctls = var.node_config.linux_node_config_sysctls
}
}
- }
-
- dynamic "autoscaling" {
- for_each = var.autoscaling_config != null ? [var.autoscaling_config] : []
- iterator = config
- content {
- min_node_count = config.value.min_node_count
- max_node_count = config.value.max_node_count
+ dynamic "reservation_affinity" {
+ for_each = var.reservation_affinity != null ? [""] : []
+ content {
+ consume_reservation_type = var.reservation_affinity.consume_reservation_type
+ key = var.reservation_affinity.key
+ values = var.reservation_affinity.values
+ }
}
- }
-
- dynamic "management" {
- for_each = var.management_config != null ? [var.management_config] : []
- iterator = config
- content {
- auto_repair = config.value.auto_repair
- auto_upgrade = config.value.auto_upgrade
+ dynamic "sandbox_config" {
+ for_each = (
+ var.node_config.sandbox_config_gvisor == true &&
+ local.image.is_cos_containerd != null
+ ? [""]
+ : []
+ )
+ content {
+ sandbox_type = "gvisor"
+ }
}
- }
-
- dynamic "upgrade_settings" {
- for_each = var.upgrade_config != null ? [var.upgrade_config] : []
- iterator = config
- content {
- max_surge = config.value.max_surge
- max_unavailable = config.value.max_unavailable
+ dynamic "shielded_instance_config" {
+ for_each = var.node_config.shielded_instance_config != null ? [""] : []
+ content {
+ enable_secure_boot = var.node_config.shielded_instance_config.enable_secure_boot
+ enable_integrity_monitoring = var.node_config.shielded_instance_config.enable_integrity_monitoring
+ }
+ }
+ dynamic "workload_metadata_config" {
+ for_each = var.node_config.workload_metadata_config_mode != null ? [""] : []
+ content {
+ mode = var.node_config.workload_metadata_config_mode
+ }
}
}
}
diff --git a/modules/gke-nodepool/outputs.tf b/modules/gke-nodepool/outputs.tf
index 2736acbf89..b0a94801c1 100644
--- a/modules/gke-nodepool/outputs.tf
+++ b/modules/gke-nodepool/outputs.tf
@@ -19,15 +19,6 @@ output "name" {
value = google_container_node_pool.nodepool.name
}
-output "service_account" {
- description = "Service account resource."
- value = (
- var.node_service_account_create
- ? google_service_account.service_account[0]
- : null
- )
-}
-
output "service_account_email" {
description = "Service account email."
value = local.service_account_email
@@ -35,8 +26,8 @@ output "service_account_email" {
output "service_account_iam_email" {
description = "Service account email."
- value = join("", [
- "serviceAccount:",
+ value = format(
+ "serviceAccount:%s",
local.service_account_email == null ? "" : local.service_account_email
- ])
+ )
}
diff --git a/modules/gke-nodepool/variables.tf b/modules/gke-nodepool/variables.tf
index 6b7cdbc51f..e0d3e967a5 100644
--- a/modules/gke-nodepool/variables.tf
+++ b/modules/gke-nodepool/variables.tf
@@ -14,13 +14,10 @@
* limitations under the License.
*/
-variable "autoscaling_config" {
- description = "Optional autoscaling configuration."
- type = object({
- min_node_count = number
- max_node_count = number
- })
- default = null
+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" {
@@ -34,26 +31,11 @@ variable "gke_version" {
default = null
}
-variable "initial_node_count" {
- description = "Initial number of nodes for the pool."
- type = number
- default = 1
-}
-
-variable "kubelet_config" {
- description = "Kubelet configuration."
- type = object({
- cpu_cfs_quota = string
- cpu_cfs_quota_period = string
- cpu_manager_policy = string
- })
- default = null
-}
-
-variable "linux_node_config_sysctls" {
- description = "Linux node configuration."
+variable "labels" {
+ description = "Kubernetes labels applied to each node."
type = map(string)
- default = null
+ default = {}
+ nullable = false
}
variable "location" {
@@ -61,15 +43,6 @@ variable "location" {
type = string
}
-variable "management_config" {
- description = "Optional node management configuration."
- type = object({
- auto_repair = bool
- auto_upgrade = bool
- })
- default = null
-}
-
variable "max_pods_per_node" {
description = "Maximum number of pods per node."
type = number
@@ -82,147 +55,141 @@ variable "name" {
default = null
}
-variable "node_boot_disk_kms_key" {
- description = "Customer Managed Encryption Key used to encrypt the boot disk attached to each node."
- type = string
- default = null
+variable "node_config" {
+ description = "Node-level configuration."
+ type = object({
+ boot_disk_kms_key = optional(string)
+ disk_size_gb = optional(number)
+ disk_type = optional(string)
+ ephemeral_ssd_count = optional(number)
+ gcfs = optional(bool, false)
+ guest_accelerator = optional(object({
+ count = number
+ type = string
+ gpu_partition_size = optional(string)
+ }))
+ gvnic = optional(bool, false)
+ image_type = optional(string)
+ kubelet_config = optional(object({
+ cpu_manager_policy = string
+ cpu_cfs_quota = optional(bool)
+ cpu_cfs_quota_period = optional(string)
+ }))
+ linux_node_config_sysctls = optional(map(string))
+ local_ssd_count = optional(number)
+ machine_type = optional(string)
+ metadata = optional(map(string))
+ min_cpu_platform = optional(string)
+ preemptible = optional(bool)
+ sandbox_config_gvisor = optional(bool)
+ shielded_instance_config = optional(object({
+ enable_integrity_monitoring = optional(bool)
+ enable_secure_boot = optional(bool)
+ }))
+ spot = optional(bool)
+ workload_metadata_config_mode = optional(string)
+ })
+ default = {
+ disk_type = "pd-balanced"
+ }
}
variable "node_count" {
- description = "Number of nodes per instance group, can be updated after creation. Ignored when autoscaling is set."
- type = number
- default = null
-}
-
-variable "node_disk_size" {
- description = "Node disk size, defaults to 100GB."
- type = number
- default = 100
-}
-
-variable "node_disk_type" {
- description = "Node disk type, defaults to pd-standard."
- type = string
- default = "pd-standard"
-}
-
-variable "node_guest_accelerator" {
- description = "Map of type and count of attached accelerator cards."
- type = map(number)
- default = {}
-}
-
-variable "node_image_type" {
- description = "Nodes image type."
- type = string
- default = null
-}
-
-variable "node_labels" {
- description = "Kubernetes labels attached to nodes."
- type = map(string)
- default = {}
+ description = "Number of nodes per instance group. Initial value can only be changed by recreation, current is ignored when autoscaling is used."
+ type = object({
+ current = optional(number)
+ initial = number
+ })
+ default = {
+ initial = 1
+ }
+ nullable = false
}
-variable "node_local_ssd_count" {
- description = "Number of local SSDs attached to nodes."
- type = number
- default = 0
-}
variable "node_locations" {
- description = "Optional list of zones in which nodes should be located. Uses cluster locations if unset."
+ description = "Node locations."
type = list(string)
default = null
}
-variable "node_machine_type" {
- description = "Nodes machine type."
- type = string
- default = "n1-standard-1"
+variable "nodepool_config" {
+ description = "Nodepool-level configuration."
+ type = object({
+ autoscaling = optional(object({
+ location_policy = optional(string)
+ max_node_count = optional(number)
+ min_node_count = optional(number)
+ use_total_nodes = optional(bool, false)
+ }))
+ management = optional(object({
+ auto_repair = optional(bool)
+ auto_upgrade = optional(bool)
+ }))
+ # placement_policy = optional(bool)
+ upgrade_settings = optional(object({
+ max_surge = number
+ max_unavailable = number
+ }))
+ })
+ default = null
}
-variable "node_metadata" {
- description = "Metadata key/value pairs assigned to nodes. Set disable-legacy-endpoints to true when using this variable."
- type = map(string)
- default = null
+variable "pod_range" {
+ description = "Pod secondary range configuration."
+ type = object({
+ secondary_pod_range = object({
+ cidr = optional(string)
+ create = optional(bool)
+ name = string
+ })
+ })
+ default = null
}
-variable "node_min_cpu_platform" {
- description = "Minimum CPU platform for nodes."
+variable "project_id" {
+ description = "Cluster project id."
type = string
- default = null
}
-variable "node_preemptible" {
- description = "Use preemptible VMs for nodes."
- type = bool
- default = null
+variable "reservation_affinity" {
+ description = "Configuration of the desired reservation which instances could take capacity from."
+ type = object({
+ consume_reservation_type = string
+ key = optional(string)
+ values = optional(list(string))
+ })
+ default = null
}
-variable "node_sandbox_config" {
- description = "GKE Sandbox configuration. Needs image_type set to COS_CONTAINERD and node_version set to 1.12.7-gke.17 when using this variable."
- type = string
- default = null
+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)
+ })
+ default = {}
+ nullable = false
}
-variable "node_service_account" {
- description = "Service account email. Unused if service account is auto-created."
+variable "sole_tenant_nodegroup" {
+ description = "Sole tenant node group."
type = string
default = null
}
-variable "node_service_account_create" {
- description = "Auto-create service account."
- type = bool
- default = false
-}
-
-# scopes and scope aliases list
-# https://cloud.google.com/sdk/gcloud/reference/compute/instances/create#--scopes
-variable "node_service_account_scopes" {
- description = "Scopes applied to service account. Default to: 'cloud-platform' when creating a service account; 'devstorage.read_only', 'logging.write', 'monitoring.write' otherwise."
- type = list(string)
- default = []
-}
-
-variable "node_shielded_instance_config" {
- description = "Shielded instance options."
- type = object({
- enable_secure_boot = bool
- enable_integrity_monitoring = bool
- })
- default = null
-}
-
-variable "node_tags" {
+variable "tags" {
description = "Network tags applied to nodes."
type = list(string)
default = null
}
-variable "node_taints" {
- description = "Kubernetes taints applied to nodes. E.g. type=blue:NoSchedule."
- type = list(string)
- default = []
-}
-
-
-variable "project_id" {
- description = "Cluster project id."
- type = string
-}
-
-variable "upgrade_config" {
- description = "Optional node upgrade configuration."
- type = object({
- max_surge = number
- max_unavailable = number
- })
+variable "taints" {
+ description = "Kubernetes taints applied to all nodes."
+ type = list(object({
+ key = string
+ value = string
+ effect = string
+ }))
default = null
}
-
-variable "workload_metadata_config" {
- description = "Metadata configuration to expose to workloads on the node pool."
- type = string
- default = "GKE_METADATA"
-}
diff --git a/modules/gke-nodepool/versions.tf b/modules/gke-nodepool/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/gke-nodepool/versions.tf
+++ b/modules/gke-nodepool/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/iam-service-account/README.md b/modules/iam-service-account/README.md
index 30085dabfa..a62ce8f536 100644
--- a/modules/iam-service-account/README.md
+++ b/modules/iam-service-account/README.md
@@ -1,17 +1,18 @@
# Google Service Account Module
-This module allows simplified creation and management of one a service account and its IAM bindings. A key can optionally be generated and will be stored in Terraform state. To use it create a sensitive output in your root modules referencing the `key` output, then extract the private key from the JSON formatted outputs. Alternatively, the `key` can be generated with `openssl` library and only public part uploaded to the Service Account, for more refer to the [Onprem SA Key Management](../../examples/cloud-operations/onprem-sa-key-management/) example.
+This module allows simplified creation and management of one a service account and its IAM bindings. A key can optionally be generated and will be stored in Terraform state. To use it create a sensitive output in your root modules referencing the `key` output, then extract the private key from the JSON formatted outputs. Alternatively, the `key` can be generated with `openssl` library and only public part uploaded to the Service Account, for more refer to the [Onprem SA Key Management](../../blueprints/cloud-operations/onprem-sa-key-management/) example.
+
+Note that this module does not fully comply with our design principles, as outputs have no dependencies on IAM bindings to prevent resource cycles.
## Example
```hcl
module "myproject-default-service-accounts" {
- source = "./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
@@ -22,7 +23,7 @@ module "myproject-default-service-accounts" {
]
}
}
-# tftest modules=1 resources=5
+# tftest modules=1 resources=4 inventory=basic.yaml
```
@@ -31,7 +32,7 @@ module "myproject-default-service-accounts" {
| name | description | resources |
|---|---|---|
-| [iam.tf](./iam.tf) | IAM bindings. | google_billing_account_iam_member
· google_folder_iam_member
· google_organization_iam_member
· google_project_iam_member
· google_service_account_iam_binding
· google_storage_bucket_iam_member
|
+| [iam.tf](./iam.tf) | IAM bindings. | google_billing_account_iam_member
· google_folder_iam_member
· google_organization_iam_member
· google_project_iam_member
· google_service_account_iam_binding
· google_service_account_iam_member
· google_storage_bucket_iam_member
|
| [main.tf](./main.tf) | Module-level locals and resources. | google_service_account
· google_service_account_key
|
| [outputs.tf](./outputs.tf) | Module outputs. | |
| [variables.tf](./variables.tf) | Module variables. | |
@@ -41,20 +42,22 @@ module "myproject-default-service-accounts" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [name](variables.tf#L77) | Name of the service account to create. | string
| ✓ | |
-| [project_id](variables.tf#L88) | Project id where service account will be created. | string
| ✓ | |
+| [name](variables.tf#L91) | Name of the service account to create. | string
| ✓ | |
+| [project_id](variables.tf#L106) | Project id where service account will be created. | string
| ✓ | |
| [description](variables.tf#L17) | Optional description. | string
| | null
|
| [display_name](variables.tf#L23) | Display name of the service account to create. | string
| | "Terraform-managed."
|
| [generate_key](variables.tf#L29) | Generate a key for service account. | bool
| | false
|
| [iam](variables.tf#L35) | IAM bindings on the service account in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
-| [iam_billing_roles](variables.tf#L42) | Billing account roles granted to the service account, by billing account id. Non-authoritative. | map(list(string))
| | {}
|
-| [iam_folder_roles](variables.tf#L49) | Folder roles granted to the service account, by folder id. Non-authoritative. | map(list(string))
| | {}
|
-| [iam_organization_roles](variables.tf#L56) | Organization roles granted to the service account, by organization id. Non-authoritative. | map(list(string))
| | {}
|
-| [iam_project_roles](variables.tf#L63) | Project roles granted to the service account, by project id. | map(list(string))
| | {}
|
-| [iam_storage_roles](variables.tf#L70) | Storage roles granted to the service account, by bucket name. | map(list(string))
| | {}
|
-| [prefix](variables.tf#L82) | Prefix applied to service account names. | string
| | null
|
-| [public_keys_directory](variables.tf#L93) | Path to public keys data files to upload to the service account (should have `.pem` extension). | string
| | ""
|
-| [service_account_create](variables.tf#L99) | Create service account. When set to false, uses a data source to reference an existing service account. | bool
| | true
|
+| [iam_additive](variables.tf#L42) | IAM additive bindings on the service account in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
+| [iam_billing_roles](variables.tf#L49) | Billing account roles granted to this service account, by billing account id. Non-authoritative. | map(list(string))
| | {}
|
+| [iam_folder_roles](variables.tf#L56) | Folder roles granted to this service account, by folder id. Non-authoritative. | map(list(string))
| | {}
|
+| [iam_organization_roles](variables.tf#L63) | Organization roles granted to this service account, by organization id. Non-authoritative. | map(list(string))
| | {}
|
+| [iam_project_roles](variables.tf#L70) | Project roles granted to this service account, by project id. | map(list(string))
| | {}
|
+| [iam_sa_roles](variables.tf#L77) | Service account roles granted to this service account, by service account name. | map(list(string))
| | {}
|
+| [iam_storage_roles](variables.tf#L84) | Storage roles granted to this service account, by bucket name. | map(list(string))
| | {}
|
+| [prefix](variables.tf#L96) | Prefix applied to service account names. | string
| | null
|
+| [public_keys_directory](variables.tf#L111) | Path to public keys data files to upload to the service account (should have `.pem` extension). | string
| | ""
|
+| [service_account_create](variables.tf#L117) | Create service account. When set to false, uses a data source to reference an existing service account. | bool
| | true
|
## Outputs
@@ -62,8 +65,10 @@ module "myproject-default-service-accounts" {
|---|---|:---:|
| [email](outputs.tf#L17) | Service account email. | |
| [iam_email](outputs.tf#L25) | IAM-format service account email. | |
-| [key](outputs.tf#L33) | Service account key. | ✓ |
-| [service_account](outputs.tf#L39) | Service account resource. | |
-| [service_account_credentials](outputs.tf#L44) | Service account json credential templates for uploaded public keys data. | |
+| [id](outputs.tf#L33) | Service account id. | |
+| [key](outputs.tf#L42) | Service account key. | ✓ |
+| [name](outputs.tf#L48) | Service account name. | |
+| [service_account](outputs.tf#L57) | Service account resource. | |
+| [service_account_credentials](outputs.tf#L62) | Service account json credential templates for uploaded public keys data. | |
diff --git a/modules/iam-service-account/iam.tf b/modules/iam-service-account/iam.tf
index b50fadecf4..02c879d9d7 100644
--- a/modules/iam-service-account/iam.tf
+++ b/modules/iam-service-account/iam.tf
@@ -17,6 +17,15 @@
# tfdoc:file:description IAM bindings.
locals {
+ _iam_additive_pairs = flatten([
+ for role, members in var.iam_additive : [
+ for member in members : { role = role, member = member }
+ ]
+ ])
+ iam_additive = {
+ for pair in local._iam_additive_pairs :
+ "${pair.role}-${pair.member}" => pair
+ }
iam_billing_pairs = flatten([
for entity, roles in var.iam_billing_roles : [
for role in roles : [
@@ -45,6 +54,13 @@ locals {
]
]
])
+ iam_sa_pairs = flatten([
+ for entity, roles in var.iam_sa_roles : [
+ for role in roles : [
+ { entity = entity, role = role }
+ ]
+ ]
+ ])
iam_storage_pairs = flatten([
for entity, roles in var.iam_storage_roles : [
for role in roles : [
@@ -54,6 +70,13 @@ locals {
])
}
+resource "google_service_account_iam_member" "roles" {
+ for_each = local.iam_additive
+ service_account_id = local.service_account.name
+ role = each.value.role
+ member = each.value.member
+}
+
resource "google_service_account_iam_binding" "roles" {
for_each = var.iam
service_account_id = local.service_account.name
@@ -101,6 +124,16 @@ resource "google_project_iam_member" "project-roles" {
member = local.resource_iam_email
}
+resource "google_service_account_iam_member" "additive" {
+ for_each = {
+ for pair in local.iam_sa_pairs :
+ "${pair.entity}-${pair.role}" => pair
+ }
+ service_account_id = each.value.entity
+ role = each.value.role
+ member = local.resource_iam_email
+}
+
resource "google_storage_bucket_iam_member" "bucket-roles" {
for_each = {
for pair in local.iam_storage_pairs :
diff --git a/modules/iam-service-account/main.tf b/modules/iam-service-account/main.tf
index 329d676e4e..2c9ee36bee 100644
--- a/modules/iam-service-account/main.tf
+++ b/modules/iam-service-account/main.tf
@@ -21,10 +21,15 @@ locals {
? google_service_account_key.key["1"]
: map("", null)
, {})
- prefix = var.prefix != null ? "${var.prefix}-" : ""
- resource_email_static = "${local.prefix}${var.name}@${var.project_id}.iam.gserviceaccount.com"
+ prefix = var.prefix == null ? "" : "${var.prefix}-"
+ resource_email_static = "${local.prefix}${var.name}@${var.project_id}.iam.gserviceaccount.com"
+ resource_iam_email = (
+ local.service_account != null
+ ? "serviceAccount:${local.service_account.email}"
+ : local.resource_iam_email_static
+ )
resource_iam_email_static = "serviceAccount:${local.resource_email_static}"
- resource_iam_email = local.service_account != null ? "serviceAccount:${local.service_account.email}" : local.resource_iam_email_static
+ service_account_id_static = "projects/${var.project_id}/serviceAccounts/${local.resource_email_static}"
service_account = (
var.service_account_create
? try(google_service_account.service_account.0, null)
diff --git a/modules/iam-service-account/outputs.tf b/modules/iam-service-account/outputs.tf
index 8653ccc7cd..e6c28dfdab 100644
--- a/modules/iam-service-account/outputs.tf
+++ b/modules/iam-service-account/outputs.tf
@@ -30,12 +30,30 @@ output "iam_email" {
]
}
+output "id" {
+ description = "Service account id."
+ value = local.service_account_id_static
+ depends_on = [
+ data.google_service_account.service_account,
+ google_service_account.service_account
+ ]
+}
+
output "key" {
description = "Service account key."
sensitive = true
value = local.key
}
+output "name" {
+ description = "Service account name."
+ value = local.service_account_id_static
+ depends_on = [
+ data.google_service_account.service_account,
+ google_service_account.service_account
+ ]
+}
+
output "service_account" {
description = "Service account resource."
value = local.service_account
diff --git a/modules/iam-service-account/variables.tf b/modules/iam-service-account/variables.tf
index 93fc7fe177..a9f60bf239 100644
--- a/modules/iam-service-account/variables.tf
+++ b/modules/iam-service-account/variables.tf
@@ -39,36 +39,50 @@ variable "iam" {
nullable = false
}
+variable "iam_additive" {
+ description = "IAM additive bindings on the service account in {ROLE => [MEMBERS]} format."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
variable "iam_billing_roles" {
- description = "Billing account roles granted to the service account, by billing account id. Non-authoritative."
+ description = "Billing account roles granted to this service account, by billing account id. Non-authoritative."
type = map(list(string))
default = {}
nullable = false
}
variable "iam_folder_roles" {
- description = "Folder roles granted to the service account, by folder id. Non-authoritative."
+ description = "Folder roles granted to this service account, by folder id. Non-authoritative."
type = map(list(string))
default = {}
nullable = false
}
variable "iam_organization_roles" {
- description = "Organization roles granted to the service account, by organization id. Non-authoritative."
+ description = "Organization roles granted to this service account, by organization id. Non-authoritative."
type = map(list(string))
default = {}
nullable = false
}
variable "iam_project_roles" {
- description = "Project roles granted to the service account, by project id."
+ description = "Project roles granted to this service account, by project id."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "iam_sa_roles" {
+ description = "Service account roles granted to this service account, by service account name."
type = map(list(string))
default = {}
nullable = false
}
variable "iam_storage_roles" {
- description = "Storage roles granted to the service account, by bucket name."
+ description = "Storage roles granted to this service account, by bucket name."
type = map(list(string))
default = {}
nullable = false
@@ -83,6 +97,10 @@ variable "prefix" {
description = "Prefix applied to service account names."
type = string
default = null
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty, please use null instead."
+ }
}
variable "project_id" {
diff --git a/modules/iam-service-account/versions.tf b/modules/iam-service-account/versions.tf
index e72a78007a..90b632f6d4 100644
--- a/modules/iam-service-account/versions.tf
+++ b/modules/iam-service-account/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.1.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/iot-core/README.md b/modules/iot-core/README.md
deleted file mode 100644
index d28006db96..0000000000
--- a/modules/iot-core/README.md
+++ /dev/null
@@ -1,145 +0,0 @@
-# Google Cloud IoT Core Module
-
-This module sets up Cloud IoT Core Registry, registers IoT Devices and configures Pub/Sub topics required in Cloud IoT Core.
-
-To use this module, ensure the following APIs are enabled:
-* pubsub.googleapis.com
-* cloudiot.googleapis.com
-
-## Simple Example
-
-Basic example showing how to create an IoT Platform (IoT Core), connected to a set of given Pub/Sub topics and provision IoT devices.
-
-Devices certificates must exist before calling this module. You can generate these certificates using the following command
-
-```
-openssl req -x509 -newkey rsa:2048 -keyout rsa_private.pem -nodes -out rsa_cert.pem -subj "/CN=unused"
-```
-
-And then provision public certificate path, together with the rest of device configuration in a devices yaml file following the following format
-```yaml
-device_id: # id of your IoT Device
- is_blocked: # false to allow device connection with IoT Registry
- is_gateway: # true to indicate the device connecting acts as a gateway for other IoT Devices
- log_level: # device logs level
- certificate_file: # public certificate path, generated as explained in the previous step
- certificate_format: # Certificates format values are RSA_PEM, RSA_X509_PEM, ES256_PEM, and ES256_X509_PEM
-```
-
-Example Device config yaml configuration
-```yaml
-device_1:
- is_blocked: false
- is_gateway: false
- log_level: INFO
- certificate_file: device_certs/rsa_cert5.pem
- certificate_format: RSA_X509_PEM
-device_2:
- is_blocked: true
- is_gateway: false
- log_level: INFO
- certificate_file: device_certs/rsa_cert5.pem
- certificate_format: RSA_X509_PEM
-```
-
-```hcl
-module "iot-platform" {
- source = "./modules/iot-core"
- project_id = "my_project_id"
- region = "europe-west1"
- telemetry_pubsub_topic_id = "telemetry_topic_id"
- status_pubsub_topic_id = "status_topic_id"
- protocols = {
- http = false,
- mqtt = true
- }
- devices_config_directory = "./devices_config_folder"
-}
-# tftest:skip
-
-```
-
-Now, we can test sending telemetry messages from devices to our IoT Platform, for example using the MQTT demo client at https://github.com/googleapis/nodejs-iot/tree/main/samples/mqtt_example
-
-## Example with specific PubSub topics for custom MQTT topics
-
-If you need to match specific MQTT topics (eg, /temperature) into specific PubSub topics, you can use extra_telemetry_pubsub_topic_ids for that, as in the following example:
-
-```hcl
-module "iot-platform" {
- source = "./modules/iot-core"
- project_id = "my_project_id"
- region = "europe-west1"
- telemetry_pubsub_topic_id = "telemetry_topic_id"
- status_pubsub_topic_id = "status_topic_id"
- extra_telemetry_pubsub_topic_ids = {
- "temperature" = "temp_topic_id",
- "humidity" = "hum_topic_id"
- }
- protocols = {
- http = false,
- mqtt = true
- }
- devices_config_directory = "./devices_config_folder"
-}
-# tftest:skip
-
-```
-
-## Example integrated with Data Foundation Platform
-In this example, we will show how to extend the **[Data Foundations Platform](../../data-solutions/data-platform-foundations/)** to include IoT Platform as a new source of data.
-
-![Target architecture](./diagram_iot.png)
-
-1. First, we will setup Environment following instructions in **[Environment Setup](../../data-solutions/data-platform-foundations/01-environment/)** to setup projects and SAs required. Get output variable project_ids.landing as will be used later
-
-1. Second, execute instructions in **[Environment Setup](../../data-solutions/data-platform-foundations/02-resources/)** to provision PubSub, DataFlow, BQ,... Get variable landing-pubsub as will be used later to create IoT Registry
-
-1. Now it is time to provision IoT Platform. Modify landing-project-id and landing_pubsub_topic_id with output variables obtained before. Create device certificates as shown in the Simple Example and register them in devices.yaml file together with deviceids.
-
-```hcl
-module "iot-platform" {
- source = "./modules/iot-core"
- project_id = "landing-project-id"
- region = "europe-west1"
- telemetry_pubsub_topic_id = "landing_pubsub_topic_id"
- status_pubsub_topic_id = "status_pubsub_topic_id"
- protocols = {
- http = false,
- mqtt = true
- }
- devices_config_directory = "./devices_config_folder"
-}
-# tftest:skip
-```
-1. After that, we can setup the pipeline "PubSub to BigQuery" shown at **[Pipeline Setup](../../data-solutions/data-platform-foundations/03-pipeline/pubsub_to_bigquery.md)**
-
-1. Finally, instead of testing the pipeline by sending messages to PubSub, we can now test sending telemetry messages from simulated IoT devices to our IoT Platform, for example using the MQTT demo client at https://github.com/googleapis/nodejs-iot/tree/main/samples/mqtt_example . We shall edit the client script cloudiot_mqtt_example_nodejs.js to send messages following the pipeline message format, so they are processed by DataFlow job and inserted in the BigQuery table.
-```
-const payload = '{"name": "device4", "surname": "NA", "timestamp":"'+Math.floor(Date.now()/1000)+'"}';
-```
-
-Or even better, create a new BigQuery table with our IoT sensors data columns and modify the DataFlow job to push data to it.
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [devices_config_directory](variables.tf#L17) | Path to folder where devices configs are stored in yaml format. Folder may include subfolders with configuration files. Files suffix must be `.yaml`. | string
| ✓ | |
-| [project_id](variables.tf#L34) | Project were resources will be deployed | string
| ✓ | |
-| [region](variables.tf#L48) | Region were resources will be deployed | string
| ✓ | |
-| [status_pubsub_topic_id](variables.tf#L59) | pub sub topic for status messages (GCP-->Device) | string
| ✓ | |
-| [telemetry_pubsub_topic_id](variables.tf#L64) | pub sub topic for telemetry messages (Device-->GCP) | string
| ✓ | |
-| [extra_telemetry_pubsub_topic_ids](variables.tf#L22) | additional pubsub topics linked to adhoc MQTT topics (Device-->GCP) in the format MQTT_TOPIC: PUBSUB_TOPIC_ID | map(string)
| | {}
|
-| [log_level](variables.tf#L28) | IoT Registry Log level | string
| | "INFO"
|
-| [protocols](variables.tf#L39) | IoT protocols (HTTP / MQTT) activation | object({…})
| | { http = true, mqtt = true }
|
-| [registry_name](variables.tf#L53) | Name for the IoT Core Registry | string
| | "cloudiot-registry"
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [iot_registry](outputs.tf#L17) | Cloud IoT Core Registry | |
-
-
diff --git a/modules/iot-core/diagram.png b/modules/iot-core/diagram.png
deleted file mode 100644
index 7a3393212d..0000000000
Binary files a/modules/iot-core/diagram.png and /dev/null differ
diff --git a/modules/iot-core/diagram_iot.png b/modules/iot-core/diagram_iot.png
deleted file mode 100644
index 4c82259f2a..0000000000
Binary files a/modules/iot-core/diagram_iot.png and /dev/null differ
diff --git a/modules/iot-core/main.tf b/modules/iot-core/main.tf
deleted file mode 100644
index 6a8c3db151..0000000000
--- a/modules/iot-core/main.tf
+++ /dev/null
@@ -1,95 +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 {
- devices_config_files = [
- for config_file in fileset("${path.root}/${var.devices_config_directory}", "**/*.yaml") :
- "${path.root}/${var.devices_config_directory}/${config_file}"
- ]
-
- device_config = merge(
- [
- for config_file in local.devices_config_files :
- try(yamldecode(file(config_file)), {})
- ]...
- )
-}
-
-#---------------------------------------------------------
-# Create IoT Core Registry
-#---------------------------------------------------------
-
-resource "google_cloudiot_registry" "registry" {
-
- name = var.registry_name
- project = var.project_id
- region = var.region
-
- dynamic "event_notification_configs" {
- for_each = var.extra_telemetry_pubsub_topic_ids
- content {
- pubsub_topic_name = event_notification_configs.value
- subfolder_matches = event_notification_configs.key
- }
- }
-
- event_notification_configs {
- pubsub_topic_name = var.telemetry_pubsub_topic_id
- subfolder_matches = ""
- }
-
- state_notification_config = {
- pubsub_topic_name = var.status_pubsub_topic_id
- }
-
- mqtt_config = {
- mqtt_enabled_state = var.protocols.mqtt ? "MQTT_ENABLED" : "MQTT_DISABLED"
- }
-
- http_config = {
- http_enabled_state = var.protocols.http ? "HTTP_ENABLED" : "HTTP_DISABLED"
- }
-
- log_level = var.log_level
-
-}
-
-#---------------------------------------------------------
-# Create IoT Core Device
-# certificate created using: openssl req -x509 -newkey rsa:2048 -keyout rsa_private.pem -nodes -out rsa_cert.pem -subj "/CN=unused"
-#---------------------------------------------------------
-
-resource "google_cloudiot_device" "device" {
- for_each = local.device_config
- name = each.key
- registry = google_cloudiot_registry.registry.id
-
- credentials {
- public_key {
- format = try(each.value.certificate_format, null)
- key = try(file(each.value.certificate_file), null)
- }
- }
-
- blocked = try(each.value.is_blocked, null)
-
- log_level = try(each.value.log_level, null)
-
- gateway_config {
- gateway_type = try(each.value.is_gateway, null) ? "GATEWAY" : "NON_GATEWAY"
- }
-}
\ No newline at end of file
diff --git a/modules/iot-core/outputs.tf b/modules/iot-core/outputs.tf
deleted file mode 100644
index fcce598ada..0000000000
--- a/modules/iot-core/outputs.tf
+++ /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.
- */
-
-output "iot_registry" {
- description = "Cloud IoT Core Registry"
- value = google_cloudiot_registry.registry
-}
\ No newline at end of file
diff --git a/modules/iot-core/variables.tf b/modules/iot-core/variables.tf
deleted file mode 100644
index f192498268..0000000000
--- a/modules/iot-core/variables.tf
+++ /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.
- */
-
-variable "devices_config_directory" {
- description = "Path to folder where devices configs are stored in yaml format. Folder may include subfolders with configuration files. Files suffix must be `.yaml`."
- type = string
-}
-
-variable "extra_telemetry_pubsub_topic_ids" {
- description = "additional pubsub topics linked to adhoc MQTT topics (Device-->GCP) in the format MQTT_TOPIC: PUBSUB_TOPIC_ID"
- type = map(string)
- default = {}
-}
-
-variable "log_level" {
- description = "IoT Registry Log level"
- type = string
- default = "INFO"
-}
-
-variable "project_id" {
- description = "Project were resources will be deployed"
- type = string
-}
-
-variable "protocols" {
- description = "IoT protocols (HTTP / MQTT) activation"
- type = object({
- http = bool,
- mqtt = bool
- })
- default = { http = true, mqtt = true }
-}
-
-variable "region" {
- description = "Region were resources will be deployed"
- type = string
-}
-
-variable "registry_name" {
- description = "Name for the IoT Core Registry"
- type = string
- default = "cloudiot-registry"
-}
-
-variable "status_pubsub_topic_id" {
- description = "pub sub topic for status messages (GCP-->Device)"
- type = string
-}
-
-variable "telemetry_pubsub_topic_id" {
- description = "pub sub topic for telemetry messages (Device-->GCP)"
- type = string
-}
\ No newline at end of file
diff --git a/modules/kms/README.md b/modules/kms/README.md
index af1f60e961..3398a595d2 100644
--- a/modules/kms/README.md
+++ b/modules/kms/README.md
@@ -14,23 +14,23 @@ In this module **no lifecycle blocks are set on resources to prevent destroy**,
```hcl
module "kms" {
- source = "./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" }
keyring_create = false
keys = { key-a = null, key-b = null, key-c = null }
}
-# tftest skip
+# tftest skip (uses data sources)
```
### Keyring creation and crypto key rotation and IAM roles
```hcl
module "kms" {
- source = "./modules/kms"
+ source = "./fabric/modules/kms"
project_id = "my-project"
iam_additive = {
"roles/cloudkms.cryptoKeyEncrypterDecrypter" = [
@@ -63,8 +63,8 @@ module "kms" {
```hcl
module "kms" {
- source = "./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 290412687a..90b632f6d4 100644
--- a/modules/kms/versions.tf
+++ b/modules/kms/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/logging-bucket/README.md b/modules/logging-bucket/README.md
index c7281771ce..7af82ccb39 100644
--- a/modules/logging-bucket/README.md
+++ b/modules/logging-bucket/README.md
@@ -12,7 +12,7 @@ See also the `logging_sinks` argument within the [project](../project/), [folder
```hcl
module "bucket" {
- source = "./modules/logging-bucket"
+ source = "./fabric/modules/logging-bucket"
parent_type = "project"
parent = var.project_id
id = "mybucket"
@@ -25,13 +25,13 @@ module "bucket" {
```hcl
module "folder" {
- source = "./modules/folder"
+ source = "./fabric/modules/folder"
parent = "folders/657104291943"
name = "my folder"
}
module "bucket-default" {
- source = "./modules/logging-bucket"
+ source = "./fabric/modules/logging-bucket"
parent_type = "folder"
parent = module.folder.id
id = "_Default"
diff --git a/modules/logging-bucket/versions.tf b/modules/logging-bucket/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/logging-bucket/versions.tf
+++ b/modules/logging-bucket/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/naming-convention/README.md b/modules/naming-convention/README.md
deleted file mode 100644
index 356a922386..0000000000
--- a/modules/naming-convention/README.md
+++ /dev/null
@@ -1,89 +0,0 @@
-# Naming Convention Module
-
-This module allows defining a naming convention in a single place, and enforcing it by pre-creating resource names based on a set of tokens (environment, team name, etc.).
-
-It implements a fairly common naming convention with optional prefix or suffix, but is really meant to be forked, and modified to adapt to individual use cases: just replace the `environment` and `team` variables with whatever makes sense for you, and edit the few lines in `main.tf` marked by comments.
-
-The module also supports labels, generating sets of per-resource labels that combine the passed in tokens with optional resource-level labels.
-
-It's completely static, using no provider resources, so its outputs are safe to use where dynamic values are not supported, like in `for_each` statements.
-
-## Example
-
-In its default configuration, the module supports an option prefix and suffix, and two tokens: one for the environment, and one for the team name.
-
-```hcl
-module "names-org" {
- source = "./modules/naming-convention"
- prefix = "myco"
- environment = "dev"
- team = "cloud"
- resources = {
- bucket = ["tf-org", "tf-sec", "tf-log"]
- project = ["tf", "sec", "log"]
- }
- labels = {
- project = {
- tf = {scope = "global"}
- }
- }
-}
-
-module "project-tf" {
- source = "./modules/project"
- # myco-cloud-dev-tf
- name = module.names-org.names.project.tf
- # { environment = "dev", scope = "global", team = "cloud" }
- labels = module.names-org.labels.project.tf
-}
-```
-
-You can also enable resource type naming, useful with some legacy CMDB setups. When doing this, resource type names become part of the final resource names and are usually shorted (e.g. `prj` instead of `project`):
-
-```hcl
-module "names-org" {
- source = "./modules/naming-convention"
- prefix = "myco"
- environment = "dev"
- team = "cloud"
- resources = {
- bkt = ["tf-org", "tf-sec", "tf-log"]
- prj = ["tf", "sec", "log"]
- }
- labels = {
- prj = {
- tf = {scope = "global"}
- }
- }
- use_resource_prefixes = true
-}
-
-module "project-tf" {
- source = "./modules/project"
- # prj-myco-cloud-dev-tf
- name = module.names-org.names.prj.tf
-}
-```
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---:|:---:|:---:|
-| [environment](variables.tf#L17) | Environment abbreviation used in names and labels. | string
| ✓ | |
-| [resources](variables.tf#L34) | Short resource names by type. | map(list(string))
| ✓ | |
-| [team](variables.tf#L51) | Team name. | string
| ✓ | |
-| [labels](variables.tf#L22) | Per-resource labels. | map(map(map(string)))
| | {}
|
-| [prefix](variables.tf#L28) | Optional name prefix. | string
| | null
|
-| [separator_override](variables.tf#L39) | Optional separator override for specific resource types. | map(string)
| | {}
|
-| [suffix](variables.tf#L45) | Optional name suffix. | string
| | null
|
-| [use_resource_prefixes](variables.tf#L56) | Prefix names with the resource type. | bool
| | false
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| [labels](outputs.tf#L17) | Per resource labels. | |
-| [names](outputs.tf#L22) | Per resource names. | |
-
-
diff --git a/modules/naming-convention/main.tf b/modules/naming-convention/main.tf
deleted file mode 100644
index aa7d406b06..0000000000
--- a/modules/naming-convention/main.tf
+++ /dev/null
@@ -1,46 +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 {
- prefix = var.prefix == null ? "" : "${var.prefix}-"
- suffix = var.suffix == null ? "" : "-${var.suffix}"
- // merge common and per-resource labels
- labels = {
- for resource_type, resources in var.resources : resource_type => {
- for name in resources : name => merge(
- // change this line if you need different tokens
- { environment = var.environment, team = var.team },
- try(var.labels[resource_type][name], {})
- )
- }
- }
- // create per-resource names by assembling tokens
- names = {
- for resource_type, resources in var.resources : resource_type => {
- for name in resources : name => join(
- try(var.separator_override[resource_type], "-"),
- compact([
- var.use_resource_prefixes ? resource_type : "",
- var.prefix,
- // change these lines if you need different tokens
- var.team,
- var.environment,
- name,
- var.suffix
- ]))
- }
- }
-}
diff --git a/modules/naming-convention/outputs.tf b/modules/naming-convention/outputs.tf
deleted file mode 100644
index d96cfa985b..0000000000
--- a/modules/naming-convention/outputs.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.
- */
-
-output "labels" {
- description = "Per resource labels."
- value = local.labels
-}
-
-output "names" {
- description = "Per resource names."
- value = local.names
-}
diff --git a/modules/naming-convention/variables.tf b/modules/naming-convention/variables.tf
deleted file mode 100644
index 3ba8cd2cf6..0000000000
--- a/modules/naming-convention/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 "environment" {
- description = "Environment abbreviation used in names and labels."
- type = string
-}
-
-variable "labels" {
- description = "Per-resource labels."
- type = map(map(map(string)))
- default = {}
-}
-
-variable "prefix" {
- description = "Optional name prefix."
- type = string
- default = null
-}
-
-variable "resources" {
- description = "Short resource names by type."
- type = map(list(string))
-}
-
-variable "separator_override" {
- description = "Optional separator override for specific resource types."
- type = map(string)
- default = {}
-}
-
-variable "suffix" {
- description = "Optional name suffix."
- type = string
- default = null
-}
-
-variable "team" {
- description = "Team name."
- type = string
-}
-
-variable "use_resource_prefixes" {
- description = "Prefix names with the resource type."
- type = bool
- default = false
-}
diff --git a/modules/naming-convention/versions.tf b/modules/naming-convention/versions.tf
deleted file mode 100644
index 290412687a..0000000000
--- a/modules/naming-convention/versions.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
-#
-# 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.0.0"
- required_providers {
- google = {
- source = "hashicorp/google"
- version = ">= 4.0.0"
- }
- google-beta = {
- source = "hashicorp/google-beta"
- version = ">= 4.0.0"
- }
- }
-}
-
-
diff --git a/modules/net-address/README.md b/modules/net-address/README.md
index a41912a72c..23e947cd45 100644
--- a/modules/net-address/README.md
+++ b/modules/net-address/README.md
@@ -8,7 +8,7 @@ This module allows reserving Compute Engine external, global, and internal addre
```hcl
module "addresses" {
- source = "./modules/net-address"
+ source = "./fabric/modules/net-address"
project_id = var.project_id
external_addresses = {
nat-1 = var.region
@@ -23,26 +23,20 @@ module "addresses" {
```hcl
module "addresses" {
- source = "./modules/net-address"
+ source = "./fabric/modules/net-address"
project_id = var.project_id
internal_addresses = {
ilb-1 = {
+ purpose = "SHARED_LOADBALANCER_VIP"
region = var.region
subnetwork = var.subnet.self_link
}
ilb-2 = {
+ address = "10.0.0.2"
region = var.region
subnetwork = var.subnet.self_link
}
}
- # optional configuration
- internal_addresses_config = {
- ilb-1 = {
- address = null
- purpose = "SHARED_LOADBALANCER_VIP"
- tier = null
- }
- }
}
# tftest modules=1 resources=2
```
@@ -51,7 +45,7 @@ module "addresses" {
```hcl
module "addresses" {
- source = "./modules/net-address"
+ source = "./fabric/modules/net-address"
project_id = var.project_id
psa_addresses = {
cloudsql-mysql = {
@@ -68,15 +62,15 @@ module "addresses" {
```hcl
module "addresses" {
- source = "./modules/net-address"
+ source = "./fabric/modules/net-address"
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
}
}
@@ -89,13 +83,12 @@ module "addresses" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [project_id](variables.tf#L60) | Project where the addresses will be created. | string
| ✓ | |
+| [project_id](variables.tf#L54) | Project where the addresses will be created. | string
| ✓ | |
| [external_addresses](variables.tf#L17) | Map of external address regions, keyed by name. | map(string)
| | {}
|
| [global_addresses](variables.tf#L29) | List of global addresses to create. | list(string)
| | []
|
-| [internal_addresses](variables.tf#L35) | Map of internal addresses to create, keyed by name. | map(object({…}))
| | {}
|
-| [internal_addresses_config](variables.tf#L44) | Optional configuration for internal addresses, keyed by name. Unused options can be set to null. | map(object({…}))
| | {}
|
-| [psa_addresses](variables.tf#L65) | Map of internal addresses used for Private Service Access. | map(object({…}))
| | {}
|
-| [psc_addresses](variables.tf#L75) | Map of internal addresses used for Private Service Connect. | map(object({…}))
| | {}
|
+| [internal_addresses](variables.tf#L35) | Map of internal addresses to create, keyed by name. | map(object({…}))
| | {}
|
+| [psa_addresses](variables.tf#L59) | Map of internal addresses used for Private Service Access. | map(object({…}))
| | {}
|
+| [psc_addresses](variables.tf#L69) | Map of internal addresses used for Private Service Connect. | map(object({…}))
| | {}
|
## Outputs
diff --git a/modules/net-address/main.tf b/modules/net-address/main.tf
index caab92a0c0..8619f95b6e 100644
--- a/modules/net-address/main.tf
+++ b/modules/net-address/main.tf
@@ -39,10 +39,10 @@ resource "google_compute_address" "internal" {
address_type = "INTERNAL"
region = each.value.region
subnetwork = each.value.subnetwork
- address = try(var.internal_addresses_config[each.key].address, null)
- network_tier = try(var.internal_addresses_config[each.key].tier, null)
- purpose = try(var.internal_addresses_config[each.key].purpose, null)
- # labels = lookup(var.internal_address_labels, each.key, {})
+ address = each.value.address
+ network_tier = each.value.tier
+ purpose = each.value.purpose
+ labels = coalesce(each.value.labels, {})
}
resource "google_compute_global_address" "psc" {
diff --git a/modules/net-address/variables.tf b/modules/net-address/variables.tf
index bb3043dc8a..35093e83ac 100644
--- a/modules/net-address/variables.tf
+++ b/modules/net-address/variables.tf
@@ -37,16 +37,10 @@ variable "internal_addresses" {
type = map(object({
region = string
subnetwork = string
- }))
- default = {}
-}
-
-variable "internal_addresses_config" {
- description = "Optional configuration for internal addresses, keyed by name. Unused options can be set to null."
- type = map(object({
- address = string
- purpose = string
- tier = string
+ address = optional(string)
+ labels = optional(map(string))
+ purpose = optional(string)
+ tier = optional(string)
}))
default = {}
}
diff --git a/modules/net-address/versions.tf b/modules/net-address/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/net-address/versions.tf
+++ b/modules/net-address/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/net-cloudnat/README.md b/modules/net-cloudnat/README.md
index bfcb208f94..435ef7f3da 100644
--- a/modules/net-cloudnat/README.md
+++ b/modules/net-cloudnat/README.md
@@ -6,7 +6,7 @@ Simple Cloud NAT management, with optional router creation.
```hcl
module "nat" {
- source = "./modules/net-cloudnat"
+ source = "./fabric/modules/net-cloudnat"
project_id = "my-project"
region = "europe-west1"
name = "default"
diff --git a/modules/net-cloudnat/versions.tf b/modules/net-cloudnat/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/net-cloudnat/versions.tf
+++ b/modules/net-cloudnat/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/net-glb/.gitignore b/modules/net-glb/.gitignore
new file mode 100644
index 0000000000..a00202d514
--- /dev/null
+++ b/modules/net-glb/.gitignore
@@ -0,0 +1,3 @@
+*backup
+*tfstate
+tfvars
diff --git a/modules/net-glb/README.md b/modules/net-glb/README.md
index e59fee2a9c..4b6d243508 100644
--- a/modules/net-glb/README.md
+++ b/modules/net-glb/README.md
@@ -1,321 +1,371 @@
-# External Global (HTTP/S) Load Balancer Module
+# Global HTTP/S Classic Load Balancer Module
-The module allows managing External Global HTTP/HTTPS Load Balancers (XGLB), integrating the global forwarding rule, the url-map, the backends (supporting buckets and groups), and optional health checks and SSL certificates (both managed and unmanaged). It's designed to be a simple match for the [`gcs`](../gcs) and the [`compute-vm`](../compute-vm) modules, which can be used to manage GCS buckets, instance templates and instance groups.
+This module allows managing Global HTTP/HTTPS Classic Load Balancers (GLBs). It's designed to expose the full configuration of the underlying resources, and to facilitate common usage patterns by providing sensible defaults, and optionally managing prerequisite resources like health checks, instance groups, etc.
-## Examples
+Due to the complexity of the underlying resources, changes to the configuration that involve recreation of resources are best applied in stages, starting by disabling the configuration in the urlmap that references the resources that neeed recreation, then doing the same for the backend service, etc.
-### GCS Bucket Minimal Example
+## Examples
-This is a minimal example, which creates a global HTTP load balancer, pointing the path `/` to an existing GCS bucket called `my_test_bucket`.
+- [Minimal HTTP Example](#minimal-http-example)
+- [Minimal HTTPS Examples](#minimal-https-examples)
+- [Health Checks](#health-checks)
+- [Backend Types and Management](#backend-types-and-management)
+ - [Instance Groups](#instance-groups)
+ - [Storage Buckets](#storage-buckets)
+ - [Network Endpoint Groups](#network-endpoint-groups-negs)
+ - [Zonal NEGs](#zonal-neg-creation)
+ - [Hybrid NEGs](#hybrid-neg-creation)
+ - [Internet NEGs](#internet-neg-creation)
+ - [Serverless NEGs](#serverless-neg-creation)
+- [URL Map](#url-map)
+- [SSL Certificates](#ssl-certificates)
+- [Complex Example](#complex-example)
+
+### Minimal HTTP Example
+
+An HTTP load balancer with a backend service pointing to a GCE instance group:
```hcl
-module "glb" {
- source = "./modules/net-glb"
- name = "glb-test"
- project_id = var.project_id
+module "glb-0" {
+ source = "./fabric/modules/net-glb"
+ project_id = "myprj"
+ name = "glb-test-0"
+ backend_service_configs = {
+ default = {
+ backends = [
+ { backend = "projects/myprj/zones/europe-west8-b/instanceGroups/myig-b" },
+ { backend = "projects/myprj/zones/europe-west8-c/instanceGroups/myig-c" },
+ ]
+ }
+ }
+}
+# tftest modules=1 resources=5
+```
+
+### Minimal HTTPS examples
+
+#### HTTP backends
- backend_services_config = {
- my-bucket-backend = {
- bucket_config = {
- bucket_name = "my_test_bucket"
- options = null
+An HTTPS load balancer needs a certificate and backends can be HTTP or HTTPS. THis is an example With HTTP backends and a managed certificate:
+
+```hcl
+module "glb-0" {
+ source = "./fabric/modules/net-glb"
+ project_id = "myprj"
+ name = "glb-test-0"
+ backend_service_configs = {
+ default = {
+ backends = [
+ { backend = "projects/myprj/zones/europe-west8-b/instanceGroups/myig-b" },
+ { backend = "projects/myprj/zones/europe-west8-c/instanceGroups/myig-c" },
+ ]
+ protocol = "HTTP"
+ }
+ }
+ protocol = "HTTPS"
+ ssl_certificates = {
+ managed_configs = {
+ default = {
+ domains = ["glb-test-0.example.org"]
}
- group_config = null
- enable_cdn = false
- cdn_config = null
}
}
}
-# tftest modules=1 resources=4
+# tftest modules=1 resources=6
```
-### Group Backend Service Minimal Example
+#### HTTPS backends
-A very similar coniguration also applies to GCE instance groups:
+For HTTPS backends the backend service protocol needs to be set to `HTTPS`. The port name if omitted is inferred from the protocol, in this case it is set internally to `https`. The health check also needs to be set to https. This is a complete example:
```hcl
-module "glb" {
- source = "./modules/net-glb"
- name = "glb-test"
- project_id = var.project_id
-
- backend_services_config = {
- my-group-backend = {
- bucket_config = null
- enable_cdn = false
- cdn_config = null
- group_config = {
- backends = [
- {
- group = "my_test_group"
- options = null
- }
- ],
- health_checks = []
- log_config = null
- options = null
+module "glb-0" {
+ source = "./fabric/modules/net-glb"
+ project_id = "myprj"
+ name = "glb-test-0"
+ backend_service_configs = {
+ default = {
+ backends = [
+ { backend = "projects/myprj/zones/europe-west8-b/instanceGroups/myig-b" },
+ { backend = "projects/myprj/zones/europe-west8-c/instanceGroups/myig-c" },
+ ]
+ protocol = "HTTPS"
+ }
+ }
+ health_check_configs = {
+ default = {
+ https = {
+ port_specification = "USE_SERVING_PORT"
}
}
}
+ protocol = "HTTPS"
+ ssl_certificates = {
+ managed_configs = {
+ default = {
+ domains = ["glb-test-0.example.org"]
+ }
+ }
+ }
+}
+# tftest modules=1 resources=6
+```
+
+### Classic vs Non-classic
+
+The module uses a classic Global Load Balancer by default. To use the non-classic version set the `use_classic_version` variable to `false` as in the following example, note that the module is not enforcing feature sets between the two versions:
+
+```hcl
+module "glb-0" {
+ source = "./fabric/modules/net-glb"
+ project_id = "myprj"
+ name = "glb-test-0"
+ use_classic_version = false
+ backend_service_configs = {
+ default = {
+ backends = [
+ { backend = "projects/myprj/zones/europe-west8-b/instanceGroups/myig-b" },
+ { backend = "projects/myprj/zones/europe-west8-c/instanceGroups/myig-c" },
+ ]
+ }
+ }
}
# tftest modules=1 resources=5
```
-### Health Checks For Group Backend Services
+### Health Checks
-Group backend services support health checks.
-If no health checks are specified, a default HTTP health check is created and associated to each group backend service. The default health check configuration can be modified through the `health_checks_config_defaults` variable.
+You can leverage externally defined health checks for backend services, or have the module create them for you.
-Alternatively, one or more health checks can be either contextually created or attached, if existing. If the id of the health checks specified is equal to one of the keys of the `health_checks_config` variable, the health check is contextually created; otherwise, the health check id is used as is, assuming an health check alredy exists.
+By default a simple HTTP health check named `default` is created and used in backend services. If you need to override the default, simply define your own health check using the same key (`default`). For more complex configurations you can define your own health checks and reference them via keys in the backend service configurations.
-For example, to contextually create a health check and attach it to the backend service:
+Health checks created by this module are controlled via the `health_check_configs` variable, which behaves in a similar way to other LB modules in this repository. This is an example that overrides the default health check configuration using a TCP health check:
```hcl
-module "glb" {
- source = "./modules/net-glb"
- name = "glb-test"
+module "glb-0" {
+ source = "./fabric/modules/net-glb"
project_id = var.project_id
-
- backend_services_config = {
- my-group-backend = {
- bucket_config = null
- enable_cdn = false
- cdn_config = null
- group_config = {
- backends = [
- {
- group = "my_test_group"
- options = null
- }
- ],
- health_checks = ["hc_1"]
- log_config = null
- options = null
- }
+ name = "glb-test-0"
+ backend_service_configs = {
+ default = {
+ backends = [{
+ backend = "projects/myprj/zones/europe-west1-a/instanceGroups/my-ig"
+ }]
+ # no need to reference the hc explicitly when using the `default` key
+ # health_checks = ["default"]
}
}
-
- health_checks_config = {
- hc_1 = {
- type = "http"
- logging = true
- options = {
- timeout_sec = 40
- }
- check = {
- port_specification = "USE_SERVING_PORT"
- }
+ health_check_configs = {
+ default = {
+ tcp = { port = 80 }
}
}
}
# tftest modules=1 resources=5
```
-### Serverless Backends
-
-Serverless backends can also be used, as shown in the example below.
+To leverage existing health checks without having the module create them, simply pass their self links to backend services and set the `health_check_configs` variable to an empty map:
```hcl
-module "glb" {
- source = "./modules/net-glb"
- name = "glb-test"
+module "glb-0" {
+ source = "./fabric/modules/net-glb"
project_id = var.project_id
+ name = "glb-test-0"
+ backend_service_configs = {
+ default = {
+ backends = [{
+ backend = "projects/myprj/zones/europe-west1-a/instanceGroups/my-ig"
+ }]
+ health_checks = ["projects/myprj/global/healthChecks/custom"]
+ }
+ }
+ health_check_configs = {}
+}
+# tftest modules=1 resources=4
+```
- # This is important as serverless backends require no HCs
- health_checks_config_defaults = null
+### Backend Types and Management
- backend_services_config = {
- my-serverless-backend = {
- bucket_config = null
- enable_cdn = false
- cdn_config = null
- group_config = {
- backends = [
- {
- group = google_compute_region_network_endpoint_group.serverless-neg.id
- options = null
- }
- ],
- health_checks = []
- log_config = null
- options = null
- }
+#### Instance Groups
+
+The module can optionally create unmanaged instance groups, which can then be referred to in backends via their key. THis is the simple HTTP example above but with instance group creation managed by the module:
+
+```hcl
+module "glb-0" {
+ source = "./fabric/modules/net-glb"
+ project_id = "myprj"
+ name = "glb-test-0"
+ backend_service_configs = {
+ default = {
+ backends = [
+ { backend = "default-b" }
+ ]
+ }
+ }
+ group_configs = {
+ default-b = {
+ zone = "europe-west8-b"
+ instances = [
+ "projects/myprj/zones/europe-west8-b/instances/vm-a"
+ ]
+ named_ports = { http = 80 }
}
}
}
+# tftest modules=1 resources=6
+```
-resource "google_compute_region_network_endpoint_group" "serverless-neg" {
- name = "my-serverless-neg"
- project = var.project_id
- region = "europe-west1"
- network_endpoint_type = "SERVERLESS"
+#### Storage Buckets
- cloud_run {
- service = "my-cloud-run-service"
+GCS bucket backends can also be managed and used in this module in a similar way to regular backend services.Multiple GCS bucket backends can be defined and referenced in URL maps by their keys (or self links if defined externally) together with regular backend services, [an example is provided later in this document](#complex-example). This is a simple example that defines a GCS backend as the default for the URL map:
+
+```hcl
+module "glb-0" {
+ source = "./fabric/modules/net-glb"
+ project_id = "myprj"
+ name = "glb-test-0"
+ backend_buckets_config = {
+ default = {
+ bucket_name = "tf-playground-svpc-gce-public"
+ }
}
+ # with a single GCS backend the implied default health check is not needed
+ health_check_configs = {}
}
# tftest modules=1 resources=4
```
-### Mixing Backends
+#### Network Endpoint Groups (NEGs)
-Backends can be multiple, group and bucket backends can be mixed and group backends support multiple groups.
+Supported Network Endpoint Groups (NEGs) can also be used as backends. Similarly to groups, you can pass a self link for existing NEGs or have the module manage them for you. A simple example using an existing zonal NEG:
```hcl
-module "glb" {
- source = "./modules/net-glb"
- name = "glb-test"
- project_id = var.project_id
+module "glb-0" {
+ source = "./fabric/modules/net-glb"
+ project_id = "myprj"
+ name = "glb-test-0"
+ backend_service_configs = {
+ default = {
+ backends = [
+ {
+ backend = "projects/myprj/zones/europe-west8-b/networkEndpointGroups/myneg-b"
+ balancing_mode = "RATE"
+ max_rate = { per_endpoint = 10 }
+ }
+ ]
+ }
+ }
+}
+# tftest modules=1 resources=5
+```
- backend_services_config = {
- my-group-backend = {
- bucket_config = null
- enable_cdn = false
- cdn_config = null
- group_config = {
- backends = [
- {
- group = "my_test_group"
- options = null
- }
- ],
- health_checks = []
- log_config = null
- options = null
- }
- },
- another-group-backend = {
- bucket_config = null
- enable_cdn = false
- cdn_config = null
- group_config = {
- backends = [
- {
- group = "my_other_test_group"
- options = null
+#### Zonal NEG creation
+
+This example shows how to create and manage zonal NEGs using GCE VMs as endpoints:
+
+```hcl
+module "glb-0" {
+ source = "./fabric/modules/net-glb"
+ project_id = "myprj"
+ name = "glb-test-0"
+ backend_service_configs = {
+ default = {
+ backends = [
+ {
+ backend = "neg-0"
+ balancing_mode = "RATE"
+ max_rate = { per_endpoint = 10 }
+ }
+ ]
+ }
+ }
+ neg_configs = {
+ neg-0 = {
+ gce = {
+ network = "projects/myprj-host/global/networks/svpc"
+ subnetwork = "projects/myprj-host/regions/europe-west8/subnetworks/gce"
+ zone = "europe-west8-b"
+ endpoints = {
+ e-0 = {
+ instance = "myinstance-b-0"
+ ip_address = "10.24.32.25"
+ port = 80
}
- ],
- health_checks = []
- log_config = null
- options = null
+ }
}
- },
- a-bucket-backend = {
- bucket_config = {
- bucket_name = "my_test_bucket"
- options = null
- }
- group_config = null
- enable_cdn = false
- cdn_config = null
}
}
}
# tftest modules=1 resources=7
```
-### Url-map
-
-The url-map can be customized with lots of different configurations. This includes leveraging multiple backends in different parts of the configuration.
-Given its complexity, it's left to the user passing the right data structure.
-
-For simplicity, *if no configurations are given* the first backend service defined (in alphabetical order, with priority to bucket backend services, if any) is used as the *default_service*, thus answering to the root (*/*) path.
+#### Hybrid NEG creation
-Backend services can be specified as needed in the url-map configuration, referencing the id used to declare them in the backend services map. If a corresponding backend service is found, their object id is automatically used; otherwise, it is assumed that the string passed is the id of an already existing backend and it is given to the provider as it was passed.
-
-In this example, we're using one backend service as the default backend
+This example shows how to create and manage hybrid NEGs:
```hcl
-module "glb" {
- source = "./modules/net-glb"
- name = "glb-test"
- project_id = var.project_id
-
- url_map_config = {
- default_service = "my-group-backend"
- default_route_action = null
- default_url_redirect = null
- tests = null
- header_action = null
- host_rules = []
- path_matchers = [
- {
- name = "my-example-page"
- path_rules = [
- {
- paths = ["/my-example-page"]
- service = "another-group-backend"
- }
- ]
- }
- ]
+module "glb-0" {
+ source = "./fabric/modules/net-glb"
+ project_id = "myprj"
+ name = "glb-test-0"
+ backend_service_configs = {
+ default = {
+ backends = [
+ {
+ backend = "neg-0"
+ balancing_mode = "RATE"
+ max_rate = { per_endpoint = 10 }
+ }
+ ]
+ }
}
-
- backend_services_config = {
- my-group-backend = {
- bucket_config = null
- enable_cdn = false
- cdn_config = null
- group_config = {
- backends = [
- {
- group = "my_test_group"
- options = null
- }
- ],
- health_checks = []
- log_config = null
- options = null
- }
- },
- my-example-page = {
- bucket_config = null
- enable_cdn = false
- cdn_config = null
- group_config = {
- backends = [
- {
- group = "my_other_test_group"
- options = null
+ neg_configs = {
+ neg-0 = {
+ hybrid = {
+ network = "projects/myprj-host/global/networks/svpc"
+ zone = "europe-west8-b"
+ endpoints = {
+ e-0 = {
+ ip_address = "10.0.0.10"
+ port = 80
}
- ],
- health_checks = []
- log_config = null
- options = null
+ }
}
}
}
}
-# tftest modules=1 resources=6
+# tftest modules=1 resources=7
```
-### Reserve a static IP address
+#### Internet NEG creation
-Optionally, a static IP address can be reserved:
+This example shows how to create and manage internet NEGs:
```hcl
-module "glb" {
- source = "./modules/net-glb"
- name = "glb-test"
- project_id = var.project_id
-
- reserve_ip_address = true
-
- backend_services_config = {
- my-group-backend = {
- bucket_config = null
- enable_cdn = false
- cdn_config = null
- group_config = {
- backends = [
- {
- group = "my_test_group"
- options = null
+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 = []
+ }
+ }
+ # with a single internet NEG the implied default health check is not needed
+ health_check_configs = {}
+ neg_configs = {
+ neg-0 = {
+ internet = {
+ use_fqdn = true
+ endpoints = {
+ e-0 = {
+ destination = "www.example.org"
+ port = 80
}
- ],
- health_checks = []
- log_config = null
- options = null
+ }
}
}
}
@@ -323,174 +373,299 @@ module "glb" {
# tftest modules=1 resources=6
```
-### HTTPS And SSL Certificates
-
-HTTPS is disabled by default but it can be optionally enabled.
-The module supports both managed and unmanaged certificates, and they can be either created contextually with other resources or attached, if already existing.
+#### Private Service Connect NEG creation
-If no `ssl_certificates_config` variable is specified, a managed certificate for the domain *example.com* is automatically created.
+The module supports managing PSC NEGs if the non-classic version of the load balancer is used:
```hcl
-module "glb" {
- source = "./modules/net-glb"
- name = "glb-test"
- project_id = var.project_id
-
- https = true
-
- backend_services_config = {
- my-group-backend = {
- bucket_config = null
- enable_cdn = false
- cdn_config = null
- group_config = {
- backends = [
- {
- group = "my_test_group"
- options = null
- }
- ],
- health_checks = []
- log_config = null
- options = null
+module "glb-0" {
+ source = "./fabric/modules/net-glb"
+ project_id = "myprj"
+ name = "glb-test-0"
+ use_classic_version = false
+ backend_service_configs = {
+ default = {
+ backends = [
+ { backend = "neg-0" }
+ ]
+ health_checks = []
+ }
+ }
+ # with a single PSC NEG the implied default health check is not needed
+ health_check_configs = {}
+ neg_configs = {
+ neg-0 = {
+ psc = {
+ region = "europe-west8"
+ target_service = "europe-west8-cloudkms.googleapis.com"
}
}
}
}
-# tftest modules=1 resources=6
+# tftest modules=1 resources=5
```
-Otherwise, SSL certificates can be explicitely defined. In this case, they'll need to be referenced from the `target_proxy_https_config.ssl_certificates` variable.
+#### Serverless NEG creation
-If the ids specified in the `target_proxy_https_config` variable are not found in the `ssl_certificates_config` map, they are used as is, assuming the ssl certificates already exist.
+The module supports managing Serverless NEGs for Cloud Run and Cloud Function. This is an example of a Cloud Run NEG:
```hcl
-module "glb" {
- source = "./modules/net-glb"
- name = "glb-test"
- project_id = var.project_id
-
- https = true
-
- ssl_certificates_config = {
- my-domain = {
- domains = [
- "my-domain.com"
- ],
- unmanaged_config = null
+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 = []
}
}
-
- target_proxy_https_config = {
- ssl_certificates = [
- "my-domain",
- "an-existing-cert"
- ]
- }
-
- backend_services_config = {
- my-group-backend = {
- bucket_config = null
- enable_cdn = false
- cdn_config = null
- group_config = {
- backends = [
- {
- group = "my_test_group"
- options = null
- }
- ]
- health_checks = []
- log_config = null
- options = null
+ # 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"
+ }
}
}
}
}
-# tftest modules=1 resources=6
+# tftest modules=1 resources=5
```
-Using unamanged certificates is also possible. Here is an example:
+### URL Map
-```hcl
-module "glb" {
- source = "./modules/net-glb"
- name = "glb-test"
- project_id = var.project_id
+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.
- https = true
+The default URL map configuration sets the `default` backend service as the default service for the load balancer as a convenience. Just override the `urlmap_config` variable to change the default behaviour:
- ssl_certificates_config = {
- my-domain = {
- domains = [
- "my-domain.com"
- ],
- unmanaged_config = {
- tls_private_key = nonsensitive(tls_private_key.self_signed_key.private_key_pem)
- tls_self_signed_cert = nonsensitive(tls_self_signed_cert.self_signed_cert.cert_pem)
- }
+```hcl
+module "glb-0" {
+ source = "./fabric/modules/net-glb"
+ project_id = "myprj"
+ name = "glb-test-0"
+ backend_service_configs = {
+ default = {
+ backends = [{
+ backend = "projects/myprj/zones/europe-west8-b/instanceGroups/ig-0"
+ }]
+ }
+ other = {
+ backends = [{
+ backend = "projects/myprj/zones/europe-west8-c/instanceGroups/ig-1"
+ }]
}
}
-
- target_proxy_https_config = {
- ssl_certificates = [
- "my-domain"
- ]
- }
-
- backend_services_config = {
- my-group-backend = {
- bucket_config = null
- enable_cdn = false
- cdn_config = null
- group_config = {
- backends = [
- {
- group = "my_test_group"
- options = null
- }
- ],
- health_checks = []
- log_config = null
- options = null
+ urlmap_config = {
+ default_service = "default"
+ host_rules = [{
+ hosts = ["*"]
+ path_matcher = "pathmap"
+ }]
+ path_matchers = {
+ pathmap = {
+ default_service = "default"
+ path_rules = [{
+ paths = ["/other", "/other/*"]
+ service = "other"
+ }]
}
}
}
}
-resource "tls_private_key" "self_signed_key" {
+# tftest modules=1 resources=6
+```
+
+### SSL Certificates
+
+The module also allows managing managed and self-managed SSL certificates via the `ssl_certificates` variable. Any certificate defined there will be added to the HTTPS proxy resource.
+
+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 = 2048
+ rsa_bits = 4096
}
-resource "tls_self_signed_cert" "self_signed_cert" {
- key_algorithm = tls_private_key.self_signed_key.algorithm
- private_key_pem = tls_private_key.self_signed_key.private_key_pem
- validity_period_hours = 12
- early_renewal_hours = 3
- dns_names = ["example.com"]
+resource "tls_self_signed_cert" "default" {
+ private_key_pem = tls_private_key.default.private_key_pem
+ subject {
+ common_name = "example.com"
+ organization = "ACME Examples, Inc"
+ }
+ validity_period_hours = 720
allowed_uses = [
"key_encipherment",
"digital_signature",
- "server_auth"
+ "server_auth",
]
- subject {
- common_name = "example.com"
- organization = "My Test Org"
+}
+
+module "glb-0" {
+ source = "./fabric/modules/net-glb"
+ project_id = "myprj"
+ name = "glb-test-0"
+ backend_service_configs = {
+ default = {
+ backends = [
+ { backend = "projects/myprj/zones/europe-west8-b/instanceGroups/myig-b" },
+ { backend = "projects/myprj/zones/europe-west8-c/instanceGroups/myig-c" },
+ ]
+ protocol = "HTTP"
+ }
+ }
+ protocol = "HTTPS"
+ ssl_certificates = {
+ create_configs = {
+ default = {
+ # certificate and key could also be read via file() from external files
+ certificate = tls_self_signed_cert.default.cert_pem
+ private_key = tls_private_key.default.private_key_pem
+ }
+ }
}
}
-# tftest modules=1 resources=6
+# tftest modules=1 resources=8
```
-## Components And Files Mapping
+### Complex example
-An External Global Load Balancer is made of multiple components, that change depending on the configurations. Sometimes, it may be tricky to understand what they are, and how they relate to each other. Following, we provide a very brief overview to become more familiar with them.
+This example mixes group and NEG backends, and shows how to set HTTPS for specific backends.
-- The global load balancer [forwarding rule](global_forwarding_rule.tf) binds a frontend public Virtual IP (VIP) to an HTTP(S) [target proxy](target_proxy.tf).
-- If the target proxy is HTTPS, it requires one or more managed or unmanaged [SSL certificates](ssl_certificates.tf).
-- Target proxies leverage [url-maps](url_map.tf): set of L7 rules, which create a mapping between specific hostnames, URIs (and more) to one or more [backends services](backend_services.tf).
-- [Backend services](backend_services.tf) can either link to a bucket or one or multiple groups, which can be GCE instance groups or NEGs. It is assumed in this module that buckets and groups are previously created through other modules, and passed in as input variables.
-- Backend services support one or more [health checks](health_checks.tf), used to verify that the backend is indeed healthy, so that traffic can be forwarded to it. Health checks currently supported in this module are HTTP, HTTPS, HTTP2, SSL, TCP.
+```hcl
+module "glb-0" {
+ source = "./fabric/modules/net-glb"
+ project_id = "myprj"
+ name = "glb-test-0"
+ backend_buckets_config = {
+ gcs-0 = {
+ bucket_name = "my-bucket"
+ }
+ }
+ backend_service_configs = {
+ default = {
+ backends = [
+ { backend = "ew8-b" },
+ { backend = "ew8-c" },
+ ]
+ }
+ neg-gce-0 = {
+ backends = [{
+ balancing_mode = "RATE"
+ backend = "neg-ew8-c"
+ max_rate = { per_endpoint = 10 }
+ }]
+ }
+ neg-hybrid-0 = {
+ backends = [{
+ backend = "neg-hello"
+ }]
+ health_checks = ["neg"]
+ protocol = "HTTPS"
+ }
+ }
+ group_configs = {
+ ew8-b = {
+ zone = "europe-west8-b"
+ instances = [
+ "projects/prj-gce/zones/europe-west8-b/instances/nginx-ew8-b"
+ ]
+ named_ports = { http = 80 }
+ }
+ ew8-c = {
+ zone = "europe-west8-c"
+ instances = [
+ "projects/prj-gce/zones/europe-west8-c/instances/nginx-ew8-c"
+ ]
+ named_ports = { http = 80 }
+ }
+ }
+ health_check_configs = {
+ default = {
+ http = {
+ port = 80
+ }
+ }
+ neg = {
+ https = {
+ host = "hello.example.com"
+ port = 443
+ }
+ }
+ }
+ neg_configs = {
+ neg-ew8-c = {
+ gce = {
+ network = "projects/myprj-host/global/networks/svpc"
+ subnetwork = "projects/myprj-host/regions/europe-west8/subnetworks/gce"
+ 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 = {
+ e-0 = {
+ ip_address = "192.168.0.3"
+ port = 443
+ }
+ }
+ }
+ }
+ }
+ urlmap_config = {
+ default_service = "default"
+ host_rules = [
+ {
+ hosts = ["*"]
+ path_matcher = "gce"
+ },
+ {
+ hosts = ["hello.example.com"]
+ path_matcher = "hello"
+ },
+ {
+ hosts = ["static.example.com"]
+ path_matcher = "static"
+ }
+ ]
+ path_matchers = {
+ gce = {
+ default_service = "default"
+ path_rules = [
+ {
+ paths = ["/gce-neg", "/gce-neg/*"]
+ service = "neg-gce-0"
+ }
+ ]
+ }
+ hello = {
+ default_service = "neg-hybrid-0"
+ }
+ static = {
+ default_service = "gcs-0"
+ }
+ }
+ }
+}
+# tftest modules=1 resources=15
+```
@@ -499,13 +674,16 @@ An External Global Load Balancer is made of multiple components, that change dep
| name | description | resources |
|---|---|---|
-| [backend-services.tf](./backend-services.tf) | Bucket and group backend services. | google_compute_backend_bucket
· google_compute_backend_service
|
-| [global-forwarding-rule.tf](./global-forwarding-rule.tf) | Global address and forwarding rule. | google_compute_global_address
· google_compute_global_forwarding_rule
|
-| [health-checks.tf](./health-checks.tf) | Health checks. | google_compute_health_check
|
+| [backend-service.tf](./backend-service.tf) | Backend service resources. | google_compute_backend_service
|
+| [backends.tf](./backends.tf) | Backend groups and backend buckets resources. | google_compute_backend_bucket
· google_compute_instance_group
|
+| [health-check.tf](./health-check.tf) | Health check resource. | google_compute_health_check
|
+| [main.tf](./main.tf) | Module-level locals and resources. | google_compute_global_forwarding_rule
· google_compute_managed_ssl_certificate
· google_compute_ssl_certificate
· google_compute_target_http_proxy
· google_compute_target_https_proxy
|
+| [negs.tf](./negs.tf) | NEG resources. | google_compute_global_network_endpoint
· google_compute_global_network_endpoint_group
· google_compute_network_endpoint
· google_compute_network_endpoint_group
· google_compute_region_network_endpoint_group
|
| [outputs.tf](./outputs.tf) | Module outputs. | |
-| [ssl-certificates.tf](./ssl-certificates.tf) | SSL certificates. | google_compute_managed_ssl_certificate
· google_compute_ssl_certificate
|
-| [target-proxy.tf](./target-proxy.tf) | HTTP and HTTPS target proxies. | google_compute_target_http_proxy
· google_compute_target_https_proxy
|
-| [url-map.tf](./url-map.tf) | URL maps. | google_compute_url_map
|
+| [urlmap.tf](./urlmap.tf) | URL map resources. | google_compute_url_map
|
+| [variables-backend-service.tf](./variables-backend-service.tf) | Backend services variables. | |
+| [variables-health-check.tf](./variables-health-check.tf) | Health check variable. | |
+| [variables-urlmap.tf](./variables-urlmap.tf) | URLmap variable. | |
| [variables.tf](./variables.tf) | Module variables. | |
| [versions.tf](./versions.tf) | Version pins. | |
@@ -513,29 +691,32 @@ An External Global Load Balancer is made of multiple components, that change dep
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [name](variables.tf#L17) | Load balancer name. | string
| ✓ | |
-| [project_id](variables.tf#L22) | Project id. | string
| ✓ | |
-| [backend_services_config](variables.tf#L56) | The backends services configuration. | map(object({…}))
| | {}
|
-| [global_forwarding_rule_config](variables.tf#L202) | Global forwarding rule configurations. | object({…})
| | {…}
|
-| [health_checks_config](variables.tf#L45) | Custom health checks configuration. | map(object({…}))
| | {}
|
-| [health_checks_config_defaults](variables.tf#L27) | Auto-created health check default configuration. | object({…})
| | {…}
|
-| [https](variables.tf#L220) | Whether to enable HTTPS. | bool
| | false
|
-| [reserve_ip_address](variables.tf#L226) | Whether to reserve a static global IP address. | bool
| | false
|
-| [ssl_certificates_config](variables.tf#L165) | The SSL certificate configuration. | map(object({…}))
| | {}
|
-| [ssl_certificates_config_defaults](variables.tf#L178) | The SSL certificate default configuration. | object({…})
| | {…}
|
-| [target_proxy_https_config](variables.tf#L194) | The HTTPS target proxy configuration. | object({…})
| | null
|
-| [url_map_config](variables.tf#L151) | The url-map configuration. | object({…})
| | null
|
+| [name](variables.tf#L91) | Load balancer name. | string
| ✓ | |
+| [project_id](variables.tf#L193) | Project id. | string
| ✓ | |
+| [address](variables.tf#L17) | Optional IP address used for the forwarding rule. | string
| | null
|
+| [backend_buckets_config](variables.tf#L23) | Backend buckets configuration. | map(object({…}))
| | {}
|
+| [backend_service_configs](variables-backend-service.tf#L19) | Backend service level configuration. | map(object({…}))
| | {}
|
+| [description](variables.tf#L56) | Optional description used for resources. | string
| | "Terraform managed."
|
+| [group_configs](variables.tf#L62) | 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({…}))
| | {…}
|
+| [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({…}))
| | {}
|
+| [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({…})
| | {}
|
+| [urlmap_config](variables-urlmap.tf#L19) | The URL map configuration. | object({…})
| | {…}
|
+| [use_classic_version](variables.tf#L228) | Use classic Global Load Balancer. | bool
| | true
|
## Outputs
| name | description | sensitive |
|---|---|:---:|
-| [backend_services](outputs.tf#L22) | Backend service resources. | |
-| [global_forwarding_rule](outputs.tf#L57) | The global forwarding rule. | |
-| [health_checks](outputs.tf#L17) | Health-check resources. | |
-| [ip_address](outputs.tf#L44) | The reserved global IP address. | |
-| [ssl_certificates](outputs.tf#L35) | The SSL certificate. | |
-| [target_proxy](outputs.tf#L49) | The target proxy. | |
-| [url_map](outputs.tf#L30) | The url-map. | |
+| [address](outputs.tf#L17) | Forwarding rule address. | |
+| [backend_service_ids](outputs.tf#L22) | Backend service resources. | |
+| [forwarding_rule](outputs.tf#L29) | Forwarding rule resource. | |
+| [group_ids](outputs.tf#L34) | Autogenerated instance group ids. | |
+| [health_check_ids](outputs.tf#L41) | Autogenerated health check ids. | |
+| [neg_ids](outputs.tf#L48) | Autogenerated network endpoint group ids. | |
diff --git a/modules/net-glb/backend-service.tf b/modules/net-glb/backend-service.tf
new file mode 100644
index 0000000000..acadda3bdb
--- /dev/null
+++ b/modules/net-glb/backend-service.tf
@@ -0,0 +1,262 @@
+/**
+ * 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:file:description Backend service resources.
+
+locals {
+ group_ids = merge(
+ {
+ for k, v in google_compute_instance_group.default : k => v.id
+ },
+ {
+ for k, v in google_compute_global_network_endpoint_group.default : k => v.id
+ },
+ {
+ for k, v in google_compute_network_endpoint_group.default : k => v.id
+ },
+ {
+ for k, v in google_compute_region_network_endpoint_group.psc : k => v.id
+ },
+ {
+ for k, v in google_compute_region_network_endpoint_group.serverless : k => v.id
+ }
+ )
+ hc_ids = {
+ for k, v in google_compute_health_check.default : k => v.id
+ }
+}
+
+# google_compute_backend_bucket
+
+resource "google_compute_backend_service" "default" {
+ provider = google-beta
+ for_each = var.backend_service_configs
+ project = (
+ each.value.project_id == null
+ ? var.project_id
+ : each.value.project_id
+ )
+ name = "${var.name}-${each.key}"
+ description = var.description
+ affinity_cookie_ttl_sec = each.value.affinity_cookie_ttl_sec
+ compression_mode = each.value.compression_mode
+ connection_draining_timeout_sec = each.value.connection_draining_timeout_sec
+ custom_request_headers = each.value.custom_request_headers
+ custom_response_headers = each.value.custom_response_headers
+ enable_cdn = each.value.enable_cdn
+ 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 = var.use_classic_version ? "EXTERNAL" : "EXTERNAL_MANAGED"
+ port_name = (
+ each.value.port_name == null
+ ? lower(each.value.protocol == null ? var.protocol : each.value.protocol)
+ : each.value.port_name
+ )
+ protocol = (
+ each.value.protocol == null ? var.protocol : each.value.protocol
+ )
+ security_policy = each.value.security_policy
+ session_affinity = each.value.session_affinity
+ timeout_sec = each.value.timeout_sec
+
+ dynamic "backend" {
+ for_each = { for b in coalesce(each.value.backends, []) : b.backend => b }
+ content {
+ group = lookup(local.group_ids, backend.key, backend.key)
+ balancing_mode = backend.value.balancing_mode # UTILIZATION, RATE
+ capacity_scaler = backend.value.capacity_scaler
+ description = backend.value.description
+ max_connections = try(
+ backend.value.max_connections.per_group, null
+ )
+ max_connections_per_endpoint = try(
+ backend.value.max_connections.per_endpoint, null
+ )
+ max_connections_per_instance = try(
+ backend.value.max_connections.per_instance, null
+ )
+ max_rate = try(
+ backend.value.max_rate.per_group, null
+ )
+ max_rate_per_endpoint = try(
+ backend.value.max_rate.per_endpoint, null
+ )
+ max_rate_per_instance = try(
+ backend.value.max_rate.per_instance, null
+ )
+ max_utilization = backend.value.max_utilization
+ }
+ }
+
+ dynamic "cdn_policy" {
+ for_each = (
+ each.value.cdn_policy == null ? [] : [each.value.cdn_policy]
+ )
+ iterator = cdn
+ content {
+ cache_mode = cdn.value.cache_mode
+ client_ttl = cdn.value.client_ttl
+ default_ttl = cdn.value.default_ttl
+ max_ttl = cdn.value.max_ttl
+ negative_caching = cdn.value.negative_caching
+ serve_while_stale = cdn.value.serve_while_stale
+ signed_url_cache_max_age_sec = cdn.value.signed_url_cache_max_age_sec
+ dynamic "cache_key_policy" {
+ for_each = (
+ cdn.value.cache_key_policy == null
+ ? []
+ : [cdn.value.cache_key_policy]
+ )
+ iterator = ck
+ content {
+ include_host = ck.value.include_host
+ include_named_cookies = ck.value.include_named_cookies
+ include_protocol = ck.value.include_protocol
+ include_query_string = ck.value.include_query_string
+ query_string_blacklist = ck.value.query_string_blacklist
+ query_string_whitelist = ck.value.query_string_whitelist
+ }
+ }
+ dynamic "negative_caching_policy" {
+ for_each = (
+ cdn.value.negative_caching_policy == null
+ ? []
+ : [cdn.value.negative_caching_policy]
+ )
+ iterator = nc
+ content {
+ code = nc.value.code
+ ttl = nc.value.ttl
+ }
+ }
+ }
+ }
+
+ dynamic "circuit_breakers" {
+ for_each = (
+ each.value.circuit_breakers == null ? [] : [each.value.circuit_breakers]
+ )
+ iterator = cb
+ content {
+ max_connections = cb.value.max_connections
+ max_pending_requests = cb.value.max_pending_requests
+ max_requests = cb.value.max_requests
+ max_requests_per_connection = cb.value.max_requests_per_connection
+ max_retries = cb.value.max_retries
+ dynamic "connect_timeout" {
+ for_each = (
+ cb.value.connect_timeout == null ? [] : [cb.value.connect_timeout]
+ )
+ content {
+ seconds = connect_timeout.value.seconds
+ nanos = connect_timeout.value.nanos
+ }
+ }
+ }
+ }
+
+ dynamic "consistent_hash" {
+ for_each = (
+ each.value.consistent_hash == null ? [] : [each.value.consistent_hash]
+ )
+ iterator = ch
+ content {
+ http_header_name = ch.value.http_header_name
+ minimum_ring_size = ch.value.minimum_ring_size
+ dynamic "http_cookie" {
+ for_each = ch.value.http_cookie == null ? [] : [ch.value.http_cookie]
+ content {
+ name = http_cookie.value.name
+ path = http_cookie.value.path
+ dynamic "ttl" {
+ for_each = (
+ http_cookie.value.ttl == null ? [] : [http_cookie.value.ttl]
+ )
+ content {
+ seconds = ttl.value.seconds
+ nanos = ttl.value.nanos
+ }
+ }
+ }
+ }
+ }
+ }
+
+ dynamic "iap" {
+ for_each = each.value.iap_config == null ? [] : [each.value.iap_config]
+ content {
+ oauth2_client_id = iap.value.oauth2_client_id
+ oauth2_client_secret = iap.value.oauth2_client_secret
+ oauth2_client_secret_sha256 = iap.value.oauth2_client_secret_sha256
+ }
+ }
+
+ dynamic "log_config" {
+ for_each = each.value.log_sample_rate == null ? [] : [""]
+ content {
+ enable = true
+ sample_rate = each.value.log_sample_rate
+ }
+ }
+
+ dynamic "outlier_detection" {
+ for_each = (
+ each.value.outlier_detection == null ? [] : [each.value.outlier_detection]
+ )
+ iterator = od
+ content {
+ consecutive_errors = od.value.consecutive_errors
+ consecutive_gateway_failure = od.value.consecutive_gateway_failure
+ enforcing_consecutive_errors = od.value.enforcing_consecutive_errors
+ enforcing_consecutive_gateway_failure = od.value.enforcing_consecutive_gateway_failure
+ enforcing_success_rate = od.value.enforcing_success_rate
+ max_ejection_percent = od.value.max_ejection_percent
+ success_rate_minimum_hosts = od.value.success_rate_minimum_hosts
+ success_rate_request_volume = od.value.success_rate_request_volume
+ success_rate_stdev_factor = od.value.success_rate_stdev_factor
+ dynamic "base_ejection_time" {
+ for_each = (
+ od.value.base_ejection_time == null ? [] : [od.value.base_ejection_time]
+ )
+ content {
+ seconds = base_ejection_time.value.seconds
+ nanos = base_ejection_time.value.nanos
+ }
+ }
+ dynamic "interval" {
+ for_each = (
+ od.value.interval == null ? [] : [od.value.interval]
+ )
+ content {
+ seconds = interval.value.seconds
+ nanos = interval.value.nanos
+ }
+ }
+ }
+ }
+
+ dynamic "security_settings" {
+ for_each = (
+ each.value.security_settings == null ? [] : [each.value.security_settings]
+ )
+ iterator = ss
+ content {
+ client_tls_policy = ss.value.client_tls_policy
+ subject_alt_names = ss.value.subject_alt_names
+ }
+ }
+}
diff --git a/modules/net-glb/backend-services.tf b/modules/net-glb/backend-services.tf
deleted file mode 100644
index 9bb925019d..0000000000
--- a/modules/net-glb/backend-services.tf
+++ /dev/null
@@ -1,210 +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:file:description Bucket and group backend services.
-
-locals {
- backend_services_bucket = {
- for k, v in coalesce(var.backend_services_config, {}) :
- k => v if v.bucket_config != null
- }
- backend_services_group = {
- for k, v in coalesce(var.backend_services_config, {}) :
- k => v if v.group_config != null
- }
-}
-
-resource "google_compute_backend_bucket" "bucket" {
- for_each = local.backend_services_bucket
- name = "${var.name}-${each.key}"
- description = "Terraform managed."
- project = var.project_id
- bucket_name = try(each.value.bucket_config.bucket_name, null)
- custom_response_headers = try(each.value.bucket_config.options.custom_response_headers, null)
- enable_cdn = try(each.value.enable_cdn, null)
-
- dynamic "cdn_policy" {
- for_each = try(each.value.cdn_policy, null) == null ? [] : [each.value.cdn_policy]
- content {
- cache_mode = try(cdn_policy.value.cache_mode, null)
- client_ttl = try(cdn_policy.value.client_ttl, null)
- default_ttl = try(cdn_policy.value.default_ttl, null)
- max_ttl = try(cdn_policy.value.max_ttl, null)
- negative_caching = try(cdn_policy.value.negative_caching, null)
- serve_while_stale = try(cdn_policy.value.serve_while_stale, null)
- signed_url_cache_max_age_sec = try(cdn_policy.value.signed_url_cache_max_age_sec, null)
-
- dynamic "negative_caching_policy" {
- for_each = try(cdn_policy.value.negative_caching_policy, null) == null ? [] : [cdn_policy.value.negative_caching_policy]
- iterator = ncp
- content {
- code = ncp.value.code
- ttl = ncp.value.ttl
- }
- }
- }
- }
-}
-
-resource "google_compute_backend_service" "group" {
- for_each = local.backend_services_group
- name = "${var.name}-${each.key}"
- project = var.project_id
- description = "Terraform managed."
- affinity_cookie_ttl_sec = try(each.value.group_config.options.affinity_cookie_ttl_sec, null)
- enable_cdn = try(each.value.enable_cdn, null)
- custom_request_headers = try(each.value.group_config.options.custom_request_headers, null)
- custom_response_headers = try(each.value.group_config.options.custom_response_headers, null)
- connection_draining_timeout_sec = try(each.value.group_config.options.connection_draining_timeout_sec, null)
- load_balancing_scheme = try(each.value.group_config.options.load_balancing_scheme, null)
- locality_lb_policy = try(each.value.group_config.options.locality_lb_policy, null)
- port_name = try(each.value.group_config.options.port_name, null)
- protocol = try(each.value.group_config.options.protocol, null)
- security_policy = try(each.value.group_config.options.security_policy, null)
- session_affinity = try(each.value.group_config.options.session_affinity, null)
- timeout_sec = try(each.value.group_config.options.timeout_sec, null)
-
- # If no health checks are defined, use the default one.
- # Otherwise, look in the health_checks_config map.
- # Otherwise, use the health_check id as is (already existing).
- health_checks = (
- try(length(each.value.group_config.health_checks), 0) == 0
- ? try(
- [google_compute_health_check.health_check["default"].id],
- null
- )
- : [
- for hc in each.value.group_config.health_checks :
- try(google_compute_health_check.health_check[hc].id, hc)
- ]
- )
-
- dynamic "backend" {
- for_each = try(each.value.group_config.backends, [])
- content {
- balancing_mode = try(backend.value.options.balancing_mode, null)
- capacity_scaler = try(backend.value.options.capacity_scaler, null)
- group = try(backend.value.group, null)
- max_connections = try(backend.value.options.max_connections, null)
- max_connections_per_instance = try(backend.value.options.max_connections_per_instance, null)
- max_connections_per_endpoint = try(backend.value.options.max_connections_per_endpoint, null)
- max_rate = try(backend.value.options.max_rate, null)
- max_rate_per_instance = try(backend.value.options.max_rate_per_instance, null)
- max_rate_per_endpoint = try(backend.value.options.max_rate_per_endpoint, null)
- max_utilization = try(backend.value.options.max_utilization, null)
- }
- }
-
- dynamic "circuit_breakers" {
- for_each = (
- try(each.value.group_config.options.circuit_breakers, null) == null
- ? []
- : [each.value.group_config.options.circuit_breakers]
- )
- iterator = cb
- content {
- max_requests_per_connection = try(cb.value.max_requests_per_connection, null)
- max_connections = try(cb.value.max_connections, null)
- max_pending_requests = try(cb.value.max_pending_requests, null)
- max_requests = try(cb.value.max_requests, null)
- max_retries = try(cb.value.max_retries, null)
- }
- }
-
- dynamic "consistent_hash" {
- for_each = (
- try(each.value.group_config.options.consistent_hash, null) == null
- ? []
- : [each.value.group_config.options.consistent_hash]
- )
- content {
- http_header_name = try(consistent_hash.value.http_header_name, null)
- minimum_ring_size = try(consistent_hash.value.minimum_ring_size, null)
-
- dynamic "http_cookie" {
- for_each = try(consistent_hash.value.http_cookie, null) == null ? [] : [consistent_hash.value.http_cookie]
- content {
- name = try(http_cookie.value.name, null)
- path = try(http_cookie.value.path, null)
-
- dynamic "ttl" {
- for_each = try(consistent_hash.value.ttl, null) == null ? [] : [consistent_hash.value.ttl]
- content {
- seconds = try(ttl.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive
- nanos = try(ttl.value.nanos, null) # Must be from 0 to 999,999,999 inclusive
- }
- }
- }
- }
- }
- }
-
- dynamic "cdn_policy" {
- for_each = (
- try(each.value.cdn_policy, null) == null
- ? []
- : [each.value.cdn_policy]
- )
- iterator = cdn_policy
- content {
- signed_url_cache_max_age_sec = try(cdn_policy.value.signed_url_cache_max_age_sec, null)
- default_ttl = try(cdn_policy.value.default_ttl, null)
- max_ttl = try(cdn_policy.value.max_ttl, null)
- client_ttl = try(cdn_policy.value.client_ttl, null)
- negative_caching = try(cdn_policy.value.negative_caching, null)
- cache_mode = try(cdn_policy.value.cache_mode, null)
- serve_while_stale = try(cdn_policy.value.serve_while_stale, null)
-
- dynamic "negative_caching_policy" {
- for_each = (
- try(cdn_policy.value.negative_caching_policy, null) == null
- ? []
- : [cdn_policy.value.negative_caching_policy]
- )
- iterator = ncp
- content {
- code = try(ncp.value.code, null)
- ttl = try(ncp.value.ttl, null)
- }
- }
- }
- }
-
- dynamic "iap" {
- for_each = (
- try(each.value.group_config.options.iap, null) == null
- ? []
- : [each.value.group_config.options.iap]
- )
- content {
- oauth2_client_id = try(iap.value.oauth2_client_id, null)
- oauth2_client_secret = try(iap.value.oauth2_client_secret, null) # sensitive
- oauth2_client_secret_sha256 = try(iap.value.oauth2_client_secret_sha256, null) # sensitive
- }
- }
-
- dynamic "log_config" {
- for_each = (
- try(each.value.group_config.log_config, null) == null
- ? []
- : [each.value.group_config.log_config]
- )
- content {
- enable = try(log_config.value.enable, null)
- sample_rate = try(log_config.value.sample_rate, null)
- }
- }
-}
diff --git a/modules/net-glb/backends.tf b/modules/net-glb/backends.tf
new file mode 100644
index 0000000000..7f4f59cce6
--- /dev/null
+++ b/modules/net-glb/backends.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.
+ */
+
+# tfdoc:file:description Backend groups and backend buckets resources.
+
+resource "google_compute_backend_bucket" "default" {
+ for_each = var.backend_buckets_config
+ project = var.project_id
+ name = "${var.name}-${each.key}"
+ bucket_name = each.value.bucket_name
+ compression_mode = each.value.compression_mode
+ custom_response_headers = each.value.custom_response_headers
+ description = each.value.description
+ edge_security_policy = each.value.edge_security_policy
+ enable_cdn = each.value.enable_cdn
+
+ dynamic "cdn_policy" {
+ for_each = each.value.cdn_policy == null ? [] : [each.value.cdn_policy]
+ iterator = p
+ content {
+ cache_mode = p.value.cache_mode
+ client_ttl = p.value.client_ttl
+ default_ttl = p.value.default_ttl
+ max_ttl = p.value.max_ttl
+ negative_caching = p.value.negative_caching
+ request_coalescing = p.value.request_coalescing
+ serve_while_stale = p.value.serve_while_stale
+ signed_url_cache_max_age_sec = p.value.signed_url_cache_max_age_sec
+ dynamic "bypass_cache_on_request_headers" {
+ for_each = (
+ p.value.bypass_cache_on_request_headers == null
+ ? []
+ : [p.value.bypass_cache_on_request_headers]
+ )
+ iterator = h
+ content {
+ header_name = h.value
+ }
+ }
+ dynamic "cache_key_policy" {
+ for_each = (
+ p.value.cache_key_policy == null ? [] : [p.value.cache_key_policy]
+ )
+ iterator = ckp
+ content {
+ include_http_headers = ckp.value.include_http_headers
+ query_string_whitelist = ckp.value.query_string_whitelist
+ }
+ }
+ dynamic "negative_caching_policy" {
+ for_each = (
+ p.value.negative_caching_policy == null
+ ? []
+ : [p.value.negative_caching_policy]
+ )
+ iterator = ncp
+ content {
+ code = ncp.value.code
+ ttl = ncp.value.ttl
+ }
+ }
+ }
+ }
+}
+
+resource "google_compute_instance_group" "default" {
+ for_each = var.group_configs
+ project = (
+ each.value.project_id == null
+ ? var.project_id
+ : each.value.project_id
+ )
+ zone = each.value.zone
+ name = "${var.name}-${each.key}"
+ description = var.description
+ instances = each.value.instances
+
+ dynamic "named_port" {
+ for_each = each.value.named_ports
+ content {
+ name = named_port.key
+ port = named_port.value
+ }
+ }
+}
diff --git a/modules/net-glb/global-forwarding-rule.tf b/modules/net-glb/global-forwarding-rule.tf
deleted file mode 100644
index b4d525c9f6..0000000000
--- a/modules/net-glb/global-forwarding-rule.tf
+++ /dev/null
@@ -1,56 +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:file:description Global address and forwarding rule.
-
-locals {
- ip_address = (
- var.reserve_ip_address
- ? google_compute_global_address.static_ip.0.id
- : null
- )
-
- port_range = coalesce(
- var.global_forwarding_rule_config.port_range,
- var.https ? "443" : "80"
- )
-
- target = (
- var.https
- ? google_compute_target_https_proxy.https.0.id
- : google_compute_target_http_proxy.http.0.id
- )
-}
-
-resource "google_compute_global_address" "static_ip" {
- count = var.reserve_ip_address ? 1 : 0
- provider = google-beta
- name = var.name
- project = var.project_id
- description = "Terraform managed."
-}
-
-resource "google_compute_global_forwarding_rule" "forwarding_rule" {
- provider = google-beta
- name = var.name
- project = var.project_id
- description = "Terraform managed."
- ip_protocol = var.global_forwarding_rule_config.ip_protocol
- load_balancing_scheme = var.global_forwarding_rule_config.load_balancing_scheme
- port_range = local.port_range
- target = local.target
- ip_address = local.ip_address
-}
diff --git a/modules/net-glb/health-check.tf b/modules/net-glb/health-check.tf
new file mode 100644
index 0000000000..66ba58c56f
--- /dev/null
+++ b/modules/net-glb/health-check.tf
@@ -0,0 +1,113 @@
+/**
+ * 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:file:description Health check resource.
+
+resource "google_compute_health_check" "default" {
+ provider = google-beta
+ for_each = var.health_check_configs
+ project = (
+ each.value.project_id == null
+ ? var.project_id
+ : each.value.project_id
+ )
+ name = "${var.name}-${each.key}"
+ description = each.value.description
+ check_interval_sec = each.value.check_interval_sec
+ healthy_threshold = each.value.healthy_threshold
+ timeout_sec = each.value.timeout_sec
+ unhealthy_threshold = each.value.unhealthy_threshold
+
+ dynamic "grpc_health_check" {
+ for_each = try(each.value.grpc, null) != null ? [""] : []
+ content {
+ port = each.value.grpc.port
+ port_name = each.value.grpc.port_name
+ port_specification = each.value.grpc.port_specification
+ grpc_service_name = each.value.grpc.service_name
+ }
+ }
+
+ dynamic "http_health_check" {
+ for_each = try(each.value.http, null) != null ? [""] : []
+ content {
+ host = each.value.http.host
+ port = each.value.http.port
+ port_name = each.value.http.port_name
+ port_specification = each.value.http.port_specification
+ proxy_header = each.value.http.proxy_header
+ request_path = each.value.http.request_path
+ response = each.value.http.response
+ }
+ }
+
+ dynamic "http2_health_check" {
+ for_each = try(each.value.http2, null) != null ? [""] : []
+ content {
+ host = each.value.http2.host
+ port = each.value.http2.port
+ port_name = each.value.http2.port_name
+ port_specification = each.value.http2.port_specification
+ proxy_header = each.value.http2.proxy_header
+ request_path = each.value.http2.request_path
+ response = each.value.http2.response
+ }
+ }
+
+ dynamic "https_health_check" {
+ for_each = try(each.value.https, null) != null ? [""] : []
+ content {
+ host = each.value.https.host
+ port = each.value.https.port
+ port_name = each.value.https.port_name
+ port_specification = each.value.https.port_specification
+ proxy_header = each.value.https.proxy_header
+ request_path = each.value.https.request_path
+ response = each.value.https.response
+ }
+ }
+
+ dynamic "ssl_health_check" {
+ for_each = try(each.value.ssl, null) != null ? [""] : []
+ content {
+ port = each.value.ssl.port
+ port_name = each.value.ssl.port_name
+ port_specification = each.value.ssl.port_specification
+ proxy_header = each.value.ssl.proxy_header
+ request = each.value.ssl.request
+ response = each.value.ssl.response
+ }
+ }
+
+ dynamic "tcp_health_check" {
+ for_each = try(each.value.tcp, null) != null ? [""] : []
+ content {
+ port = each.value.tcp.port
+ port_name = each.value.tcp.port_name
+ port_specification = each.value.tcp.port_specification
+ proxy_header = each.value.tcp.proxy_header
+ request = each.value.tcp.request
+ response = each.value.tcp.response
+ }
+ }
+
+ dynamic "log_config" {
+ for_each = try(each.value.enable_logging, null) == true ? [""] : []
+ content {
+ enable = true
+ }
+ }
+}
diff --git a/modules/net-glb/health-checks.tf b/modules/net-glb/health-checks.tf
deleted file mode 100644
index 88a2954855..0000000000
--- a/modules/net-glb/health-checks.tf
+++ /dev/null
@@ -1,150 +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:file:description Health checks.
-
-locals {
- # Get group backend services without health checks defined
- _backends_without_hcs = [
- for k, v in coalesce(var.backend_services_config, {}) :
- v if(
- v.group_config != null
- && (
- try(v.group_config.health_checks, null) == null
- || length(try(v.group_config.health_checks, [])) == 0
- )
- )
- ]
-
- health_checks_config_defaults = (
- try(var.health_checks_config_defaults, null) == null
- ? null
- : { default = var.health_checks_config_defaults }
- )
-
- # If at least one group backend service without HC is defined,
- # create also a default HC (if default HC is not null)
- health_checks_config = (
- length(local._backends_without_hcs) > 0
- ? merge(
- coalesce(local.health_checks_config_defaults, {}),
- coalesce(var.health_checks_config, {})
- )
- : coalesce(var.health_checks_config, {})
- )
-}
-
-resource "google_compute_health_check" "health_check" {
- for_each = local.health_checks_config
- provider = google-beta
- name = "${var.name}-${each.key}"
- project = var.project_id
- description = "Terraform managed."
- check_interval_sec = try(each.value.options.check_interval_sec, null)
- healthy_threshold = try(each.value.options.healthy_threshold, null)
- timeout_sec = try(each.value.options.timeout_sec, null)
- unhealthy_threshold = try(each.value.options.unhealthy_threshold, null)
-
- dynamic "http_health_check" {
- for_each = (
- try(each.value.type, null) == "http" || try(each.value.type, null) == null
- ? { 1 = 1 }
- : {}
- )
- content {
- host = try(each.value.check.host, null)
- port = try(each.value.check.port, null)
- port_name = try(each.value.check.port_name, null)
- port_specification = try(each.value.check.port_specification, null)
- proxy_header = try(each.value.check.proxy_header, null)
- request_path = try(each.value.check.request_path, null)
- response = try(each.value.check.response, null)
- }
- }
-
- dynamic "https_health_check" {
- for_each = (
- try(each.value.type, null) == "https" || try(each.value.type, null) == null
- ? { 1 = 1 }
- : {}
- )
- content {
- host = try(each.value.check.host, null)
- port = try(each.value.check.port, null)
- port_name = try(each.value.check.port_name, null)
- port_specification = try(each.value.check.port_specification, null)
- proxy_header = try(each.value.check.proxy_header, null)
- request_path = try(each.value.check.request_path, null)
- response = try(each.value.check.response, null)
- }
- }
-
- dynamic "tcp_health_check" {
- for_each = (
- try(each.value.type, null) == "tcp" || try(each.value.type, null) == null
- ? { 1 = 1 }
- : {}
- )
- content {
- port = try(each.value.check.port, null)
- port_name = try(each.value.check.port_name, null)
- port_specification = try(each.value.check.port_specification, null)
- proxy_header = try(each.value.check.proxy_header, null)
- request = try(each.value.check.request, null)
- response = try(each.value.check.response, null)
- }
- }
-
- dynamic "ssl_health_check" {
- for_each = (
- try(each.value.type, null) == "ssl" || try(each.value.type, null) == null
- ? { 1 = 1 }
- : {}
- )
- content {
- port = try(each.value.check.port, null)
- port_name = try(each.value.check.port_name, null)
- port_specification = try(each.value.check.port_specification, null)
- proxy_header = try(each.value.check.proxy_header, null)
- request = try(each.value.check.request, null)
- response = try(each.value.check.response, null)
- }
- }
-
- dynamic "http2_health_check" {
- for_each = (
- try(each.value.type, null) == "http2" || try(each.value.type, null) == null
- ? { 1 = 1 }
- : {}
- )
- content {
- host = try(each.value.check.host, null)
- port = try(each.value.check.port, null)
- port_name = try(each.value.check.port_name, null)
- port_specification = try(each.value.check.port_specification, null)
- proxy_header = try(each.value.check.proxy_header, null)
- request_path = try(each.value.check.request_path, null)
- response = try(each.value.check.response, null)
- }
- }
-
- dynamic "log_config" {
- for_each = try(each.value.logging, false) ? { 0 = 0 } : {}
- content {
- enable = true
- }
- }
-}
diff --git a/modules/net-glb/main.tf b/modules/net-glb/main.tf
new file mode 100644
index 0000000000..ebe438ec13
--- /dev/null
+++ b/modules/net-glb/main.tf
@@ -0,0 +1,91 @@
+/**
+ * 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 {
+ fwd_rule_ports = (
+ var.protocol == "HTTPS" ? [443] : coalesce(var.ports, [80])
+ )
+ fwd_rule_target = (
+ var.protocol == "HTTPS"
+ ? google_compute_target_https_proxy.default.0.id
+ : google_compute_target_http_proxy.default.0.id
+ )
+ proxy_ssl_certificates = concat(
+ coalesce(var.ssl_certificates.certificate_ids, []),
+ [for k, v in google_compute_ssl_certificate.default : v.id],
+ [for k, v in google_compute_managed_ssl_certificate.default : v.id]
+ )
+}
+
+resource "google_compute_global_forwarding_rule" "default" {
+ provider = google-beta
+ project = var.project_id
+ name = var.name
+ description = var.description
+ ip_address = var.address
+ ip_protocol = "TCP"
+ load_balancing_scheme = (
+ var.use_classic_version ? "EXTERNAL" : "EXTERNAL_MANAGED"
+ )
+ port_range = join(",", local.fwd_rule_ports)
+ labels = var.labels
+ target = local.fwd_rule_target
+}
+
+# certificates
+
+resource "google_compute_ssl_certificate" "default" {
+ for_each = var.ssl_certificates.create_configs
+ project = var.project_id
+ name = "${var.name}-${each.key}"
+ certificate = trimspace(each.value.certificate)
+ private_key = trimspace(each.value.private_key)
+}
+
+resource "google_compute_managed_ssl_certificate" "default" {
+ for_each = var.ssl_certificates.managed_configs
+ project = var.project_id
+ name = "${var.name}-${each.key}"
+ description = each.value.description
+ managed {
+ domains = each.value.domains
+ }
+ lifecycle {
+ create_before_destroy = true
+ }
+}
+
+# proxies
+
+resource "google_compute_target_http_proxy" "default" {
+ count = var.protocol == "HTTPS" ? 0 : 1
+ project = var.project_id
+ name = var.name
+ description = var.description
+ url_map = google_compute_url_map.default.id
+}
+
+resource "google_compute_target_https_proxy" "default" {
+ count = var.protocol == "HTTPS" ? 1 : 0
+ project = var.project_id
+ name = var.name
+ description = var.description
+ certificate_map = var.https_proxy_config.certificate_map
+ quic_override = var.https_proxy_config.quic_override
+ ssl_certificates = local.proxy_ssl_certificates
+ ssl_policy = var.https_proxy_config.ssl_policy
+ url_map = google_compute_url_map.default.id
+}
diff --git a/modules/net-glb/negs.tf b/modules/net-glb/negs.tf
new file mode 100644
index 0000000000..0011968d52
--- /dev/null
+++ b/modules/net-glb/negs.tf
@@ -0,0 +1,159 @@
+/**
+ * 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:file:description NEG resources.
+
+locals {
+ _neg_endpoints_global = flatten([
+ for k, v in local.neg_global : [
+ 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 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.key) => v
+ }
+ neg_endpoints_zonal = {
+ for v in local._neg_endpoints_zonal : (v.key) => v
+ }
+ neg_global = {
+ for k, v in var.neg_configs :
+ k => v if v.internet != null
+ }
+ neg_regional_psc = {
+ for k, v in var.neg_configs :
+ k => v if v.psc != null
+ }
+ neg_regional_serverless = {
+ for k, v in var.neg_configs :
+ k => v if v.cloudrun != null || v.cloudfunction != null
+ }
+ neg_zonal = {
+ # we need to rebuild new objects as we cannot merge different types
+ for k, v in var.neg_configs : k => {
+ description = v.description
+ endpoints = v.gce != null ? v.gce.endpoints : v.hybrid.endpoints
+ network = v.gce != null ? v.gce.network : v.hybrid.network
+ subnetwork = v.gce != null ? v.gce.subnetwork : null
+ type = v.gce != null ? "GCE_VM_IP_PORT" : "NON_GCP_PRIVATE_IP_PORT"
+ zone = v.gce != null ? v.gce.zone : v.hybrid.zone
+ } if v.gce != null || v.hybrid != null
+ }
+}
+
+
+resource "google_compute_global_network_endpoint_group" "default" {
+ for_each = local.neg_global
+ project = var.project_id
+ name = "${var.name}-${each.key}"
+ # re-enable once provider properly supports this
+ # default_port = each.value.default_port
+ description = coalesce(each.value.description, var.description)
+ network_endpoint_type = (
+ each.value.internet.use_fqdn ? "INTERNET_FQDN_PORT" : "INTERNET_IP_PORT"
+ )
+}
+
+resource "google_compute_global_network_endpoint" "default" {
+ for_each = local.neg_endpoints_global
+ project = (
+ google_compute_global_network_endpoint_group.default[each.value.neg].project
+ )
+ global_network_endpoint_group = (
+ google_compute_global_network_endpoint_group.default[each.value.neg].name
+ )
+ fqdn = each.value.use_fqdn ? each.value.destination : null
+ ip_address = each.value.use_fqdn ? null : each.value.destination
+ port = each.value.port
+}
+
+
+resource "google_compute_network_endpoint_group" "default" {
+ for_each = local.neg_zonal
+ project = var.project_id
+ zone = each.value.zone
+ name = "${var.name}-${each.key}"
+ # re-enable once provider properly supports this
+ # default_port = each.value.default_port
+ description = coalesce(each.value.description, var.description)
+ network_endpoint_type = each.value.type
+ network = each.value.network
+ subnetwork = (
+ each.value.type == "NON_GCP_PRIVATE_IP_PORT"
+ ? null
+ : each.value.subnetwork
+ )
+}
+
+resource "google_compute_network_endpoint" "default" {
+ for_each = local.neg_endpoints_zonal
+ project = (
+ google_compute_network_endpoint_group.default[each.value.neg].project
+ )
+ network_endpoint_group = (
+ google_compute_network_endpoint_group.default[each.value.neg].name
+ )
+ instance = try(each.value.instance, null)
+ ip_address = each.value.ip_address
+ port = each.value.port
+ zone = each.value.zone
+}
+
+resource "google_compute_region_network_endpoint_group" "psc" {
+ for_each = local.neg_regional_psc
+ project = var.project_id
+ region = each.value.psc.region
+ name = "${var.name}-${each.key}"
+ description = coalesce(each.value.description, var.description)
+ network_endpoint_type = "PRIVATE_SERVICE_CONNECT"
+ psc_target_service = each.value.psc.target_service
+ network = each.value.psc.network
+ subnetwork = each.value.psc.subnetwork
+}
+
+resource "google_compute_region_network_endpoint_group" "serverless" {
+ for_each = local.neg_regional_serverless
+ project = var.project_id
+ region = try(
+ each.value.cloudrun.region, each.value.cloudfunction.region, null
+ )
+ name = "${var.name}-${each.key}"
+ description = coalesce(each.value.description, var.description)
+ network_endpoint_type = "SERVERLESS"
+ dynamic "cloud_function" {
+ for_each = each.value.cloudfunction == null ? [] : [""]
+ content {
+ function = each.value.cloudfunction.target_function
+ url_mask = each.value.cloudfunction.target_urlmask
+ }
+ }
+ dynamic "cloud_run" {
+ for_each = each.value.cloudrun == null ? [] : [""]
+ content {
+ service = try(each.value.cloudrun.target_service.name, null)
+ tag = try(each.value.cloudrun.target_service.tag, null)
+ url_mask = each.value.cloudrun.target_urlmask
+ }
+ }
+}
diff --git a/modules/net-glb/outputs.tf b/modules/net-glb/outputs.tf
index 1f4754db41..bc95e985a5 100644
--- a/modules/net-glb/outputs.tf
+++ b/modules/net-glb/outputs.tf
@@ -14,47 +14,40 @@
* limitations under the License.
*/
-output "health_checks" {
- description = "Health-check resources."
- value = try(google_compute_health_check.health_check, [])
+output "address" {
+ description = "Forwarding rule address."
+ value = google_compute_global_forwarding_rule.default.ip_address
}
-output "backend_services" {
+output "backend_service_ids" {
description = "Backend service resources."
value = {
- bucket = try(google_compute_backend_bucket.bucket, [])
- group = try(google_compute_backend_service.group, [])
+ for k, v in google_compute_backend_service.default : k => v.id
}
}
-output "url_map" {
- description = "The url-map."
- value = google_compute_url_map.url_map
+output "forwarding_rule" {
+ description = "Forwarding rule resource."
+ value = google_compute_global_forwarding_rule.default
}
-output "ssl_certificates" {
- description = "The SSL certificate."
- value = try(
- google_compute_managed_ssl_certificate.managed,
- google_compute_ssl_certificate.unmanaged,
- null
- )
-}
-
-output "ip_address" {
- description = "The reserved global IP address."
- value = try(google_compute_global_address.static_ip, null)
+output "group_ids" {
+ description = "Autogenerated instance group ids."
+ value = {
+ for k, v in google_compute_instance_group.default : k => v.id
+ }
}
-output "target_proxy" {
- description = "The target proxy."
- value = try(
- google_compute_target_http_proxy.http,
- google_compute_target_https_proxy.https
- )
+output "health_check_ids" {
+ description = "Autogenerated health check ids."
+ value = {
+ for k, v in google_compute_health_check.default : k => v.id
+ }
}
-output "global_forwarding_rule" {
- description = "The global forwarding rule."
- value = google_compute_global_forwarding_rule.forwarding_rule
+output "neg_ids" {
+ description = "Autogenerated network endpoint group ids."
+ value = {
+ for k, v in google_compute_network_endpoint_group.default : k => v.id
+ }
}
diff --git a/modules/net-glb/ssl-certificates.tf b/modules/net-glb/ssl-certificates.tf
deleted file mode 100644
index 8e45f5ffde..0000000000
--- a/modules/net-glb/ssl-certificates.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:file:description SSL certificates.
-
-locals {
- # If the HTTPS target proxy has no SSL certs
- # set, create also a default managed SSL cert
- ssl_certificates_config = merge(
- coalesce(var.ssl_certificates_config, {}),
- try(length(var.target_proxy_https_config.ssl_certificates), 0) == 0
- ? { default = var.ssl_certificates_config_defaults }
- : {}
- )
-
- ssl_certs_managed = (
- var.https
- ? {
- for k, v in coalesce(local.ssl_certificates_config, {}) :
- k => v if v.unmanaged_config == null
- }
- : {}
- )
- ssl_certs_unmanaged = (
- var.https
- ? {
- for k, v in coalesce(local.ssl_certificates_config, {}) :
- k => v if v.unmanaged_config != null
- }
- : {}
- )
-}
-
-resource "google_compute_managed_ssl_certificate" "managed" {
- for_each = local.ssl_certs_managed
- project = var.project_id
- name = "${var.name}-${each.key}"
- managed {
- domains = try(each.value.domains, null)
- }
-}
-
-resource "google_compute_ssl_certificate" "unmanaged" {
- for_each = local.ssl_certs_unmanaged
- project = var.project_id
- name = "${var.name}-${each.key}"
- certificate = try(each.value.unmanaged_config.tls_self_signed_cert, null)
- private_key = try(each.value.unmanaged_config.tls_private_key, null)
-}
diff --git a/modules/net-glb/target-proxy.tf b/modules/net-glb/target-proxy.tf
deleted file mode 100644
index c3ede2282b..0000000000
--- a/modules/net-glb/target-proxy.tf
+++ /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.
- */
-
-# tfdoc:file:description HTTP and HTTPS target proxies.
-
-locals {
- # If no SSL certificates are defined, use the default one.
- # Otherwise, look in the ssl_certificates_config map.
- # Otherwise, use the SSL certificate id as is (already existing).
- ssl_certificates = (
- try(var.target_proxy_https_config.ssl_certificates, null) == null
- || length(coalesce(try(var.target_proxy_https_config.ssl_certificates, null), [])) == 0
- ? try(
- [google_compute_managed_ssl_certificate.managed["default"].id],
- [google_compute_ssl_certificate.unmanaged["default"].id],
- null
- )
- : [
- for cert in try(var.target_proxy_https_config.ssl_certificates, []) :
- try(
- google_compute_managed_ssl_certificate.managed[cert].id,
- google_compute_ssl_certificate.unmanaged[cert].id,
- cert
- )
- ]
- )
-}
-
-resource "google_compute_target_http_proxy" "http" {
- count = var.https ? 0 : 1
- name = var.name
- project = var.project_id
- description = "Terraform managed."
- url_map = google_compute_url_map.url_map.id
-}
-
-resource "google_compute_target_https_proxy" "https" {
- count = var.https ? 1 : 0
- name = var.name
- project = var.project_id
- description = "Terraform managed."
- url_map = google_compute_url_map.url_map.id
- ssl_certificates = local.ssl_certificates
-}
diff --git a/modules/net-glb/url-map.tf b/modules/net-glb/url-map.tf
deleted file mode 100644
index 90cb44e34e..0000000000
--- a/modules/net-glb/url-map.tf
+++ /dev/null
@@ -1,1182 +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:file:description URL maps.
-
-locals {
- # Look for a backend services in the config whose id is
- # the default_service given in the url-map.
- # If not found, use the default_service id as given
- # (assuming it's already existing).
- # If the variable is null, will be set to null.
- _default_service = try(
- google_compute_backend_bucket.bucket[var.url_map_config.default_service].id,
- google_compute_backend_service.group[var.url_map_config.default_service].id,
- var.url_map_config.default_service,
- null
- )
-
- # If no backend services are specified,
- # the first backend service defined is associated
- default_service = (
- try(local._default_service, null) == null
- && try(var.url_map_config.default_route_action.weighted_backend_services, null) == null
- && try(var.url_map_config.default_url_redirect, null) == null
- ? try(
- google_compute_backend_bucket.bucket[keys(google_compute_backend_bucket.bucket)[0]].id,
- google_compute_backend_service.group[keys(google_compute_backend_service.group)[0]].id,
- null
- )
- : null
- )
-}
-
-resource "google_compute_url_map" "url_map" {
- name = var.name
- description = "Terraform managed."
- project = var.project_id
- default_service = local.default_service
-
- dynamic "header_action" {
- for_each = (
- try(var.url_map_config.header_action, null) == null
- ? []
- : [var.url_map_config.header_action]
- )
- content {
- request_headers_to_remove = try(header_action.value.request_headers_to_remove, null)
- response_headers_to_remove = try(header_action.value.response_headers_to_remove, null)
-
- dynamic "request_headers_to_add" {
- for_each = (
- try(header_action.value.request_headers_to_add, null) == null
- ? []
- : [header_action.value.request_headers_to_add]
- )
- content {
- header_name = try(request_headers_to_add.value.header_name, null)
- header_value = try(request_headers_to_add.value.header_value, null)
- replace = try(request_headers_to_add.value.replace, null)
- }
- }
-
- dynamic "response_headers_to_add" {
- for_each = (
- try(header_action.response_headers_to_add, null) == null
- ? []
- : [header_action.response_headers_to_add]
- )
- content {
- header_name = try(response_headers_to_add.value.header_name, null)
- header_value = try(response_headers_to_add.value.header_value, null)
- replace = try(response_headers_to_add.value.replace, null)
- }
- }
- }
- }
-
- dynamic "host_rule" {
- for_each = (
- try(var.url_map_config.host_rules, null) == null
- ? []
- : var.url_map_config.host_rules
- )
- content {
- description = try(host_rule.value.description, null)
- hosts = try(host_rule.value.hosts, null)
- path_matcher = try(host_rule.value.path_matcher, null)
- }
- }
-
- dynamic "path_matcher" {
- for_each = (
- try(var.url_map_config.path_matchers, null) == null
- ? []
- : var.url_map_config.path_matchers
- )
- content {
- name = try(path_matcher.value.name, null)
- description = try(path_matcher.value.description, null)
- default_service = try(
- google_compute_backend_bucket.bucket[var.url_map_config.default_service].id,
- google_compute_backend_service.group[var.url_map_config.default_service].id,
- path_matcher.value.default_service,
- null
- )
-
- dynamic "header_action" {
- for_each = (
- try(path_matcher.value.header_action, null) == null
- ? []
- : [path_matcher.value.header_action]
- )
- content {
- request_headers_to_remove = try(header_action.value.request_headers_to_remove, null)
- response_headers_to_remove = try(header_action.value.response_headers_to_remove, null)
-
- dynamic "request_headers_to_add" {
- for_each = (
- try(header_action.value.request_headers_to_add, null) == null
- ? []
- : [header_action.value.request_headers_to_add]
- )
- content {
- header_name = try(request_headers_to_add.value.header_name, null)
- header_value = try(request_headers_to_add.value.header_value, null)
- replace = try(request_headers_to_add.value.replace, null)
- }
- }
-
- dynamic "response_headers_to_add" {
- for_each = (
- try(header_action.response_headers_to_add, null) == null
- ? []
- : [header_action.response_headers_to_add]
- )
- content {
- header_name = try(response_headers_to_add.value.header_name, null)
- header_value = try(response_headers_to_add.value.header_value, null)
- replace = try(response_headers_to_add.value.replace, null)
- }
- }
- }
- }
-
- dynamic "path_rule" {
- for_each = (
- try(path_matcher.value.path_rules, null) == null
- ? []
- : path_matcher.value.path_rules
- )
- content {
- paths = try(path_rule.value.paths, null)
- service = try(
- google_compute_backend_bucket.bucket[path_rule.value.service].id,
- google_compute_backend_service.group[path_rule.value.service].id,
- path_rule.value.service,
- null
- )
-
- dynamic "route_action" {
- for_each = (
- try(path_rule.value.route_action, null) == null
- ? []
- : [path_rule.value.route_action]
- )
- content {
-
- dynamic "cors_policy" {
- for_each = (
- try(route_action.value.cors_policy, null) == null
- ? []
- : [route_action.value.cors_policy]
- )
- content {
- allow_credentials = try(cors_policy.value.allow_credentials, null)
- allow_headers = try(cors_policy.value.allow_headers, null)
- allow_methods = try(cors_policy.value.allow_methods, null)
- allow_origin_regexes = try(cors_policy.value.allow_origin_regexes, null)
- allow_origins = try(cors_policy.value.allow_origins, null)
- disabled = try(cors_policy.value.disabled, null)
- expose_headers = try(cors_policy.value.expose_headers, null)
- max_age = try(cors_policy.value.max_age, null)
- }
- }
-
- dynamic "fault_injection_policy" {
- for_each = (
- try(route_action.value.fault_injection_policy, null) == null
- ? []
- : [route_action.value.fault_injection_policy]
- )
- iterator = policy
- content {
-
- dynamic "abort" {
- for_each = (
- try(policy.value.abort, null) == null
- ? []
- : [policy.value.abort]
- )
- content {
- http_status = try(abort.value.http_status, null) # Must be between 200 and 599 inclusive
- percentage = try(abort.value.percentage, null) # Must be between 0.0 and 100.0 inclusive
- }
- }
-
- dynamic "delay" {
- for_each = (
- try(policy.value.delay, null) == null
- ? []
- : [policy.value.delay]
- )
- content {
- percentage = try(delay.value.percentage, null) # Must be between 0.0 and 100.0 inclusive
-
- dynamic "fixed_delay" {
- for_each = (
- try(delay.value.fixed_delay, null) == null
- ? []
- : [delay.value.fixed_delay]
- )
- content {
- nanos = try(fixed_delay.value.nanos, null) # Must be from 0 to 999,999,999 inclusive
- seconds = try(fixed_delay.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive
- }
- }
- }
- }
- }
- }
-
- dynamic "request_mirror_policy" {
- for_each = (
- try(route_action.value.request_mirror_policy, null) == null
- ? []
- : [route_action.value.request_mirror_policy]
- )
- iterator = policy
- content {
- backend_service = try(
- google_compute_backend_bucket.bucket[policy.value.backend_service].id,
- google_compute_backend_service.group[policy.value.backend_service].id,
- policy.value.backend_service,
- null
- )
- }
- }
-
- dynamic "retry_policy" {
- for_each = (
- try(route_action.value.retry_policy, null) == null
- ? []
- : [route_action.value.retry_policy]
- )
- iterator = policy
- content {
- num_retries = try(policy.num_retries, null) # Must be > 0
- # Valid values at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#retry_conditions
- retry_conditions = try(policy.retry_conditions, null)
-
- dynamic "per_try_timeout" {
- for_each = (
- try(policy.value.per_try_timeout, null) == null
- ? []
- : [policy.value.per_try_timeout]
- )
- iterator = timeout
- content {
- nanos = try(timeout.value.nanos, null) # Must be from 0 to 999,999,999 inclusive
- seconds = try(timeout.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive
- }
- }
- }
- }
-
- dynamic "timeout" {
- for_each = (
- try(route_action.value.timeout, null) == null
- ? []
- : [route_action.value.timeout]
- )
- content {
- nanos = try(timeout.value.nanos, null) # Must be from 0 to 999,999,999 inclusive
- seconds = try(timeout.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive
- }
- }
-
- dynamic "url_rewrite" {
- for_each = (
- try(route_action.value.url_rewrite, null) == null
- ? []
- : [route_action.value.url_rewrite]
- )
- content {
- host_rewrite = try(url_rewrite.value.host_rewrite, null) # Must be between 1 and 255 characters
- path_prefix_rewrite = try(url_rewrite.value.path_prefix_rewrite, null) # Must be between 1 and 1024 characters
- }
- }
-
- dynamic "weighted_backend_services" {
- for_each = (
- try(route_action.value.weighted_backend_services, null) == null
- ? []
- : route_action.value.weighted_backend_services
- )
- iterator = weighted
- content {
- weight = try(weighted.value.weigth, null)
- backend_service = try(
- google_compute_backend_bucket.bucket[weighted.value.backend_service].id,
- google_compute_backend_service.group[weighted.value.backend_service].id,
- policy.value.backend_service,
- null
- )
- dynamic "header_action" {
- for_each = (
- try(path_matcher.value.header_action, null) == null
- ? []
- : [path_matcher.value.header_action]
- )
- content {
- request_headers_to_remove = try(header_action.value.request_headers_to_remove, null)
- response_headers_to_remove = try(header_action.value.response_headers_to_remove, null)
-
- dynamic "request_headers_to_add" {
- for_each = (
- try(header_action.value.request_headers_to_add, null) == null
- ? [] :
- [header_action.value.request_headers_to_add]
- )
- content {
- header_name = try(request_headers_to_add.value.header_name, null)
- header_value = try(request_headers_to_add.value.header_value, null)
- replace = try(request_headers_to_add.value.replace, null)
- }
- }
-
- dynamic "response_headers_to_add" {
- for_each = (
- try(header_action.response_headers_to_add, null) == null
- ? []
- : [header_action.response_headers_to_add]
- )
- content {
- header_name = try(response_headers_to_add.value.header_name, null)
- header_value = try(response_headers_to_add.value.header_value, null)
- replace = try(response_headers_to_add.value.replace, null)
- }
- }
- }
- }
- }
- }
- }
- }
-
- dynamic "url_redirect" {
- for_each = (
- try(path_rule.value.url_redirect, null) == null
- ? []
- : path_rule.value.url_redirect
- )
- content {
- host_redirect = try(url_redirect.value.host_redirect, null) # Must be between 1 and 255 characters
- https_redirect = try(url_redirect.value.https_redirect, null)
- path_redirect = try(url_redirect.value.path_redirect, null)
- prefix_redirect = try(url_redirect.value.prefix_redirect, null) # Must be between 1 and 1024 characters
- # Valid valus at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#redirect_response_code
- redirect_response_code = try(url_redirect.value.redirect_response_code, null)
- strip_query = try(url_redirect.value.strip_query, null)
- }
- }
- }
- }
-
- dynamic "route_rules" {
- for_each = (
- try(path_matcher.value.route_rules, null) == null
- ? []
- : path_matcher.value.route_rules
- )
- content {
- priority = try(route_rules.value.priority, null)
- service = try(
- google_compute_backend_bucket.bucket[route_rules.value.service].id,
- google_compute_backend_service.group[route_rules.value.service].id,
- route_rules.value.service,
- null
- )
-
- dynamic "header_action" {
- for_each = (
- try(path_matcher.value.header_action, null) == null
- ? []
- : [path_matcher.value.header_action]
- )
- content {
- request_headers_to_remove = try(header_action.value.request_headers_to_remove, null)
- response_headers_to_remove = try(header_action.value.response_headers_to_remove, null)
-
- dynamic "request_headers_to_add" {
- for_each = (
- try(header_action.value.request_headers_to_add, null) == null
- ? []
- : [header_action.value.request_headers_to_add]
- )
- content {
- header_name = try(request_headers_to_add.value.header_name, null)
- header_value = try(request_headers_to_add.value.header_value, null)
- replace = try(request_headers_to_add.value.replace, null)
- }
- }
-
- dynamic "response_headers_to_add" {
- for_each = (
- try(header_action.response_headers_to_add, null) == null
- ? []
- : [header_action.response_headers_to_add]
- )
- content {
- header_name = try(response_headers_to_add.value.header_name, null)
- header_value = try(response_headers_to_add.value.header_value, null)
- replace = try(response_headers_to_add.value.replace, null)
- }
- }
- }
- }
-
- dynamic "match_rules" {
- for_each = (
- try(path_matcher.value.match_rules, null) == null
- ? []
- : path_matcher.value.match_rules
- )
- content {
- full_path_match = try(match_rules.value.full_path_match, null) # Must be between 1 and 1024 characters
- ignore_case = try(match_rules.value.ignore_case, null)
- prefix_match = try(match_rules.value.prefix_match, null)
- regex_match = try(match_rules.value.regex_match, null)
-
- dynamic "header_matches" {
- for_each = (
- try(match_rules.value.header_matches, null) == null
- ? []
- : [match_rules.value.header_matches]
- )
- content {
- exact_match = try(header_matches.value.exact_match, null)
- header_name = try(header_matches.value.header_name, null)
- invert_match = try(header_matches.value.invert_match, null)
- prefix_match = try(header_matches.value.prefix_match, null)
- present_match = try(header_matches.value.present_match, null)
- regex_match = try(header_matches.value.regex_match, null)
- suffix_match = try(header_matches.value, null)
-
- dynamic "range_match" {
- for_each = try(header_matches.value.range_match, null) == null ? [] : [header_matches.value.range_match]
- content {
- range_end = try(range_match.value.range_end, null)
- range_start = try(range_match.value.range_start, null)
- }
- }
- }
- }
-
- dynamic "metadata_filters" {
- for_each = (
- try(match_rules.value.metadata_filters, null) == null
- ? []
- : [match_rules.value.metadata_filters]
- )
- content {
- # Valid values at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#filter_match_criteria
- filter_match_criteria = try(metadata_filters.value.filter_match_criteria, null)
-
- dynamic "filter_labels" {
- for_each = (
- try(metadata_filters.value.filter_labels, null) == null
- ? []
- : metadata_filters.value.filter_labels
- )
- content {
- name = try(filter_labels.value.name, null) # Must be between 1 and 1024 characters
- value = try(filter_labels.value.value, null) # Must be between 1 and 1024 characters
- }
- }
- }
- }
-
- dynamic "query_parameter_matches" {
- for_each = (
- try(match_rules.value.query_parameter_matches, null) == null
- ? []
- : [match_rules.value.query_parameter_matches]
- )
- iterator = query
- content {
- exact_match = try(query.value.exact_match, null)
- name = try(query.value.name, null)
- present_match = try(query.value.present_match, null)
- regex_match = try(query.value.regex_match, null)
- }
- }
- }
- }
-
- dynamic "route_action" {
- for_each = (
- try(route_rules.value.route_action, null) == null
- ? []
- : [route_rules.value.route_action]
- )
- content {
-
- dynamic "cors_policy" {
- for_each = (
- try(route_action.value.cors_policy, null) == null
- ? []
- : [route_action.value.cors_policy]
- )
- content {
- allow_credentials = try(cors_policy.value.allow_credentials, null)
- allow_headers = try(cors_policy.value.allow_headers, null)
- allow_methods = try(cors_policy.value.allow_methods, null)
- allow_origin_regexes = try(cors_policy.value.allow_origin_regexes, null)
- allow_origins = try(cors_policy.value.allow_origins, null)
- disabled = try(cors_policy.value.disabled, null)
- expose_headers = try(cors_policy.value.expose_headers, null)
- max_age = try(cors_policy.value.max_age, null)
- }
- }
-
- dynamic "fault_injection_policy" {
- for_each = (
- try(route_action.value.fault_injection_policy, null) == null
- ? []
- : [route_action.value.fault_injection_policy]
- )
- iterator = policy
- content {
-
- dynamic "abort" {
- for_each = (
- try(policy.value.abort, null) == null
- ? []
- : [policy.value.abort]
- )
- content {
- http_status = try(abort.value.http_status, null) # Must be between 200 and 599 inclusive
- percentage = try(abort.value.percentage, null) # Must be between 0.0 and 100.0 inclusive
- }
- }
-
- dynamic "delay" {
- for_each = (
- try(policy.value.delay, null) == null
- ? []
- : [policy.value.delay]
- )
- content {
- percentage = try(delay.value.percentage, null) # Must be between 0.0 and 100.0 inclusive
-
- dynamic "fixed_delay" {
- for_each = (
- try(delay.value.fixed_delay, null) == null
- ? []
- : [delay.value.fixed_delay]
- )
- content {
- nanos = try(fixed_delay.value.nanos, null) # Must be from 0 to 999,999,999 inclusive
- seconds = try(fixed_delay.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive
- }
- }
- }
- }
- }
- }
-
- dynamic "request_mirror_policy" {
- for_each = (
- try(route_action.value.request_mirror_policy, null) == null
- ? []
- : [route_action.value.request_mirror_policy]
- )
- iterator = policy
- content {
- backend_service = try(
- google_compute_backend_bucket.bucket[policy.value.backend_service].id,
- google_compute_backend_service.group[policy.value.backend_service].id,
- policy.value.backend_service,
- null
- )
- }
- }
-
- dynamic "retry_policy" {
- for_each = (
- try(route_action.value.retry_policy, null) == null
- ? []
- : [route_action.value.retry_policy]
- )
- iterator = policy
- content {
- num_retries = try(policy.num_retries, null) # Must be > 0
- # Valid values at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#retry_conditions
- retry_conditions = try(policy.retry_conditions, null)
-
- dynamic "per_try_timeout" {
- for_each = (
- try(policy.value.per_try_timeout, null) == null
- ? []
- : [policy.value.per_try_timeout]
- )
- iterator = timeout
- content {
- nanos = try(timeout.value.nanos, null) # Must be from 0 to 999,999,999 inclusive
- seconds = try(timeout.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive
- }
- }
- }
- }
-
- dynamic "timeout" {
- for_each = (
- try(route_action.value.timeout, null) == null
- ? []
- : [route_action.value.timeout]
- )
- content {
- nanos = try(timeout.value.nanos, null) # Must be from 0 to 999,999,999 inclusive
- seconds = try(timeout.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive
- }
- }
-
- dynamic "url_rewrite" {
- for_each = (
- try(route_action.value.url_rewrite, null) == null
- ? []
- : [route_action.value.url_rewrite]
- )
- content {
- host_rewrite = try(url_rewrite.value.host_rewrite, null) # Must be between 1 and 255 characters
- path_prefix_rewrite = try(url_rewrite.value.path_prefix_rewrite, null) # Must be between 1 and 1024 characters
- }
- }
-
- dynamic "weighted_backend_services" {
- for_each = (
- try(route_action.value.weighted_backend_services, null) == null
- ? []
- : [route_action.value.url_rewrite]
- )
- iterator = weighted
- content {
- weight = try(weighted.value.weigth, null)
- backend_service = try(
- google_compute_backend_bucket.bucket[weighted.value.backend_service].id,
- google_compute_backend_service.group[weighted.value.backend_service].id,
- weighted.value.backend_service,
- null
- )
-
- dynamic "header_action" {
- for_each = (
- try(path_matcher.value.header_action, null) == null
- ? [] :
- [path_matcher.value.header_action]
- )
- content {
- request_headers_to_remove = try(header_action.value.request_headers_to_remove, null)
- response_headers_to_remove = try(header_action.value.response_headers_to_remove, null)
-
- dynamic "request_headers_to_add" {
- for_each = (
- try(header_action.value.request_headers_to_add, null) == null
- ? []
- : [header_action.value.request_headers_to_add]
- )
- content {
- header_name = try(request_headers_to_add.value.header_name, null)
- header_value = try(request_headers_to_add.value.header_value, null)
- replace = try(request_headers_to_add.value.replace, null)
- }
- }
-
- dynamic "response_headers_to_add" {
- for_each = (
- try(header_action.response_headers_to_add, null) == null
- ? []
- : [header_action.response_headers_to_add]
- )
- content {
- header_name = try(response_headers_to_add.value.header_name, null)
- header_value = try(response_headers_to_add.value.header_value, null)
- replace = try(response_headers_to_add.value.replace, null)
- }
- }
- }
- }
- }
- }
- }
- }
-
- dynamic "url_redirect" {
- for_each = (
- try(route_rules.value.url_redirect, null) == null
- ? []
- : route_rules.value.url_redirect
- )
- content {
- host_redirect = try(url_redirect.value.host_redirect, null) # Must be between 1 and 255 characters
- https_redirect = try(url_redirect.value.https_redirect, null)
- path_redirect = try(url_redirect.value.path_redirect, null)
- prefix_redirect = try(url_redirect.value.prefix_redirect, null) # Must be between 1 and 1024 characters
- # Valid valus at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#redirect_response_code
- redirect_response_code = try(url_redirect.value.redirect_response_code, null)
- strip_query = try(url_redirect.value.strip_query, null)
- }
- }
- }
- }
-
- dynamic "default_url_redirect" {
- for_each = (
- try(path_matcher.value.default_url_redirect, null) == null
- ? []
- : path_matcher.value.default_url_redirect
- )
- content {
- host_redirect = try(default_url_redirect.value.host_redirect, null) # Must be between 1 and 255 characters
- https_redirect = try(default_url_redirect.value.https_redirect, null)
- path_redirect = try(default_url_redirect.value.path_redirect, null) # Must be between 1 and 1024 characters
- prefix_redirect = try(default_url_redirect.value.prefix_redirect, null) # Must be between 1 and 1024 characters
- # Valid values at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#redirect_response_code
- redirect_response_code = try(default_url_redirect.value.redirect_response_code, null)
- strip_query = try(default_url_redirect.value.strip_query, null)
- }
- }
-
- dynamic "default_route_action" {
- for_each = (
- try(path_matcher.value.default_route_action, null) == null
- ? []
- : path_matcher.value.default_route_action
- )
- content {
- dynamic "cors_policy" {
- for_each = (
- try(default_route_action.value.cors_policy, null) == null
- ? []
- : [default_route_action.value.cors_policy]
- )
- content {
- allow_credentials = try(cors_policy.value.allow_credentials, null)
- allow_headers = try(cors_policy.value.allow_headers, null)
- allow_methods = try(cors_policy.value.allow_methods, null)
- allow_origin_regexes = try(cors_policy.value.allow_origin_regexes, null)
- allow_origins = try(cors_policy.value.allow_origins, null)
- disabled = try(cors_policy.value.disabled, null)
- expose_headers = try(cors_policy.value.expose_headers, null)
- max_age = try(cors_policy.value.max_age, null)
- }
- }
-
- dynamic "fault_injection_policy" {
- for_each = (
- try(default_route_action.value.fault_injection_policy, null) == null
- ? []
- : [default_route_action.value.fault_injection_policy]
- )
- iterator = policy
- content {
-
- dynamic "abort" {
- for_each = (
- try(policy.value.abort, null) == null
- ? []
- : [policy.value.abort]
- )
- content {
- http_status = try(abort.value.http_status, null) # Must be between 200 and 599 inclusive
- percentage = try(abort.value.percentage, null) # Must be between 0.0 and 100.0 inclusive
- }
- }
-
- dynamic "delay" {
- for_each = (
- try(policy.value.delay, null) == null
- ? []
- : [policy.value.delay]
- )
- content {
- percentage = try(delay.value.percentage, null) # Must be between 0.0 and 100.0 inclusive
-
- dynamic "fixed_delay" {
- for_each = (
- try(delay.value.fixed_delay, null) == null
- ? []
- : [delay.value.fixed_delay]
- )
- content {
- nanos = try(fixed_delay.value.nanos, null) # Must be from 0 to 999,999,999 inclusive
- seconds = try(fixed_delay.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive
- }
- }
- }
- }
- }
- }
-
- dynamic "request_mirror_policy" {
- for_each = (
- try(default_route_action.value.request_mirror_policy, null) == null
- ? []
- : [default_route_action.value.request_mirror_policy]
- )
- iterator = policy
- content {
- backend_service = try(
- google_compute_backend_bucket.bucket[policy.value.backend_service].id,
- google_compute_backend_service.group[policy.value.backend_service].id,
- policy.value.backend_service,
- null
- )
- }
- }
-
- dynamic "retry_policy" {
- for_each = (
- try(default_route_action.value.retry_policy, null) == null
- ? []
- : [default_route_action.value.retry_policy]
- )
- iterator = policy
- content {
- num_retries = try(policy.num_retries, null) # Must be > 0
- # Valid values at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#retry_conditions
- retry_conditions = try(policy.retry_conditions, null)
-
- dynamic "per_try_timeout" {
- for_each = (
- try(policy.value.per_try_timeout, null) == null
- ? []
- : [policy.value.per_try_timeout]
- )
- iterator = timeout
- content {
- nanos = try(timeout.value.nanos, null) # Must be from 0 to 999,999,999 inclusive
- seconds = try(timeout.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive
- }
- }
- }
- }
-
- dynamic "timeout" {
- for_each = (
- try(default_route_action.value.timeout, null) == null
- ? []
- : [default_route_action.value.timeout]
- )
- content {
- nanos = try(timeout.value.nanos, null) # Must be from 0 to 999,999,999 inclusive
- seconds = try(timeout.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive
- }
- }
-
- dynamic "url_rewrite" {
- for_each = (
- try(default_route_action.value.url_rewrite, null) == null
- ? []
- : [default_route_action.value.url_rewrite]
- )
- content {
- host_rewrite = try(url_rewrite.value.host_rewrite, null) # Must be between 1 and 255 characters
- path_prefix_rewrite = try(url_rewrite.value.path_prefix_rewrite, null) # Must be between 1 and 1024 characters
- }
- }
-
- dynamic "weighted_backend_services" {
- for_each = (
- try(default_route_action.value.weighted_backend_services, null) == null
- ? []
- : default_route_action.value.weighted_backend_services
- )
- iterator = weighted
- content {
- weight = try(weighted.value.weigth, null)
- backend_service = try(
- google_compute_backend_bucket.bucket[weighted.value.backend_service].id,
- google_compute_backend_service.group[weighted.value.backend_service].id,
- weighted.value.backend_service,
- null
- )
-
- dynamic "header_action" {
- for_each = (
- try(path_matcher.value.header_action, null) == null
- ? []
- : [path_matcher.value.header_action]
- )
- content {
- request_headers_to_remove = try(header_action.value.request_headers_to_remove, null)
- response_headers_to_remove = try(header_action.value.response_headers_to_remove, null)
-
- dynamic "request_headers_to_add" {
- for_each = (
- try(header_action.value.request_headers_to_add, null) == null
- ? []
- : [header_action.value.request_headers_to_add]
- )
- content {
- header_name = try(request_headers_to_add.value.header_name, null)
- header_value = try(request_headers_to_add.value.header_value, null)
- replace = try(request_headers_to_add.value.replace, null)
- }
- }
-
- dynamic "response_headers_to_add" {
- for_each = (
- try(header_action.response_headers_to_add, null) == null
- ? []
- : [header_action.response_headers_to_add]
- )
- content {
- header_name = try(response_headers_to_add.value.header_name, null)
- header_value = try(response_headers_to_add.value.header_value, null)
- replace = try(response_headers_to_add.value.replace, null)
- }
- }
- }
- }
- }
- }
- }
- }
- }
- }
-
- # Up to 100 tests per url_map
- dynamic "test" {
- for_each = (
- try(var.url_map_config.tests, null) == null
- ? []
- : var.url_map_config.tests
- )
- content {
- description = try(test.value.description, null)
- host = try(test.value.host, null)
- path = try(test.value.path, null)
- service = try(
- google_compute_backend_bucket.bucket[test.value.service].id,
- google_compute_backend_service.group[test.value.service].id,
- test.value.service,
- null
- )
- }
- }
-
- dynamic "default_url_redirect" {
- for_each = (
- try(var.url_map_config.default_url_redirect, null) == null
- ? []
- : [var.url_map_config.default_url_redirect]
- )
- content {
- host_redirect = try(default_url_redirect.value.host_redirect, null) # Must be between 1 and 255 characters
- https_redirect = try(default_url_redirect.value.https_redirect, null)
- path_redirect = try(default_url_redirect.value.path_redirect, null) # Must be between 1 and 1024 characters
- prefix_redirect = try(default_url_redirect.value.prefix_redirect, null) # Must be between 1 and 1024 characters
- # Valid values at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#redirect_response_code
- redirect_response_code = try(default_url_redirect.value.redirect_response_code, null)
- strip_query = try(default_url_redirect.value.strip_query, null)
- }
- }
-
- dynamic "default_route_action" {
- for_each = (
- try(var.url_map_config.default_route_action, null) == null
- ? []
- : [var.url_map_config.default_route_action]
- )
- content {
- dynamic "cors_policy" {
- for_each = (
- try(default_route_action.value.cors_policy, null) == null
- ? []
- : [default_route_action.value.cors_policy]
- )
- content {
- allow_credentials = try(cors_policy.value.allow_credentials, null)
- allow_headers = try(cors_policy.value.allow_headers, null)
- allow_methods = try(cors_policy.value.allow_methods, null)
- allow_origin_regexes = try(cors_policy.value.allow_origin_regexes, null)
- allow_origins = try(cors_policy.value.allow_origins, null)
- disabled = try(cors_policy.value.disabled, null)
- expose_headers = try(cors_policy.value.expose_headers, null)
- max_age = try(cors_policy.value.max_age, null)
- }
- }
-
- dynamic "fault_injection_policy" {
- for_each = (
- try(default_route_action.value.fault_injection_policy, null) == null
- ? []
- : [default_route_action.value.fault_injection_policy]
- )
- iterator = policy
- content {
-
- dynamic "abort" {
- for_each = (
- try(policy.value.abort, null) == null
- ? []
- : [policy.value.abort]
- )
- content {
- http_status = try(abort.value.http_status, null) # Must be between 200 and 599 inclusive
- percentage = try(abort.value.percentage, null) # Must be between 0.0 and 100.0 inclusive
- }
- }
-
- dynamic "delay" {
- for_each = (
- try(policy.value.delay, null) == null
- ? []
- : [policy.value.delay]
- )
- content {
- percentage = try(delay.value.percentage, null) # Must be between 0.0 and 100.0 inclusive
-
- dynamic "fixed_delay" {
- for_each = try(delay.value.fixed_delay, null) == null ? [] : [delay.value.fixed_delay]
- content {
- nanos = try(fixed_delay.value.nanos, null) # Must be from 0 to 999,999,999 inclusive
- seconds = try(fixed_delay.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive
- }
- }
- }
- }
- }
- }
-
- dynamic "request_mirror_policy" {
- for_each = (
- try(default_route_action.value.request_mirror_policy, null) == null
- ? []
- : [default_route_action.value.request_mirror_policy]
- )
- iterator = policy
- content {
- backend_service = try(
- google_compute_backend_bucket.bucket[policy.value.backend_service].id,
- google_compute_backend_service.group[policy.value.backend_service].id,
- policy.value.backend_service,
- null
- )
- }
- }
-
- dynamic "retry_policy" {
- for_each = (
- try(default_route_action.value.retry_policy, null) == null
- ? []
- : [default_route_action.value.retry_policy]
- )
- iterator = policy
- content {
- num_retries = try(policy.num_retries, null) # Must be > 0
- # Valid values at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#retry_conditions
- retry_conditions = try(policy.retry_conditions, null)
-
- dynamic "per_try_timeout" {
- for_each = (
- try(policy.value.per_try_timeout, null) == null
- ? []
- : [policy.value.per_try_timeout]
- )
- iterator = timeout
- content {
- nanos = try(timeout.value.nanos, null) # Must be from 0 to 999,999,999 inclusive
- seconds = try(timeout.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive
- }
- }
- }
- }
-
- dynamic "timeout" {
- for_each = (
- try(default_route_action.value.timeout, null) == null
- ? []
- : [default_route_action.value.timeout]
- )
- content {
- nanos = try(timeout.value.nanos, null) # Must be from 0 to 999,999,999 inclusive
- seconds = try(timeout.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive
- }
- }
-
- dynamic "url_rewrite" {
- for_each = (
- try(default_route_action.value.url_rewrite, null) == null
- ? []
- : [default_route_action.value.url_rewrite]
- )
- content {
- host_rewrite = try(url_rewrite.value.host_rewrite, null) # Must be between 1 and 255 characters
- path_prefix_rewrite = try(url_rewrite.value.path_prefix_rewrite, null) # Must be between 1 and 1024 characters
- }
- }
-
- dynamic "weighted_backend_services" {
- for_each = (
- try(default_route_action.value.weighted_backend_services, null) == null
- ? []
- : default_route_action.value.weighted_backend_services
- )
- iterator = weighted
- content {
- weight = try(weighted.value.weigth, null)
- backend_service = try(
- google_compute_backend_bucket.bucket[weighted.value.backend_service].id,
- google_compute_backend_service.group[weighted.value.backend_service].id,
- weighted.value.backend_service,
- null
- )
-
- dynamic "header_action" {
- for_each = (
- try(weighted.value.header_action, null) == null
- ? []
- : [weighted.value.header_action]
- )
- content {
- request_headers_to_remove = try(header_action.value.request_headers_to_remove, null)
- response_headers_to_remove = try(header_action.value.response_headers_to_remove, null)
-
- dynamic "request_headers_to_add" {
- for_each = (
- try(header_action.value.request_headers_to_add, null) == null
- ? []
- : [header_action.value.request_headers_to_add]
- )
- content {
- header_name = try(request_headers_to_add.value.header_name, null)
- header_value = try(request_headers_to_add.value.header_value, null)
- replace = try(request_headers_to_add.value.replace, null)
- }
- }
-
- dynamic "response_headers_to_add" {
- for_each = (
- try(header_action.response_headers_to_add, null) == null
- ? []
- : [header_action.response_headers_to_add]
- )
- content {
- header_name = try(response_headers_to_add.value.header_name, null)
- header_value = try(response_headers_to_add.value.header_value, null)
- replace = try(response_headers_to_add.value.replace, null)
- }
- }
- }
- }
- }
- }
- }
- }
-}
diff --git a/modules/net-glb/urlmap.tf b/modules/net-glb/urlmap.tf
new file mode 100644
index 0000000000..a7f01d55b1
--- /dev/null
+++ b/modules/net-glb/urlmap.tf
@@ -0,0 +1,952 @@
+/**
+ * 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:file:description URL map resources.
+
+locals {
+ backend_ids = merge(
+ { for k, v in google_compute_backend_service.default : k => v.id },
+ { for k, v in google_compute_backend_bucket.default : k => v.id }
+ )
+}
+
+resource "google_compute_url_map" "default" {
+ provider = google-beta
+ project = var.project_id
+ name = var.name
+ description = var.description
+ default_service = (
+ var.urlmap_config.default_service == null ? null : lookup(
+ local.backend_ids,
+ var.urlmap_config.default_service,
+ var.urlmap_config.default_service
+ )
+ )
+
+ dynamic "default_route_action" {
+ for_each = (
+ var.urlmap_config.default_route_action == null
+ ? []
+ : [var.urlmap_config.default_route_action]
+ )
+ iterator = route_action
+ content {
+ dynamic "cors_policy" {
+ for_each = (
+ route_action.value.cors_policy == null
+ ? []
+ : [route_action.value.cors_policy]
+ )
+ content {
+ allow_credentials = cors_policy.value.allow_credentials
+ allow_headers = cors_policy.value.allow_headers
+ allow_methods = cors_policy.value.allow_methods
+ allow_origin_regexes = cors_policy.value.allow_origin_regexes
+ allow_origins = cors_policy.value.allow_origins
+ disabled = cors_policy.value.disabled
+ expose_headers = cors_policy.value.expose_headers
+ max_age = cors_policy.value.max_age
+ }
+ }
+ dynamic "fault_injection_policy" {
+ for_each = (
+ route_action.value.fault_injection_policy == null
+ ? []
+ : [route_action.value.fault_injection_policy]
+ )
+ content {
+ dynamic "abort" {
+ for_each = (
+ fault_injection_policy.value.abort == null
+ ? []
+ : [fault_injection_policy.value.abort]
+ )
+ content {
+ http_status = abort.value.status
+ percentage = abort.value.percentage
+ }
+ }
+ dynamic "delay" {
+ for_each = (
+ fault_injection_policy.value.delay == null
+ ? []
+ : [fault_injection_policy.value.delay]
+ )
+ content {
+ percentage = delay.value.percentage
+ fixed_delay {
+ nanos = delay.value.fixed.nanos
+ seconds = delay.value.fixed.seconds
+ }
+ }
+ }
+ }
+ }
+ dynamic "request_mirror_policy" {
+ for_each = (
+ route_action.value.request_mirror_backend == null
+ ? []
+ : [""]
+ )
+ content {
+ backend_service = lookup(
+ local.backend_ids,
+ route_action.value.request_mirror_backend,
+ route_action.value.request_mirror_backend
+ )
+ }
+ }
+ dynamic "retry_policy" {
+ for_each = (
+ route_action.value.retry_policy == null
+ ? []
+ : [route_action.value.retry_policy]
+ )
+ content {
+ num_retries = retry_policy.value.num_retries
+ retry_conditions = retry_policy.value.retry_conditions
+ dynamic "per_try_timeout" {
+ for_each = (
+ retry_policy.value.per_try_timeout == null
+ ? []
+ : [retry_policy.value.per_try_timeout]
+ )
+ content {
+ nanos = per_try_timeout.value.nanos
+ seconds = per_try_timeout.value.seconds
+ }
+ }
+ }
+ }
+ dynamic "timeout" {
+ for_each = (
+ route_action.value.timeout == null
+ ? []
+ : [route_action.value.timeout]
+ )
+ content {
+ nanos = timeout.value.nanos
+ seconds = timeout.value.seconds
+ }
+ }
+ dynamic "url_rewrite" {
+ for_each = (
+ route_action.value.url_rewrite == null
+ ? []
+ : [route_action.value.url_rewrite]
+ )
+ content {
+ host_rewrite = url_rewrite.value.host
+ path_prefix_rewrite = url_rewrite.value.path_prefix
+ }
+ }
+ dynamic "weighted_backend_services" {
+ for_each = coalesce(
+ route_action.value.weighted_backend_services, {}
+ )
+ iterator = service
+ content {
+ backend_service = lookup(
+ local.backend_ids, service.key, service.key
+ )
+ weight = service.value.weight
+ dynamic "header_action" {
+ for_each = (
+ service.value.header_action == null
+ ? []
+ : [service.value.header_action]
+ )
+ iterator = h
+ content {
+ request_headers_to_remove = h.value.request_remove
+ response_headers_to_remove = h.value.response_remove
+ dynamic "request_headers_to_add" {
+ for_each = coalesce(h.value.request_add, {})
+ content {
+ header_name = request_headers_to_add.key
+ header_value = request_headers_to_add.value.value
+ replace = request_headers_to_add.value.replace
+ }
+ }
+ dynamic "response_headers_to_add" {
+ for_each = coalesce(h.value.response_add, {})
+ content {
+ header_name = response_headers_to_add.key
+ header_value = response_headers_to_add.value.value
+ replace = response_headers_to_add.value.replace
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ dynamic "default_url_redirect" {
+ for_each = (
+ var.urlmap_config.default_url_redirect == null
+ ? []
+ : [var.urlmap_config.default_url_redirect]
+ )
+ iterator = r
+ content {
+ host_redirect = r.value.host
+ https_redirect = r.value.https
+ path_redirect = r.value.path
+ prefix_redirect = r.value.prefix
+ redirect_response_code = r.value.response_code
+ strip_query = r.value.strip_query
+ }
+ }
+
+ dynamic "header_action" {
+ for_each = (
+ var.urlmap_config.header_action == null
+ ? []
+ : [var.urlmap_config.header_action]
+ )
+ iterator = h
+ content {
+ request_headers_to_remove = h.value.request_remove
+ response_headers_to_remove = h.value.response_remove
+ dynamic "request_headers_to_add" {
+ for_each = coalesce(h.value.request_add, {})
+ content {
+ header_name = request_headers_to_add.key
+ header_value = request_headers_to_add.value.value
+ replace = request_headers_to_add.value.replace
+ }
+ }
+ dynamic "response_headers_to_add" {
+ for_each = coalesce(h.value.response_add, {})
+ content {
+ header_name = response_headers_to_add.key
+ header_value = response_headers_to_add.value.value
+ replace = response_headers_to_add.value.replace
+ }
+ }
+ }
+ }
+
+ dynamic "host_rule" {
+ for_each = coalesce(var.urlmap_config.host_rules, [])
+ iterator = r
+ content {
+ hosts = r.value.hosts
+ path_matcher = r.value.path_matcher
+ description = r.value.description
+ }
+ }
+
+ dynamic "path_matcher" {
+ for_each = coalesce(var.urlmap_config.path_matchers, {})
+ iterator = m
+ content {
+ default_service = m.value.default_service == null ? null : lookup(
+ local.backend_ids, m.value.default_service, m.value.default_service
+ )
+ description = m.value.description
+ name = m.key
+ dynamic "default_route_action" {
+ for_each = (
+ m.value.default_route_action == null
+ ? []
+ : [m.value.default_route_action]
+ )
+ iterator = route_action
+ content {
+ dynamic "cors_policy" {
+ for_each = (
+ route_action.value.cors_policy == null
+ ? []
+ : [route_action.value.cors_policy]
+ )
+ content {
+ allow_credentials = cors_policy.value.allow_credentials
+ allow_headers = cors_policy.value.allow_headers
+ allow_methods = cors_policy.value.allow_methods
+ allow_origin_regexes = cors_policy.value.allow_origin_regexes
+ allow_origins = cors_policy.value.allow_origins
+ disabled = cors_policy.value.disabled
+ expose_headers = cors_policy.value.expose_headers
+ max_age = cors_policy.value.max_age
+ }
+ }
+ dynamic "fault_injection_policy" {
+ for_each = (
+ route_action.value.fault_injection_policy == null
+ ? []
+ : [route_action.value.fault_injection_policy]
+ )
+ content {
+ dynamic "abort" {
+ for_each = (
+ fault_injection_policy.value.abort == null
+ ? []
+ : [fault_injection_policy.value.abort]
+ )
+ content {
+ http_status = abort.value.status
+ percentage = abort.value.percentage
+ }
+ }
+ dynamic "delay" {
+ for_each = (
+ fault_injection_policy.value.delay == null
+ ? []
+ : [fault_injection_policy.value.delay]
+ )
+ content {
+ percentage = delay.value.percentage
+ fixed_delay {
+ nanos = delay.value.fixed.nanos
+ seconds = delay.value.fixed.seconds
+ }
+ }
+ }
+ }
+ }
+ dynamic "request_mirror_policy" {
+ for_each = (
+ route_action.value.request_mirror_backend == null
+ ? []
+ : [""]
+ )
+ content {
+ backend_service = lookup(
+ local.backend_ids,
+ route_action.value.request_mirror_backend,
+ route_action.value.request_mirror_backend
+ )
+ }
+ }
+ dynamic "retry_policy" {
+ for_each = (
+ route_action.value.retry_policy == null
+ ? []
+ : [route_action.value.retry_policy]
+ )
+ content {
+ num_retries = retry_policy.value.num_retries
+ retry_conditions = retry_policy.value.retry_conditions
+ dynamic "per_try_timeout" {
+ for_each = (
+ retry_policy.value.per_try_timeout == null
+ ? []
+ : [retry_policy.value.per_try_timeout]
+ )
+ content {
+ nanos = per_try_timeout.value.nanos
+ seconds = per_try_timeout.value.seconds
+ }
+ }
+ }
+ }
+ dynamic "timeout" {
+ for_each = (
+ route_action.value.timeout == null
+ ? []
+ : [route_action.value.timeout]
+ )
+ content {
+ nanos = timeout.value.nanos
+ seconds = timeout.value.seconds
+ }
+ }
+ dynamic "url_rewrite" {
+ for_each = (
+ route_action.value.url_rewrite == null
+ ? []
+ : [route_action.value.url_rewrite]
+ )
+ content {
+ host_rewrite = url_rewrite.value.host
+ path_prefix_rewrite = url_rewrite.value.path_prefix
+ }
+ }
+ dynamic "weighted_backend_services" {
+ for_each = coalesce(
+ route_action.value.weighted_backend_services, {}
+ )
+ iterator = service
+ content {
+ backend_service = lookup(
+ local.backend_ids, service.key, service.key
+ )
+ weight = service.value.weight
+ dynamic "header_action" {
+ for_each = (
+ service.value.header_action == null
+ ? []
+ : [service.value.header_action]
+ )
+ iterator = h
+ content {
+ request_headers_to_remove = h.value.request_remove
+ response_headers_to_remove = h.value.response_remove
+ dynamic "request_headers_to_add" {
+ for_each = coalesce(h.value.request_add, {})
+ content {
+ header_name = request_headers_to_add.key
+ header_value = request_headers_to_add.value.value
+ replace = request_headers_to_add.value.replace
+ }
+ }
+ dynamic "response_headers_to_add" {
+ for_each = coalesce(h.value.response_add, {})
+ content {
+ header_name = response_headers_to_add.key
+ header_value = response_headers_to_add.value.value
+ replace = response_headers_to_add.value.replace
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ dynamic "default_url_redirect" {
+ for_each = (
+ m.value.default_url_redirect == null
+ ? []
+ : [m.value.default_url_redirect]
+ )
+ content {
+ host_redirect = default_url_redirect.value.host
+ https_redirect = default_url_redirect.value.https
+ path_redirect = default_url_redirect.value.path
+ prefix_redirect = default_url_redirect.value.prefix
+ redirect_response_code = default_url_redirect.value.response_code
+ strip_query = default_url_redirect.value.strip_query
+ }
+ }
+ dynamic "header_action" {
+ for_each = (
+ m.value.header_action == null
+ ? []
+ : [m.value.header_action]
+ )
+ iterator = h
+ content {
+ request_headers_to_remove = h.value.request_remove
+ response_headers_to_remove = h.value.response_remove
+ dynamic "request_headers_to_add" {
+ for_each = coalesce(h.value.request_add, {})
+ content {
+ header_name = request_headers_to_add.key
+ header_value = request_headers_to_add.value.value
+ replace = request_headers_to_add.value.replace
+ }
+ }
+ dynamic "response_headers_to_add" {
+ for_each = coalesce(h.value.response_add, {})
+ content {
+ header_name = response_headers_to_add.key
+ header_value = response_headers_to_add.value.value
+ replace = response_headers_to_add.value.replace
+ }
+ }
+ }
+ }
+ dynamic "path_rule" {
+ for_each = toset(coalesce(m.value.path_rules, []))
+ content {
+ paths = path_rule.value.paths
+ service = path_rule.value.service == null ? null : lookup(
+ local.backend_ids,
+ path_rule.value.service,
+ path_rule.value.service
+ )
+ dynamic "route_action" {
+ for_each = (
+ path_rule.value.route_action == null
+ ? []
+ : [path_rule.value.route_action]
+ )
+ content {
+ dynamic "cors_policy" {
+ for_each = (
+ route_action.value.cors_policy == null
+ ? []
+ : [route_action.value.cors_policy]
+ )
+ content {
+ allow_credentials = cors_policy.value.allow_credentials
+ allow_headers = cors_policy.value.allow_headers
+ allow_methods = cors_policy.value.allow_methods
+ allow_origin_regexes = cors_policy.value.allow_origin_regexes
+ allow_origins = cors_policy.value.allow_origins
+ disabled = cors_policy.value.disabled
+ expose_headers = cors_policy.value.expose_headers
+ max_age = cors_policy.value.max_age
+ }
+ }
+ dynamic "fault_injection_policy" {
+ for_each = (
+ route_action.value.fault_injection_policy == null
+ ? []
+ : [route_action.value.fault_injection_policy]
+ )
+ content {
+ dynamic "abort" {
+ for_each = (
+ fault_injection_policy.value.abort == null
+ ? []
+ : [fault_injection_policy.value.abort]
+ )
+ content {
+ http_status = abort.value.status
+ percentage = abort.value.percentage
+ }
+ }
+ dynamic "delay" {
+ for_each = (
+ fault_injection_policy.value.delay == null
+ ? []
+ : [fault_injection_policy.value.delay]
+ )
+ content {
+ percentage = delay.value.percentage
+ fixed_delay {
+ nanos = delay.value.fixed.nanos
+ seconds = delay.value.fixed.seconds
+ }
+ }
+ }
+ }
+ }
+ dynamic "request_mirror_policy" {
+ for_each = (
+ route_action.value.request_mirror_backend == null
+ ? []
+ : [""]
+ )
+ content {
+ backend_service = lookup(
+ local.backend_ids,
+ route_action.value.request_mirror_backend,
+ route_action.value.request_mirror_backend
+ )
+ }
+ }
+ dynamic "retry_policy" {
+ for_each = (
+ route_action.value.retry_policy == null
+ ? []
+ : [route_action.value.retry_policy]
+ )
+ content {
+ num_retries = retry_policy.value.num_retries
+ retry_conditions = retry_policy.value.retry_conditions
+ dynamic "per_try_timeout" {
+ for_each = (
+ retry_policy.value.per_try_timeout == null
+ ? []
+ : [retry_policy.value.per_try_timeout]
+ )
+ content {
+ nanos = per_try_timeout.value.nanos
+ seconds = per_try_timeout.value.seconds
+ }
+ }
+ }
+ }
+ dynamic "timeout" {
+ for_each = (
+ route_action.value.timeout == null
+ ? []
+ : [route_action.value.timeout]
+ )
+ content {
+ nanos = timeout.value.nanos
+ seconds = timeout.value.seconds
+ }
+ }
+ dynamic "url_rewrite" {
+ for_each = (
+ route_action.value.url_rewrite == null
+ ? []
+ : [route_action.value.url_rewrite]
+ )
+ content {
+ host_rewrite = url_rewrite.value.host
+ path_prefix_rewrite = url_rewrite.value.path_prefix
+ }
+ }
+ dynamic "weighted_backend_services" {
+ for_each = coalesce(
+ route_action.value.weighted_backend_services, {}
+ )
+ iterator = service
+ content {
+ backend_service = lookup(
+ local.backend_ids, service.key, service.key
+ )
+ weight = service.value.weight
+ dynamic "header_action" {
+ for_each = (
+ service.value.header_action == null
+ ? []
+ : [service.value.header_action]
+ )
+ iterator = h
+ content {
+ request_headers_to_remove = h.value.request_remove
+ response_headers_to_remove = h.value.response_remove
+ dynamic "request_headers_to_add" {
+ for_each = coalesce(h.value.request_add, {})
+ content {
+ header_name = request_headers_to_add.key
+ header_value = request_headers_to_add.value.value
+ replace = request_headers_to_add.value.replace
+ }
+ }
+ dynamic "response_headers_to_add" {
+ for_each = coalesce(h.value.response_add, {})
+ content {
+ header_name = response_headers_to_add.key
+ header_value = response_headers_to_add.value.value
+ replace = response_headers_to_add.value.replace
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ dynamic "url_redirect" {
+ for_each = (
+ path_rule.value.url_redirect == null
+ ? []
+ : [path_rule.value.url_redirect]
+ )
+ content {
+ host_redirect = url_redirect.value.host
+ https_redirect = url_redirect.value.https
+ path_redirect = url_redirect.value.path
+ prefix_redirect = url_redirect.value.prefix
+ redirect_response_code = url_redirect.value.response_code
+ strip_query = url_redirect.value.strip_query
+ }
+ }
+ }
+ }
+ dynamic "route_rules" {
+ for_each = toset(coalesce(m.value.route_rules, []))
+ content {
+ priority = route_rules.value.priority
+ service = route_rules.value.service == null ? null : lookup(
+ local.backend_ids,
+ route_rules.value.service,
+ route_rules.value.service
+ )
+ dynamic "header_action" {
+ for_each = (
+ route_rules.value.header_action == null
+ ? []
+ : [route_rules.value.header_action]
+ )
+ iterator = h
+ content {
+ request_headers_to_remove = h.value.request_remove
+ response_headers_to_remove = h.value.response_remove
+ dynamic "request_headers_to_add" {
+ for_each = coalesce(h.value.request_add, {})
+ content {
+ header_name = request_headers_to_add.key
+ header_value = request_headers_to_add.value.value
+ replace = request_headers_to_add.value.replace
+ }
+ }
+ dynamic "response_headers_to_add" {
+ for_each = coalesce(h.value.response_add, {})
+ content {
+ header_name = response_headers_to_add.key
+ header_value = response_headers_to_add.value.value
+ replace = response_headers_to_add.value.replace
+ }
+ }
+ }
+ }
+ dynamic "match_rules" {
+ for_each = toset(coalesce(route_rules.value.match_rules, []))
+ content {
+ ignore_case = match_rules.value.ignore_case
+ full_path_match = (
+ try(match_rules.value.path.type, null) == "full"
+ ? match_rules.value.path.value
+ : null
+ )
+ prefix_match = (
+ try(match_rules.value.path.type, null) == "prefix"
+ ? match_rules.value.path.value
+ : null
+ )
+ regex_match = (
+ try(match_rules.value.path.type, null) == "regex"
+ ? match_rules.value.path.value
+ : null
+ )
+ dynamic "header_matches" {
+ for_each = toset(coalesce(match_rules.value.headers, []))
+ iterator = h
+ content {
+ header_name = h.value.name
+ exact_match = h.value.type == "exact" ? h.value.value : null
+ invert_match = h.value.invert_match
+ prefix_match = h.value.type == "prefix" ? h.value.value : null
+ present_match = h.value.type == "present" ? h.value.value : null
+ regex_match = h.value.type == "regex" ? h.value.value : null
+ suffix_match = h.value.type == "suffix" ? h.value.value : null
+ dynamic "range_match" {
+ for_each = (
+ h.value.type != "range" || h.value.range_value == null
+ ? []
+ : [""]
+ )
+ content {
+ range_end = h.value.range_value.end
+ range_start = h.value.range_value.start
+ }
+ }
+ }
+ }
+ dynamic "metadata_filters" {
+ for_each = toset(coalesce(match_rules.value.metadata_filters, []))
+ iterator = m
+ content {
+ filter_match_criteria = (
+ m.value.match_all ? "MATCH_ALL" : "MATCH_ANY"
+ )
+ dynamic "filter_labels" {
+ for_each = m.value.labels
+ content {
+ name = filter_labels.key
+ value = filter_labels.value
+ }
+ }
+ }
+ }
+ dynamic "query_parameter_matches" {
+ for_each = toset(coalesce(match_rules.value.query_params, []))
+ iterator = q
+ content {
+ name = q.value.name
+ exact_match = (
+ q.value.type == "exact" ? q.value.value : null
+ )
+ present_match = (
+ q.value.type == "present" ? q.value.value : null
+ )
+ regex_match = (
+ q.value.type == "regex" ? q.value.value : null
+ )
+ }
+ }
+ }
+ }
+ dynamic "route_action" {
+ for_each = (
+ route_rules.value.route_action == null
+ ? []
+ : [route_rules.value.route_action]
+ )
+ content {
+ dynamic "cors_policy" {
+ for_each = (
+ route_action.value.cors_policy == null
+ ? []
+ : [route_action.value.cors_policy]
+ )
+ content {
+ allow_credentials = cors_policy.value.allow_credentials
+ allow_headers = cors_policy.value.allow_headers
+ allow_methods = cors_policy.value.allow_methods
+ allow_origin_regexes = cors_policy.value.allow_origin_regexes
+ allow_origins = cors_policy.value.allow_origins
+ disabled = cors_policy.value.disabled
+ expose_headers = cors_policy.value.expose_headers
+ max_age = cors_policy.value.max_age
+ }
+ }
+ dynamic "fault_injection_policy" {
+ for_each = (
+ route_action.value.fault_injection_policy == null
+ ? []
+ : [route_action.value.fault_injection_policy]
+ )
+ content {
+ dynamic "abort" {
+ for_each = (
+ fault_injection_policy.value.abort == null
+ ? []
+ : [fault_injection_policy.value.abort]
+ )
+ content {
+ http_status = abort.value.status
+ percentage = abort.value.percentage
+ }
+ }
+ dynamic "delay" {
+ for_each = (
+ fault_injection_policy.value.delay == null
+ ? []
+ : [fault_injection_policy.value.delay]
+ )
+ content {
+ percentage = delay.value.percentage
+ fixed_delay {
+ nanos = delay.value.fixed.nanos
+ seconds = delay.value.fixed.seconds
+ }
+ }
+ }
+ }
+ }
+ dynamic "request_mirror_policy" {
+ for_each = (
+ route_action.value.request_mirror_backend == null
+ ? []
+ : [""]
+ )
+ content {
+ backend_service = lookup(
+ local.backend_ids,
+ route_action.value.request_mirror_backend,
+ route_action.value.request_mirror_backend
+ )
+ }
+ }
+ dynamic "retry_policy" {
+ for_each = (
+ route_action.value.retry_policy == null
+ ? []
+ : [route_action.value.retry_policy]
+ )
+ content {
+ num_retries = retry_policy.value.num_retries
+ retry_conditions = retry_policy.value.retry_conditions
+ dynamic "per_try_timeout" {
+ for_each = (
+ retry_policy.value.per_try_timeout == null
+ ? []
+ : [retry_policy.value.per_try_timeout]
+ )
+ content {
+ nanos = per_try_timeout.value.nanos
+ seconds = per_try_timeout.value.seconds
+ }
+ }
+ }
+ }
+ dynamic "timeout" {
+ for_each = (
+ route_action.value.timeout == null
+ ? []
+ : [route_action.value.timeout]
+ )
+ content {
+ nanos = timeout.value.nanos
+ seconds = timeout.value.seconds
+ }
+ }
+ dynamic "url_rewrite" {
+ for_each = (
+ route_action.value.url_rewrite == null
+ ? []
+ : [route_action.value.url_rewrite]
+ )
+ content {
+ host_rewrite = url_rewrite.value.host
+ path_prefix_rewrite = url_rewrite.value.path_prefix
+ }
+ }
+ dynamic "weighted_backend_services" {
+ for_each = coalesce(
+ route_action.value.weighted_backend_services, {}
+ )
+ iterator = service
+ content {
+ backend_service = lookup(
+ local.backend_ids, service.key, service.key
+ )
+ weight = service.value.weight
+ dynamic "header_action" {
+ for_each = (
+ service.value.header_action == null
+ ? []
+ : [service.value.header_action]
+ )
+ iterator = h
+ content {
+ request_headers_to_remove = h.value.request_remove
+ response_headers_to_remove = h.value.response_remove
+ dynamic "request_headers_to_add" {
+ for_each = coalesce(h.value.request_add, {})
+ content {
+ header_name = request_headers_to_add.key
+ header_value = request_headers_to_add.value.value
+ replace = request_headers_to_add.value.replace
+ }
+ }
+ dynamic "response_headers_to_add" {
+ for_each = coalesce(h.value.response_add, {})
+ content {
+ header_name = response_headers_to_add.key
+ header_value = response_headers_to_add.value.value
+ replace = response_headers_to_add.value.replace
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ dynamic "url_redirect" {
+ for_each = (
+ route_rules.value.default_url_redirect == null
+ ? []
+ : [route_rules.value.default_url_redirect]
+ )
+ content {
+ host_redirect = url_redirect.value.host
+ https_redirect = url_redirect.value.https
+ path_redirect = url_redirect.value.path
+ prefix_redirect = url_redirect.value.prefix
+ redirect_response_code = url_redirect.value.response_code
+ strip_query = url_redirect.value.strip_query
+ }
+ }
+ }
+ }
+ }
+ }
+
+ dynamic "test" {
+ for_each = toset(coalesce(var.urlmap_config.test, []))
+ content {
+ host = test.value.host
+ path = test.value.path
+ service = test.value.service
+ description = test.value.description
+ }
+ }
+
+}
diff --git a/modules/net-glb/variables-backend-service.tf b/modules/net-glb/variables-backend-service.tf
new file mode 100644
index 0000000000..1d70d8415d
--- /dev/null
+++ b/modules/net-glb/variables-backend-service.tf
@@ -0,0 +1,150 @@
+/**
+ * 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:file:description Backend services variables.
+
+variable "backend_service_configs" {
+ description = "Backend service level configuration."
+ type = map(object({
+ affinity_cookie_ttl_sec = optional(number)
+ compression_mode = optional(string)
+ connection_draining_timeout_sec = optional(number)
+ custom_request_headers = optional(list(string))
+ custom_response_headers = optional(list(string))
+ enable_cdn = optional(bool)
+ health_checks = optional(list(string), ["default"])
+ log_sample_rate = optional(number)
+ port_name = optional(string)
+ project_id = optional(string)
+ protocol = optional(string)
+ security_policy = optional(string)
+ session_affinity = optional(string)
+ timeout_sec = optional(number)
+ backends = list(object({
+ # group renamed to backend
+ backend = string
+ balancing_mode = optional(string, "UTILIZATION")
+ capacity_scaler = optional(number, 1)
+ description = optional(string, "Terraform managed.")
+ failover = optional(bool, false)
+ max_connections = optional(object({
+ per_endpoint = optional(number)
+ per_group = optional(number)
+ per_instance = optional(number)
+ }))
+ max_rate = optional(object({
+ per_endpoint = optional(number)
+ per_group = optional(number)
+ per_instance = optional(number)
+ }))
+ max_utilization = optional(number)
+ }))
+ cdn_policy = optional(object({
+ cache_mode = optional(string)
+ client_ttl = optional(number)
+ default_ttl = optional(number)
+ max_ttl = optional(number)
+ negative_caching = optional(bool)
+ serve_while_stale = optional(bool)
+ signed_url_cache_max_age_sec = optional(number)
+ cache_key_policy = optional(object({
+ include_host = optional(bool)
+ include_named_cookies = optional(list(string))
+ include_protocol = optional(bool)
+ include_query_string = optional(bool)
+ query_string_blacklist = optional(list(string))
+ query_string_whitelist = optional(list(string))
+ }))
+ negative_caching_policy = optional(object({
+ code = optional(number)
+ ttl = optional(number)
+ }))
+ }))
+ circuit_breakers = optional(object({
+ max_connections = optional(number)
+ max_pending_requests = optional(number)
+ max_requests = optional(number)
+ max_requests_per_connection = optional(number)
+ max_retries = optional(number)
+ connect_timeout = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ }))
+ consistent_hash = optional(object({
+ http_header_name = optional(string)
+ minimum_ring_size = optional(number)
+ http_cookie = optional(object({
+ name = optional(string)
+ path = optional(string)
+ ttl = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ }))
+ }))
+ iap_config = optional(object({
+ oauth2_client_id = string
+ oauth2_client_secret = string
+ oauth2_client_secret_sha256 = optional(string)
+ }))
+ outlier_detection = optional(object({
+ consecutive_errors = optional(number)
+ consecutive_gateway_failure = optional(number)
+ enforcing_consecutive_errors = optional(number)
+ enforcing_consecutive_gateway_failure = optional(number)
+ enforcing_success_rate = optional(number)
+ max_ejection_percent = optional(number)
+ success_rate_minimum_hosts = optional(number)
+ success_rate_request_volume = optional(number)
+ success_rate_stdev_factor = optional(number)
+ base_ejection_time = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ interval = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ }))
+ security_settings = optional(object({
+ client_tls_policy = string
+ subject_alt_names = list(string)
+ }))
+ }))
+ default = {}
+ nullable = false
+ validation {
+ condition = contains(
+ [
+ "-", "ROUND_ROBIN", "LEAST_REQUEST", "RING_HASH",
+ "RANDOM", "ORIGINAL_DESTINATION", "MAGLEV"
+ ],
+ try(var.backend_service_configs.locality_lb_policy, "-")
+ )
+ error_message = "Invalid locality lb policy value."
+ }
+ validation {
+ condition = contains(
+ [
+ "NONE", "CLIENT_IP", "CLIENT_IP_NO_DESTINATION",
+ "CLIENT_IP_PORT_PROTO", "CLIENT_IP_PROTO"
+ ],
+ try(var.backend_service_configs.session_affinity, "NONE")
+ )
+ error_message = "Invalid session affinity value."
+ }
+}
diff --git a/modules/net-glb/variables-health-check.tf b/modules/net-glb/variables-health-check.tf
new file mode 100644
index 0000000000..bfb8b67b26
--- /dev/null
+++ b/modules/net-glb/variables-health-check.tf
@@ -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.
+ */
+
+# tfdoc:file:description Health check variable.
+
+variable "health_check_configs" {
+ description = "Optional auto-created health check configurations, use the output self-link to set it in the auto healing policy. Refer to examples for usage."
+ type = map(object({
+ check_interval_sec = optional(number)
+ description = optional(string, "Terraform managed.")
+ enable_logging = optional(bool, false)
+ healthy_threshold = optional(number)
+ project_id = optional(string)
+ timeout_sec = optional(number)
+ unhealthy_threshold = optional(number)
+ grpc = optional(object({
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ service_name = optional(string)
+ }))
+ http = optional(object({
+ host = optional(string)
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request_path = optional(string)
+ response = optional(string)
+ }))
+ http2 = optional(object({
+ host = optional(string)
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request_path = optional(string)
+ response = optional(string)
+ }))
+ https = optional(object({
+ host = optional(string)
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request_path = optional(string)
+ response = optional(string)
+ }))
+ tcp = optional(object({
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request = optional(string)
+ response = optional(string)
+ }))
+ ssl = optional(object({
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request = optional(string)
+ response = optional(string)
+ }))
+ }))
+ default = {
+ default = {
+ http = {
+ port_specification = "USE_SERVING_PORT"
+ }
+ }
+ }
+ validation {
+ condition = alltrue([
+ for k, v in var.health_check_configs : (
+ (try(v.grpc, null) == null ? 0 : 1) +
+ (try(v.http, null) == null ? 0 : 1) +
+ (try(v.tcp, null) == null ? 0 : 1) <= 1
+ )
+ ])
+ error_message = "At most one health check type can be configured at a time."
+ }
+ validation {
+ condition = alltrue(flatten([
+ for k, v in var.health_check_configs : [
+ for kk, vv in v : contains([
+ "-", "USE_FIXED_PORT", "USE_NAMED_PORT", "USE_SERVING_PORT"
+ ], coalesce(try(vv.port_specification, null), "-"))
+ ]
+ ]))
+ error_message = "Invalid 'port_specification' value. Supported values are 'USE_FIXED_PORT', 'USE_NAMED_PORT', 'USE_SERVING_PORT'."
+ }
+}
diff --git a/modules/net-glb/variables-urlmap.tf b/modules/net-glb/variables-urlmap.tf
new file mode 100644
index 0000000000..e4b72dfec4
--- /dev/null
+++ b/modules/net-glb/variables-urlmap.tf
@@ -0,0 +1,372 @@
+/**
+ * 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:file:description URLmap variable.
+
+variable "urlmap_config" {
+ description = "The URL map configuration."
+ type = object({
+ default_route_action = optional(object({
+ request_mirror_backend = optional(string)
+ cors_policy = optional(object({
+ allow_credentials = optional(bool)
+ allow_headers = optional(string)
+ allow_methods = optional(string)
+ allow_origin_regexes = list(string)
+ allow_origins = list(string)
+ disabled = optional(bool)
+ expose_headers = optional(string)
+ max_age = optional(string)
+ }))
+ fault_injection_policy = optional(object({
+ abort = optional(object({
+ percentage = number
+ status = number
+ }))
+ delay = optional(object({
+ fixed = object({
+ seconds = number
+ nanos = number
+ })
+ percentage = number
+ }))
+ }))
+ retry_policy = optional(object({
+ num_retries = number
+ retry_conditions = optional(list(string))
+ per_try_timeout = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ }))
+ timeout = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ url_rewrite = optional(object({
+ host = optional(string)
+ path_prefix = optional(string)
+ }))
+ weighted_backend_services = optional(map(object({
+ weight = number
+ header_action = optional(object({
+ request_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ request_remove = optional(list(string))
+ response_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ response_remove = optional(list(string))
+ }))
+ })))
+ }))
+ default_service = optional(string)
+ default_url_redirect = optional(object({
+ host = optional(string)
+ https = optional(bool)
+ path = optional(string)
+ prefix = optional(string)
+ response_code = optional(string)
+ strip_query = optional(bool)
+ }))
+ header_action = optional(object({
+ request_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ request_remove = optional(list(string))
+ response_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ response_remove = optional(list(string))
+ }))
+ host_rules = optional(list(object({
+ hosts = list(string)
+ path_matcher = string
+ description = optional(string)
+ })))
+ path_matchers = optional(map(object({
+ description = optional(string)
+ default_route_action = optional(object({
+ request_mirror_backend = optional(string)
+ cors_policy = optional(object({
+ allow_credentials = optional(bool)
+ allow_headers = optional(string)
+ allow_methods = optional(string)
+ allow_origin_regexes = list(string)
+ allow_origins = list(string)
+ disabled = optional(bool)
+ expose_headers = optional(string)
+ max_age = optional(string)
+ }))
+ fault_injection_policy = optional(object({
+ abort = optional(object({
+ percentage = number
+ status = number
+ }))
+ delay = optional(object({
+ fixed = object({
+ seconds = number
+ nanos = number
+ })
+ percentage = number
+ }))
+ }))
+ retry_policy = optional(object({
+ num_retries = number
+ retry_conditions = optional(list(string))
+ per_try_timeout = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ }))
+ timeout = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ url_rewrite = optional(object({
+ host = optional(string)
+ path_prefix = optional(string)
+ }))
+ weighted_backend_services = optional(map(object({
+ weight = number
+ header_action = optional(object({
+ request_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ request_remove = optional(list(string))
+ response_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ response_remove = optional(list(string))
+ }))
+ })))
+ }))
+ default_service = optional(string)
+ default_url_redirect = optional(object({
+ host = optional(string)
+ https = optional(bool)
+ path = optional(string)
+ prefix = optional(string)
+ response_code = optional(string)
+ strip_query = optional(bool)
+ }))
+ header_action = optional(object({
+ request_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ request_remove = optional(list(string))
+ response_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ response_remove = optional(list(string))
+ }))
+ path_rules = optional(list(object({
+ paths = list(string)
+ service = optional(string)
+ route_action = optional(object({
+ request_mirror_backend = optional(string)
+ cors_policy = optional(object({
+ allow_credentials = optional(bool)
+ allow_headers = optional(string)
+ allow_methods = optional(string)
+ allow_origin_regexes = list(string)
+ allow_origins = list(string)
+ disabled = optional(bool)
+ expose_headers = optional(string)
+ max_age = optional(string)
+ }))
+ fault_injection_policy = optional(object({
+ abort = optional(object({
+ percentage = number
+ status = number
+ }))
+ delay = optional(object({
+ fixed = object({
+ seconds = number
+ nanos = number
+ })
+ percentage = number
+ }))
+ }))
+ retry_policy = optional(object({
+ num_retries = number
+ retry_conditions = optional(list(string))
+ per_try_timeout = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ }))
+ timeout = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ url_rewrite = optional(object({
+ host = optional(string)
+ path_prefix = optional(string)
+ }))
+ weighted_backend_services = optional(map(object({
+ weight = number
+ header_action = optional(object({
+ request_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ request_remove = optional(list(string))
+ response_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ response_remove = optional(list(string))
+ }))
+ })))
+ }))
+ url_redirect = optional(object({
+ host = optional(string)
+ https = optional(bool)
+ path = optional(string)
+ prefix = optional(string)
+ response_code = optional(string)
+ strip_query = optional(bool)
+ }))
+ })))
+ route_rules = optional(list(object({
+ priority = number
+ service = optional(string)
+ header_action = optional(object({
+ request_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ request_remove = optional(list(string))
+ response_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ response_remove = optional(list(string))
+ }))
+ match_rules = optional(list(object({
+ ignore_case = optional(bool, false)
+ headers = optional(list(object({
+ name = string
+ invert_match = optional(bool, false)
+ type = optional(string, "present") # exact, prefix, suffix, regex, present, range
+ value = optional(string)
+ range_value = optional(object({
+ end = string
+ start = string
+ }))
+ })))
+ metadata_filters = optional(list(object({
+ labels = map(string)
+ match_all = bool # MATCH_ANY, MATCH_ALL
+ })))
+ path = optional(object({
+ value = string
+ type = optional(string, "prefix") # full, prefix, regex
+ }))
+ query_params = optional(list(object({
+ name = string
+ value = string
+ type = optional(string, "present") # exact, present, regex
+ })))
+ })))
+ route_action = optional(object({
+ request_mirror_backend = optional(string)
+ cors_policy = optional(object({
+ allow_credentials = optional(bool)
+ allow_headers = optional(string)
+ allow_methods = optional(string)
+ allow_origin_regexes = list(string)
+ allow_origins = list(string)
+ disabled = optional(bool)
+ expose_headers = optional(string)
+ max_age = optional(string)
+ }))
+ fault_injection_policy = optional(object({
+ abort = optional(object({
+ percentage = number
+ status = number
+ }))
+ delay = optional(object({
+ fixed = object({
+ seconds = number
+ nanos = number
+ })
+ percentage = number
+ }))
+ }))
+ retry_policy = optional(object({
+ num_retries = number
+ retry_conditions = optional(list(string))
+ per_try_timeout = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ }))
+ timeout = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ url_rewrite = optional(object({
+ host = optional(string)
+ path_prefix = optional(string)
+ }))
+ weighted_backend_services = optional(map(object({
+ weight = number
+ header_action = optional(object({
+ request_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ request_remove = optional(list(string))
+ response_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ response_remove = optional(list(string))
+ }))
+ })))
+ }))
+ url_redirect = optional(object({
+ host = optional(string)
+ https = optional(bool)
+ path = optional(string)
+ prefix = optional(string)
+ response_code = optional(string)
+ strip_query = optional(bool)
+ }))
+ })))
+ })))
+ test = optional(list(object({
+ host = string
+ path = string
+ service = string
+ description = optional(string)
+ })))
+ })
+ default = {
+ default_service = "default"
+ }
+}
diff --git a/modules/net-glb/variables.tf b/modules/net-glb/variables.tf
index a53c113b3e..72e6c0c402 100644
--- a/modules/net-glb/variables.tf
+++ b/modules/net-glb/variables.tf
@@ -14,217 +14,219 @@
* limitations under the License.
*/
-variable "name" {
- description = "Load balancer name."
+variable "address" {
+ description = "Optional IP address used for the forwarding rule."
type = string
+ default = null
}
-variable "project_id" {
- description = "Project id."
- type = string
+variable "backend_buckets_config" {
+ description = "Backend buckets configuration."
+ type = map(object({
+ bucket_name = string
+ compression_mode = optional(string)
+ custom_response_headers = optional(list(string))
+ description = optional(string)
+ edge_security_policy = optional(string)
+ enable_cdn = optional(bool)
+ cdn_policy = optional(object({
+ bypass_cache_on_request_headers = optional(list(string))
+ cache_mode = optional(string)
+ client_ttl = optional(number)
+ default_ttl = optional(number)
+ max_ttl = optional(number)
+ negative_caching = optional(bool)
+ request_coalescing = optional(bool)
+ serve_while_stale = optional(bool)
+ signed_url_cache_max_age_sec = optional(number)
+ cache_key_policy = optional(object({
+ include_http_headers = optional(list(string))
+ query_string_whitelist = optional(list(string))
+ }))
+ negative_caching_policy = optional(object({
+ code = optional(number)
+ ttl = optional(number)
+ }))
+ }))
+ }))
+ default = {}
+ nullable = true
}
-variable "health_checks_config_defaults" {
- description = "Auto-created health check default configuration."
- type = object({
- type = string # http https tcp ssl http2
- check = map(any) # actual health check block attributes
- options = map(number) # interval, thresholds, timeout
- logging = bool
- })
- default = {
- type = "http"
- logging = false
- options = {}
- check = {
- port_specification = "USE_SERVING_PORT"
- }
- }
+variable "description" {
+ description = "Optional description used for resources."
+ type = string
+ default = "Terraform managed."
}
-variable "health_checks_config" {
- description = "Custom health checks configuration."
+variable "group_configs" {
+ description = "Optional unmanaged groups to create. Can be referenced in backends via key or outputs."
type = map(object({
- type = string # http https tcp ssl http2
- check = map(any) # actual health check block attributes
- options = map(number) # interval, thresholds, timeout
- logging = bool
+ zone = string
+ instances = optional(list(string), [])
+ named_ports = optional(map(number), {})
+ project_id = optional(string)
}))
- default = {}
+ default = {}
+ nullable = false
}
-variable "backend_services_config" {
- description = "The backends services configuration."
- type = map(object({
- enable_cdn = bool
-
- cdn_config = object({
- cache_mode = string
- client_ttl = number
- default_ttl = number
- max_ttl = number
- negative_caching = bool
- negative_caching_policy = map(number)
- serve_while_stale = bool
- signed_url_cache_max_age_sec = string
- })
-
- bucket_config = object({
- bucket_name = string
- options = object({
- custom_response_headers = list(string)
- })
- })
-
- group_config = object({
- backends = list(object({
- group = string # IG or NEG FQDN address
- options = object({
- balancing_mode = string # Can be UTILIZATION, RATE, CONNECTION
- capacity_scaler = number # Valid range is [0.0,1.0]
- max_connections = number
- max_connections_per_instance = number
- max_connections_per_endpoint = number
- max_rate = number
- max_rate_per_instance = number
- max_rate_per_endpoint = number
- max_utilization = number
- })
- }))
-
- # Optional health check ids for backend service groups.
- # Will lookup for ids in health_chacks_config first,
- # then will use the id as is. If no ids are defined
- # at all (null, []) health_checks_config_defaults is used
- health_checks = list(string)
-
- log_config = object({
- enable = bool
- sample_rate = number # must be in [0, 1]
- })
-
- options = object({
- affinity_cookie_ttl_sec = number
- custom_request_headers = list(string)
- custom_response_headers = list(string)
- connection_draining_timeout_sec = number
- load_balancing_scheme = string # only EXTERNAL (default) makes sense here
- locality_lb_policy = string
- port_name = string
- protocol = string
- security_policy = string
- session_affinity = string
- timeout_sec = number
-
- circuits_breakers = object({
- max_requests_per_connection = number # Set to 1 to disable keep-alive
- max_connections = number # Defaults to 1024
- max_pending_requests = number # Defaults to 1024
- max_requests = number # Defaults to 1024
- max_retries = number # Defaults to 3
- })
-
- consistent_hash = object({
- http_header_name = string
- minimum_ring_size = string
- http_cookie = object({
- name = string
- path = string
- ttl = object({
- seconds = number
- nanos = number
- })
- })
- })
+variable "https_proxy_config" {
+ description = "HTTPS proxy connfiguration."
+ type = object({
+ certificate_map = optional(string)
+ quic_override = optional(string)
+ ssl_policy = optional(string)
+ })
+ default = {}
+ nullable = false
+}
- iap = object({
- oauth2_client_id = string
- oauth2_client_secret = string
- oauth2_client_secret_sha256 = string
- })
- })
- })
- }))
- default = {}
+variable "labels" {
+ description = "Labels set on resources."
+ type = map(string)
+ default = {}
}
-variable "url_map_config" {
- description = "The url-map configuration."
- type = object({
- default_service = string
- default_route_action = any
- default_url_redirect = map(any)
- header_action = any
- host_rules = list(any)
- path_matchers = list(any)
- tests = list(map(string))
- })
- default = null
+variable "name" {
+ description = "Load balancer name."
+ type = string
}
-variable "ssl_certificates_config" {
- description = "The SSL certificate configuration."
+variable "neg_configs" {
+ description = "Optional network endpoint groups to create. Can be referenced in backends via key or outputs."
type = map(object({
- domains = list(string)
- # If unmanaged_config is null, the certificate will be managed
- unmanaged_config = object({
- tls_private_key = string
- tls_self_signed_cert = string
- })
+ description = optional(string)
+ cloudfunction = optional(object({
+ region = string
+ target_function = optional(string)
+ target_urlmask = optional(string)
+ }))
+ cloudrun = optional(object({
+ region = string
+ target_service = optional(object({
+ name = string
+ tag = optional(string)
+ }))
+ target_urlmask = optional(string)
+ }))
+ gce = optional(object({
+ network = string
+ subnetwork = string
+ zone = string
+ # default_port = optional(number)
+ endpoints = optional(map(object({
+ instance = string
+ ip_address = string
+ port = number
+ })))
+ }))
+ hybrid = optional(object({
+ network = string
+ zone = string
+ # re-enable once provider properly support this
+ # default_port = optional(number)
+ endpoints = optional(map(object({
+ ip_address = string
+ port = number
+ })))
+ }))
+ internet = optional(object({
+ use_fqdn = optional(bool, true)
+ # re-enable once provider properly support this
+ # default_port = optional(number)
+ endpoints = optional(map(object({
+ destination = string
+ port = number
+ })))
+ }))
+ psc = optional(object({
+ region = string
+ target_service = string
+ network = optional(string)
+ subnetwork = optional(string)
+ }))
}))
- default = {}
-}
-
-variable "ssl_certificates_config_defaults" {
- description = "The SSL certificate default configuration."
- type = object({
- domains = list(string)
- # If unmanaged_config is null, the certificate will be managed
- unmanaged_config = object({
- tls_private_key = string
- tls_self_signed_cert = string
- })
- })
- default = {
- domains = ["example.com"],
- unmanaged_config = null
+ default = {}
+ nullable = false
+ validation {
+ condition = alltrue([
+ for k, v in var.neg_configs : (
+ (try(v.cloudfunction, null) == null ? 0 : 1) +
+ (try(v.cloudrun, null) == null ? 0 : 1) +
+ (try(v.gce, null) == null ? 0 : 1) +
+ (try(v.hybrid, null) == null ? 0 : 1) +
+ (try(v.internet, null) == null ? 0 : 1) +
+ (try(v.psc, null) == null ? 0 : 1) == 1
+ )
+ ])
+ error_message = "Only one type of NEG can be configured at a time."
+ }
+ validation {
+ condition = alltrue([
+ for k, v in var.neg_configs : (
+ v.cloudrun == null
+ ? true
+ : v.cloudrun.target_urlmask != null || v.cloudrun.target_service != null
+ )
+ ])
+ error_message = "Cloud Run NEGs need either target service or target urlmask defined."
+ }
+ validation {
+ condition = alltrue([
+ for k, v in var.neg_configs : (
+ v.cloudfunction == null
+ ? true
+ : v.cloudfunction.target_urlmask != null || v.cloudfunction.target_function != null
+ )
+ ])
+ error_message = "Cloud Function NEGs need either target function or target urlmask defined."
}
}
-variable "target_proxy_https_config" {
- description = "The HTTPS target proxy configuration."
- type = object({
- ssl_certificates = list(string)
- })
- default = null
+variable "ports" {
+ description = "Optional ports for HTTP load balancer, valid ports are 80 and 8080."
+ type = list(string)
+ default = null
}
-variable "global_forwarding_rule_config" {
- description = "Global forwarding rule configurations."
- type = object({
- ip_protocol = string
- ip_version = string
- load_balancing_scheme = string
- port_range = string
+variable "project_id" {
+ description = "Project id."
+ type = string
+}
- })
- default = {
- load_balancing_scheme = "EXTERNAL"
- ip_protocol = "TCP"
- ip_version = "IPV4"
- # If not specified, 80 for https = false, 443 otherwise
- port_range = null
+variable "protocol" {
+ description = "Protocol supported by this load balancer."
+ type = string
+ default = "HTTP"
+ nullable = false
+ validation {
+ condition = (
+ var.protocol == null || var.protocol == "HTTP" || var.protocol == "HTTPS"
+ )
+ error_message = "Protocol must be HTTP or HTTPS"
}
}
-variable "https" {
- description = "Whether to enable HTTPS."
- type = bool
- default = false
+variable "ssl_certificates" {
+ description = "SSL target proxy certificates (only if protocol is HTTPS) for existing, custom, and managed certificates."
+ type = object({
+ certificate_ids = optional(list(string), [])
+ create_configs = optional(map(object({
+ certificate = string
+ private_key = string
+ })), {})
+ managed_configs = optional(map(object({
+ domains = list(string)
+ description = optional(string)
+ })), {})
+ })
+ default = {}
+ nullable = false
}
-variable "reserve_ip_address" {
- description = "Whether to reserve a static global IP address."
+variable "use_classic_version" {
+ description = "Use classic Global Load Balancer."
type = bool
- default = false
+ default = true
}
diff --git a/modules/net-glb/versions.tf b/modules/net-glb/versions.tf
index 764197882f..90b632f6d4 100644
--- a/modules/net-glb/versions.tf
+++ b/modules/net-glb/versions.tf
@@ -13,15 +13,17 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
+
+
diff --git a/modules/net-ilb-l7/README.md b/modules/net-ilb-l7/README.md
new file mode 100644
index 0000000000..b5862f31e6
--- /dev/null
+++ b/modules/net-ilb-l7/README.md
@@ -0,0 +1,636 @@
+# Internal (HTTP/S) Load Balancer Module
+
+This module allows managing Internal HTTP/HTTPS Load Balancers (L7 ILBs). It's designed to expose the full configuration of the underlying resources, and to facilitate common usage patterns by providing sensible defaults, and optionally managing prerequisite resources like health checks, instance groups, etc.
+
+Due to the complexity of the underlying resources, changes to the configuration that involve recreation of resources are best applied in stages, starting by disabling the configuration in the urlmap that references the resources that neeed recreation, then doing the same for the backend service, etc.
+
+## Examples
+
+- [Minimal Example](#minimal-example)
+- [Cross-project Backend Services](#cross-project-backend-services)
+- [Health Checks](#health-checks)
+- [Instance Groups](#instance-groups)
+- [Network Endpoint Groups](#network-endpoint-groups-negs)
+- [URL Map](#url-map)
+- [SSL Certificates](#ssl-certificates)
+- [Complex Example](#complex-example)
+
+### Minimal Example
+
+An HTTP ILB with a backend service pointing to a GCE instance group:
+
+```hcl
+module "ilb-l7" {
+ source = "./fabric/modules/net-ilb-l7"
+ name = "ilb-test"
+ project_id = var.project_id
+ region = "europe-west1"
+ backend_service_configs = {
+ default = {
+ backends = [{
+ group = "projects/myprj/zones/europe-west1-a/instanceGroups/my-ig"
+ }]
+ }
+ }
+ vpc_config = {
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ }
+}
+# tftest modules=1 resources=5
+```
+
+An HTTPS ILB needs a few additional fields:
+
+```hcl
+module "ilb-l7" {
+ source = "./fabric/modules/net-ilb-l7"
+ name = "ilb-test"
+ project_id = var.project_id
+ region = "europe-west1"
+ backend_service_configs = {
+ default = {
+ backends = [{
+ group = "projects/myprj/zones/europe-west1-a/instanceGroups/my-ig"
+ }]
+ }
+ }
+ protocol = "HTTPS"
+ ssl_certificates = {
+ certificate_ids = [
+ "projects/myprj/regions/europe-west1/sslCertificates/my-cert"
+ ]
+ }
+ vpc_config = {
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ }
+}
+# tftest modules=1 resources=5
+```
+
+### Cross-project backend services
+
+When using Shared VPC, this module also allows configuring [cross-project backend services](https://cloud.google.com/load-balancing/docs/l7-internal/l7-internal-shared-vpc#cross-project):
+
+```hcl
+module "ilb-l7" {
+ source = "./fabric/modules/net-ilb-l7"
+ name = "ilb-test"
+ project_id = "prj-host"
+ region = "europe-west1"
+ backend_service_configs = {
+ default = {
+ project_id = "prj-svc"
+ backends = [{
+ group = "projects/prj-svc/zones/europe-west1-a/instanceGroups/my-ig"
+ }]
+ }
+ }
+ health_check_configs = {
+ default = {
+ project_id = "prj-svc"
+ http = {
+ port_specification = "USE_SERVING_PORT"
+ }
+ }
+ }
+ vpc_config = {
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ }
+}
+# tftest modules=1 resources=5
+```
+
+### Health Checks
+
+You can leverage externally defined health checks for backend services, or have the module create them for you. By default a simple HTTP health check is created, and used in backend services.
+
+Health check configuration is controlled via the `health_check_configs` variable, which behaves in a similar way to other LB modules in this repository.
+
+Defining different health checks fromt he default is very easy. You can for example replace the default HTTP health check with a TCP one and reference it in you backend service:
+
+```hcl
+module "ilb-l7" {
+ source = "./fabric/modules/net-ilb-l7"
+ name = "ilb-test"
+ project_id = var.project_id
+ region = "europe-west1"
+ backend_service_configs = {
+ default = {
+ backends = [{
+ group = "projects/myprj/zones/europe-west1-a/instanceGroups/my-ig"
+ }]
+ health_checks = ["custom-tcp"]
+ }
+ }
+ health_check_configs = {
+ custom-tcp = {
+ tcp = { port = 80 }
+ }
+ }
+ vpc_config = {
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ }
+}
+# tftest modules=1 resources=5
+```
+
+To leverage existing health checks without having the module create them, simply pass their self links to backend services and set the `health_check_configs` variable to an empty map:
+
+```hcl
+module "ilb-l7" {
+ source = "./fabric/modules/net-ilb-l7"
+ name = "ilb-test"
+ project_id = var.project_id
+ region = "europe-west1"
+ backend_service_configs = {
+ default = {
+ backends = [{
+ group = "projects/myprj/zones/europe-west1-a/instanceGroups/my-ig"
+ }]
+ health_checks = ["projects/myprj/global/healthChecks/custom"]
+ }
+ }
+ health_check_configs = {}
+ vpc_config = {
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ }
+}
+# tftest modules=1 resources=4
+```
+
+### Instance Groups
+
+The module can optionally create unmanaged instance groups, which can then be referred to in backends via their key:
+
+```hcl
+module "ilb-l7" {
+ source = "./fabric/modules/net-ilb-l7"
+ name = "ilb-test"
+ project_id = var.project_id
+ region = "europe-west1"
+ backend_service_configs = {
+ default = {
+ port_name = "http"
+ backends = [
+ { group = "default" }
+ ]
+ }
+ }
+ group_configs = {
+ default = {
+ zone = "europe-west1-b"
+ instances = [
+ "projects/myprj/zones/europe-west1-b/instances/vm-a"
+ ]
+ named_ports = { http = 80 }
+ }
+ }
+ vpc_config = {
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ }
+}
+# tftest modules=1 resources=6
+```
+
+### Network Endpoint Groups (NEGs)
+
+Network Endpoint Groups (NEGs) can be used as backends, by passing their id as the backend group in a backends service configuration:
+
+```hcl
+module "ilb-l7" {
+ source = "./fabric/modules/net-ilb-l7"
+ name = "ilb-test"
+ project_id = var.project_id
+ region = "europe-west1"
+ backend_service_configs = {
+ default = {
+ backends = [{
+ balancing_mode = "RATE"
+ group = "projects/myprj/zones/europe-west1-a/networkEndpointGroups/my-neg"
+ max_rate = { per_endpoint = 1 }
+ }]
+ }
+ }
+ vpc_config = {
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ }
+}
+# tftest modules=1 resources=5
+```
+
+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"
+ project_id = var.project_id
+ region = "europe-west1"
+ backend_service_configs = {
+ default = {
+ backends = [{
+ balancing_mode = "RATE"
+ group = "my-neg"
+ max_rate = { per_endpoint = 1 }
+ }]
+ }
+ }
+ neg_configs = {
+ my-neg = {
+ gce = {
+ 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
+ }
+ }
+ }
+ }
+ }
+ vpc_config = {
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ }
+}
+# tftest modules=1 resources=8
+```
+
+Hybrid NEGs are also supported:
+
+```hcl
+module "ilb-l7" {
+ source = "./fabric/modules/net-ilb-l7"
+ name = "ilb-test"
+ project_id = var.project_id
+ region = "europe-west1"
+ backend_service_configs = {
+ default = {
+ backends = [{
+ balancing_mode = "RATE"
+ group = "my-neg"
+ max_rate = { per_endpoint = 1 }
+ }]
+ }
+ }
+ neg_configs = {
+ my-neg = {
+ hybrid = {
+ zone = "europe-west1-b"
+ endpoints = {
+ e-0 = {
+ ip_address = "10.0.0.10"
+ port = 80
+ }
+ }
+ }
+ }
+ }
+ vpc_config = {
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ }
+}
+# tftest modules=1 resources=7
+```
+
+As are serverless NEGs for Cloud Run:
+
+```hcl
+module "ilb-l7" {
+ source = "./fabric/modules/net-ilb-l7"
+ name = "ilb-test"
+ project_id = var.project_id
+ region = "europe-west1"
+ backend_service_configs = {
+ default = {
+ backends = [{
+ balancing_mode = "RATE"
+ group = "my-neg"
+ max_rate = { per_endpoint = 1 }
+ }]
+ }
+ }
+ neg_configs = {
+ my-neg = {
+ cloudrun = {
+ region = "europe-west1"
+ target_service = {
+ name = "my-run-service"
+ }
+ }
+ }
+ }
+ vpc_config = {
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ }
+}
+# tftest modules=1 resources=6
+```
+
+### 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.
+
+The default URL map configuration sets the `default` backend service as the default service for the load balancer as a convenience. Just override the `urlmap_config` variable to change the default behaviour:
+
+```hcl
+module "ilb-l7" {
+ source = "./fabric/modules/net-ilb-l7"
+ name = "ilb-test"
+ project_id = var.project_id
+ region = "europe-west1"
+ backend_service_configs = {
+ default = {
+ backends = [{
+ group = "projects/myprj/zones/europe-west1-a/instanceGroups/my-ig"
+ }]
+ }
+ video = {
+ backends = [{
+ group = "projects/myprj/zones/europe-west1-a/instanceGroups/my-ig-2"
+ }]
+ }
+ }
+ urlmap_config = {
+ default_service = "default"
+ host_rules = [{
+ hosts = ["*"]
+ path_matcher = "pathmap"
+ }]
+ path_matchers = {
+ pathmap = {
+ default_service = "default"
+ path_rules = [{
+ paths = ["/video", "/video/*"]
+ service = "video"
+ }]
+ }
+ }
+ }
+ vpc_config = {
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ }
+}
+
+# tftest modules=1 resources=6
+```
+
+### SSL Certificates
+
+Similarly to health checks, SSL certificates can also be created by the module. In this example we are using private key and certificate resources so that the example test only depends on Terraform providers, but in real use those can be replaced by external files.
+
+```hcl
+
+resource "tls_private_key" "default" {
+ algorithm = "RSA"
+ rsa_bits = 4096
+}
+
+resource "tls_self_signed_cert" "default" {
+ private_key_pem = tls_private_key.default.private_key_pem
+ subject {
+ common_name = "example.com"
+ organization = "ACME Examples, Inc"
+ }
+ validity_period_hours = 720
+ allowed_uses = [
+ "key_encipherment",
+ "digital_signature",
+ "server_auth",
+ ]
+}
+
+module "ilb-l7" {
+ source = "./fabric/modules/net-ilb-l7"
+ name = "ilb-test"
+ project_id = var.project_id
+ region = "europe-west1"
+ backend_service_configs = {
+ default = {
+ backends = [{
+ group = "projects/myprj/zones/europe-west1-a/instanceGroups/my-ig"
+ }]
+ }
+ }
+ health_check_configs = {
+ default = {
+ https = { port = 443 }
+ }
+ }
+ protocol = "HTTPS"
+ ssl_certificates = {
+ create_configs = {
+ default = {
+ # certificate and key could also be read via file() from external files
+ certificate = tls_self_signed_cert.default.cert_pem
+ private_key = tls_private_key.default.private_key_pem
+ }
+ }
+ }
+ vpc_config = {
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ }
+}
+# tftest modules=1 resources=8
+```
+
+### Complex example
+
+This example mixes group and NEG backends, and shows how to set HTTPS for specific backends.
+
+```hcl
+module "ilb-l7" {
+ source = "./fabric/modules/net-ilb-l7"
+ name = "ilb-l7-test-0"
+ project_id = "prj-gce"
+ region = "europe-west8"
+ backend_service_configs = {
+ default = {
+ backends = [
+ { group = "nginx-ew8-b" },
+ { group = "nginx-ew8-c" },
+ ]
+ }
+ gce-neg = {
+ backends = [{
+ balancing_mode = "RATE"
+ group = "neg-nginx-ew8-c"
+ max_rate = { per_endpoint = 1 }
+ }]
+ }
+ home = {
+ backends = [{
+ balancing_mode = "RATE"
+ group = "neg-home-hello"
+ max_rate = {
+ per_endpoint = 1
+ }
+ }]
+ health_checks = ["neg"]
+ locality_lb_policy = "ROUND_ROBIN"
+ protocol = "HTTPS"
+ }
+ }
+ group_configs = {
+ nginx-ew8-b = {
+ zone = "europe-west8-b"
+ instances = [
+ "projects/prj-gce/zones/europe-west8-b/instances/nginx-ew8-b"
+ ]
+ named_ports = { http = 80 }
+ }
+ nginx-ew8-c = {
+ zone = "europe-west8-c"
+ instances = [
+ "projects/prj-gce/zones/europe-west8-c/instances/nginx-ew8-c"
+ ]
+ named_ports = { http = 80 }
+ }
+ }
+ health_check_configs = {
+ default = {
+ http = {
+ port = 80
+ }
+ }
+ neg = {
+ https = {
+ host = "hello.home.example.com"
+ port = 443
+ }
+ }
+ }
+ neg_configs = {
+ neg-nginx-ew8-c = {
+ gce = {
+ zone = "europe-west8-c"
+ endpoints = {
+ e-0 = {
+ instance = "nginx-ew8-c"
+ ip_address = "10.24.32.26"
+ port = 80
+ }
+ }
+ }
+ }
+ neg-home-hello = {
+ hybrid = {
+ zone = "europe-west8-b"
+ endpoints = {
+ e-0 = {
+ ip_address = "192.168.0.3"
+ port = 443
+ }
+ }
+ }
+ }
+ }
+ urlmap_config = {
+ default_service = "default"
+ host_rules = [
+ {
+ hosts = ["*"]
+ path_matcher = "gce"
+ },
+ {
+ hosts = ["hello.home.example.com"]
+ path_matcher = "home"
+ }
+ ]
+ path_matchers = {
+ gce = {
+ default_service = "default"
+ path_rules = [
+ {
+ paths = ["/gce-neg", "/gce-neg/*"]
+ service = "gce-neg"
+ }
+ ]
+ }
+ home = {
+ default_service = "home"
+ }
+ }
+ }
+ vpc_config = {
+ network = "projects/prj-host/global/networks/shared-vpc"
+ subnetwork = "projects/prj-host/regions/europe-west8/subnetworks/gce"
+ }
+}
+# tftest modules=1 resources=14
+```
+
+
+
+
+## Files
+
+| name | description | resources |
+|---|---|---|
+| [backend-service.tf](./backend-service.tf) | Backend service resources. | google_compute_region_backend_service
|
+| [health-check.tf](./health-check.tf) | Health check resource. | google_compute_health_check
|
+| [main.tf](./main.tf) | Module-level locals and resources. | google_compute_forwarding_rule
· google_compute_instance_group
· google_compute_network_endpoint
· google_compute_network_endpoint_group
· google_compute_region_network_endpoint_group
· google_compute_region_ssl_certificate
· google_compute_region_target_http_proxy
· google_compute_region_target_https_proxy
|
+| [outputs.tf](./outputs.tf) | Module outputs. | |
+| [urlmap.tf](./urlmap.tf) | URL map resources. | google_compute_region_url_map
|
+| [variables-backend-service.tf](./variables-backend-service.tf) | Backend services variables. | |
+| [variables-health-check.tf](./variables-health-check.tf) | Health check variable. | |
+| [variables-urlmap.tf](./variables-urlmap.tf) | URLmap variable. | |
+| [variables.tf](./variables.tf) | Module variables. | |
+| [versions.tf](./versions.tf) | Version pins. | |
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [name](variables.tf#L54) | Load balancer name. | string
| ✓ | |
+| [project_id](variables.tf#L132) | Project id. | string
| ✓ | |
+| [region](variables.tf#L150) | The region where to allocate the ILB resources. | string
| ✓ | |
+| [vpc_config](variables.tf#L177) | VPC-level configuration. | object({…})
| ✓ | |
+| [address](variables.tf#L17) | Optional IP address used for the forwarding rule. | string
| | null
|
+| [backend_service_configs](variables-backend-service.tf#L19) | Backend service level configuration. | map(object({…}))
| | {}
|
+| [description](variables.tf#L23) | Optional description used for resources. | string
| | "Terraform managed."
|
+| [global_access](variables.tf#L30) | Allow client access from all regions. | bool
| | null
|
+| [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({…}))
| | {}
|
+| [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"
|
+| [service_directory_registration](variables.tf#L155) | Service directory namespace and service used to register this load balancer. | object({…})
| | null
|
+| [ssl_certificates](variables.tf#L164) | SSL target proxy certificates (only if protocol is HTTPS). | object({…})
| | {}
|
+| [urlmap_config](variables-urlmap.tf#L19) | The URL map configuration. | object({…})
| | {…}
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [address](outputs.tf#L17) | Forwarding rule address. | |
+| [backend_service_ids](outputs.tf#L22) | Backend service resources. | |
+| [forwarding_rule](outputs.tf#L29) | Forwarding rule resource. | |
+| [group_ids](outputs.tf#L34) | Autogenerated instance group ids. | |
+| [health_check_ids](outputs.tf#L41) | Autogenerated health check ids. | |
+| [neg_ids](outputs.tf#L48) | Autogenerated network endpoint group ids. | |
+
+
diff --git a/modules/net-ilb-l7/backend-service.tf b/modules/net-ilb-l7/backend-service.tf
new file mode 100644
index 0000000000..a517bd08c8
--- /dev/null
+++ b/modules/net-ilb-l7/backend-service.tf
@@ -0,0 +1,234 @@
+/**
+ * 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:file:description Backend service resources.
+
+locals {
+ group_ids = merge(
+ {
+ for k, v in google_compute_instance_group.default : k => v.id
+ },
+ {
+ 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 = {
+ for k, v in google_compute_health_check.default : k => v.id
+ }
+}
+
+resource "google_compute_region_backend_service" "default" {
+ provider = google-beta
+ for_each = var.backend_service_configs
+ project = (
+ each.value.project_id == null
+ ? var.project_id
+ : each.value.project_id
+ )
+ region = var.region
+ name = "${var.name}-${each.key}"
+ description = var.description
+ affinity_cookie_ttl_sec = each.value.affinity_cookie_ttl_sec
+ connection_draining_timeout_sec = each.value.connection_draining_timeout_sec
+ health_checks = [
+ for k in each.value.health_checks : lookup(local.hc_ids, k, k)
+ ] # not for internet / serverless NEGs
+ locality_lb_policy = each.value.locality_lb_policy
+ load_balancing_scheme = "INTERNAL_MANAGED"
+ port_name = each.value.port_name # defaults to http, not for NEGs
+ protocol = (
+ each.value.protocol == null ? var.protocol : each.value.protocol
+ )
+ session_affinity = each.value.session_affinity
+ timeout_sec = each.value.timeout_sec
+
+ dynamic "backend" {
+ for_each = { for b in coalesce(each.value.backends, []) : b.group => b }
+ content {
+ group = lookup(local.group_ids, backend.key, backend.key)
+ balancing_mode = backend.value.balancing_mode
+ capacity_scaler = backend.value.capacity_scaler
+ description = backend.value.description
+ failover = backend.value.failover
+ max_connections = try(
+ backend.value.max_connections.per_group, null
+ )
+ max_connections_per_endpoint = try(
+ backend.value.max_connections.per_endpoint, null
+ )
+ max_connections_per_instance = try(
+ backend.value.max_connections.per_instance, null
+ )
+ max_rate = try(
+ backend.value.max_rate.per_group, null
+ )
+ max_rate_per_endpoint = try(
+ backend.value.max_rate.per_endpoint, null
+ )
+ max_rate_per_instance = try(
+ backend.value.max_rate.per_instance, null
+ )
+ max_utilization = backend.value.max_utilization
+ }
+ }
+
+ dynamic "circuit_breakers" {
+ for_each = (
+ each.value.circuit_breakers == null ? [] : [each.value.circuit_breakers]
+ )
+ iterator = cb
+ content {
+ max_connections = cb.value.max_connections
+ max_pending_requests = cb.value.max_pending_requests
+ max_requests = cb.value.max_requests
+ max_requests_per_connection = cb.value.max_requests_per_connection
+ max_retries = cb.value.max_retries
+ dynamic "connect_timeout" {
+ for_each = (
+ cb.value.connect_timeout == null ? [] : [cb.value.connect_timeout]
+ )
+ content {
+ seconds = connect_timeout.value.seconds
+ nanos = connect_timeout.value.nanos
+ }
+ }
+ }
+ }
+
+ dynamic "connection_tracking_policy" {
+ for_each = (
+ each.value.connection_tracking == null
+ ? []
+ : [each.value.connection_tracking]
+ )
+ iterator = cb
+ content {
+ connection_persistence_on_unhealthy_backends = (
+ cb.value.persist_conn_on_unhealthy != null
+ ? cb.value.persist_conn_on_unhealthy
+ : null
+ )
+ idle_timeout_sec = cb.value.idle_timeout_sec
+ tracking_mode = (
+ cb.value.track_per_session != null
+ ? cb.value.track_per_session
+ : null
+ )
+ }
+ }
+
+ dynamic "consistent_hash" {
+ for_each = (
+ each.value.consistent_hash == null ? [] : [each.value.consistent_hash]
+ )
+ iterator = ch
+ content {
+ http_header_name = ch.value.http_header_name
+ minimum_ring_size = ch.value.minimum_ring_size
+ dynamic "http_cookie" {
+ for_each = ch.value.http_cookie == null ? [] : [ch.value.http_cookie]
+ content {
+ name = http_cookie.value.name
+ path = http_cookie.value.path
+ dynamic "ttl" {
+ for_each = (
+ http_cookie.value.ttl == null ? [] : [http_cookie.value.ttl]
+ )
+ content {
+ seconds = ttl.value.seconds
+ nanos = ttl.value.nanos
+ }
+ }
+ }
+ }
+ }
+ }
+
+ dynamic "failover_policy" {
+ for_each = (
+ each.value.failover_config == null ? [] : [each.value.failover_config]
+ )
+ iterator = fc
+ content {
+ disable_connection_drain_on_failover = fc.value.disable_conn_drain
+ drop_traffic_if_unhealthy = fc.value.drop_traffic_if_unhealthy
+ failover_ratio = fc.value.ratio
+ }
+ }
+
+ dynamic "iap" {
+ for_each = each.value.iap_config == null ? [] : [each.value.iap_config]
+ content {
+ oauth2_client_id = iap.value.oauth2_client_id
+ oauth2_client_secret = iap.value.oauth2_client_secret
+ oauth2_client_secret_sha256 = iap.value.oauth2_client_secret_sha256
+ }
+ }
+
+ dynamic "log_config" {
+ for_each = each.value.log_sample_rate == null ? [] : [""]
+ content {
+ enable = true
+ sample_rate = each.value.log_sample_rate
+ }
+ }
+
+ dynamic "outlier_detection" {
+ for_each = (
+ each.value.outlier_detection == null ? [] : [each.value.outlier_detection]
+ )
+ iterator = od
+ content {
+ consecutive_errors = od.value.consecutive_errors
+ consecutive_gateway_failure = od.value.consecutive_gateway_failure
+ enforcing_consecutive_errors = od.value.enforcing_consecutive_errors
+ enforcing_consecutive_gateway_failure = od.value.enforcing_consecutive_gateway_failure
+ enforcing_success_rate = od.value.enforcing_success_rate
+ max_ejection_percent = od.value.max_ejection_percent
+ success_rate_minimum_hosts = od.value.success_rate_minimum_hosts
+ success_rate_request_volume = od.value.success_rate_request_volume
+ success_rate_stdev_factor = od.value.success_rate_stdev_factor
+ dynamic "base_ejection_time" {
+ for_each = (
+ od.value.base_ejection_time == null ? [] : [od.value.base_ejection_time]
+ )
+ content {
+ seconds = base_ejection_time.value.seconds
+ nanos = base_ejection_time.value.nanos
+ }
+ }
+ dynamic "interval" {
+ for_each = (
+ od.value.interval == null ? [] : [od.value.interval]
+ )
+ content {
+ seconds = interval.value.seconds
+ nanos = interval.value.nanos
+ }
+ }
+ }
+ }
+
+ dynamic "subsetting" {
+ for_each = each.value.enable_subsetting == true ? [""] : []
+ content {
+ policy = "CONSISTENT_HASH_SUBSETTING"
+ }
+ }
+}
diff --git a/modules/net-ilb-l7/health-check.tf b/modules/net-ilb-l7/health-check.tf
new file mode 100644
index 0000000000..66ba58c56f
--- /dev/null
+++ b/modules/net-ilb-l7/health-check.tf
@@ -0,0 +1,113 @@
+/**
+ * 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:file:description Health check resource.
+
+resource "google_compute_health_check" "default" {
+ provider = google-beta
+ for_each = var.health_check_configs
+ project = (
+ each.value.project_id == null
+ ? var.project_id
+ : each.value.project_id
+ )
+ name = "${var.name}-${each.key}"
+ description = each.value.description
+ check_interval_sec = each.value.check_interval_sec
+ healthy_threshold = each.value.healthy_threshold
+ timeout_sec = each.value.timeout_sec
+ unhealthy_threshold = each.value.unhealthy_threshold
+
+ dynamic "grpc_health_check" {
+ for_each = try(each.value.grpc, null) != null ? [""] : []
+ content {
+ port = each.value.grpc.port
+ port_name = each.value.grpc.port_name
+ port_specification = each.value.grpc.port_specification
+ grpc_service_name = each.value.grpc.service_name
+ }
+ }
+
+ dynamic "http_health_check" {
+ for_each = try(each.value.http, null) != null ? [""] : []
+ content {
+ host = each.value.http.host
+ port = each.value.http.port
+ port_name = each.value.http.port_name
+ port_specification = each.value.http.port_specification
+ proxy_header = each.value.http.proxy_header
+ request_path = each.value.http.request_path
+ response = each.value.http.response
+ }
+ }
+
+ dynamic "http2_health_check" {
+ for_each = try(each.value.http2, null) != null ? [""] : []
+ content {
+ host = each.value.http2.host
+ port = each.value.http2.port
+ port_name = each.value.http2.port_name
+ port_specification = each.value.http2.port_specification
+ proxy_header = each.value.http2.proxy_header
+ request_path = each.value.http2.request_path
+ response = each.value.http2.response
+ }
+ }
+
+ dynamic "https_health_check" {
+ for_each = try(each.value.https, null) != null ? [""] : []
+ content {
+ host = each.value.https.host
+ port = each.value.https.port
+ port_name = each.value.https.port_name
+ port_specification = each.value.https.port_specification
+ proxy_header = each.value.https.proxy_header
+ request_path = each.value.https.request_path
+ response = each.value.https.response
+ }
+ }
+
+ dynamic "ssl_health_check" {
+ for_each = try(each.value.ssl, null) != null ? [""] : []
+ content {
+ port = each.value.ssl.port
+ port_name = each.value.ssl.port_name
+ port_specification = each.value.ssl.port_specification
+ proxy_header = each.value.ssl.proxy_header
+ request = each.value.ssl.request
+ response = each.value.ssl.response
+ }
+ }
+
+ dynamic "tcp_health_check" {
+ for_each = try(each.value.tcp, null) != null ? [""] : []
+ content {
+ port = each.value.tcp.port
+ port_name = each.value.tcp.port_name
+ port_specification = each.value.tcp.port_specification
+ proxy_header = each.value.tcp.proxy_header
+ request = each.value.tcp.request
+ response = each.value.tcp.response
+ }
+ }
+
+ dynamic "log_config" {
+ for_each = try(each.value.enable_logging, null) == true ? [""] : []
+ content {
+ enable = true
+ }
+ }
+}
diff --git a/modules/net-ilb-l7/main.tf b/modules/net-ilb-l7/main.tf
new file mode 100644
index 0000000000..803b3ff5c1
--- /dev/null
+++ b/modules/net-ilb-l7/main.tf
@@ -0,0 +1,185 @@
+/**
+ * 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 {
+ # we need keys in the endpoint type to address issue #1055
+ _neg_endpoints = flatten([
+ for k, v in local.neg_zonal : [
+ for kk, vv in v.endpoints : merge(vv, {
+ key = "${k}-${kk}", neg = k, zone = v.zone
+ })
+ ]
+ ])
+ fwd_rule_ports = (
+ var.protocol == "HTTPS" ? [443] : coalesce(var.ports, [80])
+ )
+ fwd_rule_target = (
+ var.protocol == "HTTPS"
+ ? google_compute_region_target_https_proxy.default.0.id
+ : google_compute_region_target_http_proxy.default.0.id
+ )
+ neg_endpoints = {
+ for v in local._neg_endpoints : (v.key) => v
+ }
+ neg_regional = {
+ for k, v in var.neg_configs :
+ k => merge(v.cloudrun, { project_id = v.project_id }) if v.cloudrun != null
+ }
+ neg_zonal = {
+ # we need to rebuild new objects as we cannot merge different types
+ for k, v in var.neg_configs : k => {
+ endpoints = v.gce != null ? v.gce.endpoints : v.hybrid.endpoints
+ network = v.gce != null ? v.gce.network : v.hybrid.network
+ project_id = v.project_id
+ subnetwork = v.gce != null ? v.gce.subnetwork : null
+ type = v.gce != null ? "GCE_VM_IP_PORT" : "NON_GCP_PRIVATE_IP_PORT"
+ zone = v.gce != null ? v.gce.zone : v.hybrid.zone
+ } if v.gce != null || v.hybrid != null
+ }
+ proxy_ssl_certificates = concat(
+ coalesce(var.ssl_certificates.certificate_ids, []),
+ [for k, v in google_compute_region_ssl_certificate.default : v.id]
+ )
+}
+
+resource "google_compute_forwarding_rule" "default" {
+ provider = google-beta
+ project = var.project_id
+ region = var.region
+ name = var.name
+ description = var.description
+ ip_address = var.address
+ ip_protocol = "TCP"
+ load_balancing_scheme = "INTERNAL_MANAGED"
+ network = var.vpc_config.network
+ network_tier = var.network_tier_premium ? "PREMIUM" : "STANDARD"
+ port_range = join(",", local.fwd_rule_ports)
+ subnetwork = var.vpc_config.subnetwork
+ labels = var.labels
+ target = local.fwd_rule_target
+ # during the preview phase you cannot change this attribute on an existing rule
+ allow_global_access = var.global_access
+ dynamic "service_directory_registrations" {
+ for_each = var.service_directory_registration == null ? [] : [""]
+ content {
+ namespace = var.service_directory_registration.namespace
+ service = var.service_directory_registration.service
+ }
+ }
+}
+
+resource "google_compute_region_ssl_certificate" "default" {
+ for_each = var.ssl_certificates.create_configs
+ project = var.project_id
+ region = var.region
+ name = "${var.name}-${each.key}"
+ certificate = each.value.certificate
+ private_key = each.value.private_key
+}
+
+resource "google_compute_region_target_http_proxy" "default" {
+ count = var.protocol == "HTTPS" ? 0 : 1
+ project = var.project_id
+ region = var.region
+ name = var.name
+ description = var.description
+ url_map = google_compute_region_url_map.default.id
+}
+
+resource "google_compute_region_target_https_proxy" "default" {
+ count = var.protocol == "HTTPS" ? 1 : 0
+ project = var.project_id
+ region = var.region
+ name = var.name
+ description = var.description
+ ssl_certificates = local.proxy_ssl_certificates
+ url_map = google_compute_region_url_map.default.id
+}
+
+resource "google_compute_instance_group" "default" {
+ for_each = var.group_configs
+ project = (
+ each.value.project_id == null
+ ? var.project_id
+ : each.value.project_id
+ )
+ zone = each.value.zone
+ name = "${var.name}-${each.key}"
+ description = var.description
+ instances = each.value.instances
+ dynamic "named_port" {
+ for_each = each.value.named_ports
+ content {
+ name = named_port.key
+ port = named_port.value
+ }
+ }
+}
+
+resource "google_compute_network_endpoint_group" "default" {
+ for_each = local.neg_zonal
+ project = (
+ each.value.project_id == null
+ ? var.project_id
+ : each.value.project_id
+ )
+ zone = each.value.zone
+ name = "${var.name}-${each.key}"
+ # re-enable once provider properly supports this
+ # default_port = each.value.default_port
+ description = var.description
+ network_endpoint_type = each.value.type
+ network = (
+ each.value.network != null ? each.value.network : var.vpc_config.network
+ )
+ subnetwork = (
+ each.value.type == "NON_GCP_PRIVATE_IP_PORT"
+ ? null
+ : try(each.value.subnetwork, var.vpc_config.subnetwork)
+ )
+}
+
+resource "google_compute_network_endpoint" "default" {
+ for_each = local.neg_endpoints
+ project = (
+ google_compute_network_endpoint_group.default[each.value.neg].project
+ )
+ network_endpoint_group = (
+ google_compute_network_endpoint_group.default[each.value.neg].name
+ )
+ instance = try(each.value.instance, null)
+ ip_address = each.value.ip_address
+ port = each.value.port
+ zone = each.value.zone
+}
+
+resource "google_compute_region_network_endpoint_group" "default" {
+ for_each = local.neg_regional
+ project = (
+ each.value.project_id == null
+ ? var.project_id
+ : each.value.project_id
+ )
+ region = each.value.region
+ name = "${var.name}-${each.key}"
+ description = var.description
+ network_endpoint_type = "SERVERLESS"
+ cloud_run {
+ service = try(each.value.target_service.name, null)
+ tag = try(each.value.target_service.tag, null)
+ url_mask = each.value.target_urlmask
+ }
+}
diff --git a/modules/net-ilb-l7/outputs.tf b/modules/net-ilb-l7/outputs.tf
new file mode 100644
index 0000000000..9082dfecac
--- /dev/null
+++ b/modules/net-ilb-l7/outputs.tf
@@ -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.
+ */
+
+output "address" {
+ description = "Forwarding rule address."
+ value = google_compute_forwarding_rule.default.ip_address
+}
+
+output "backend_service_ids" {
+ description = "Backend service resources."
+ value = {
+ for k, v in google_compute_region_backend_service.default : k => v.id
+ }
+}
+
+output "forwarding_rule" {
+ description = "Forwarding rule resource."
+ value = google_compute_forwarding_rule.default
+}
+
+output "group_ids" {
+ description = "Autogenerated instance group ids."
+ value = {
+ for k, v in google_compute_instance_group.default : k => v.id
+ }
+}
+
+output "health_check_ids" {
+ description = "Autogenerated health check ids."
+ value = {
+ for k, v in google_compute_health_check.default : k => v.id
+ }
+}
+
+output "neg_ids" {
+ description = "Autogenerated network endpoint group ids."
+ value = {
+ for k, v in google_compute_network_endpoint_group.default : k => v.id
+ }
+}
diff --git a/modules/net-ilb-l7/urlmap.tf b/modules/net-ilb-l7/urlmap.tf
new file mode 100644
index 0000000000..21764ce090
--- /dev/null
+++ b/modules/net-ilb-l7/urlmap.tf
@@ -0,0 +1,576 @@
+/**
+ * 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:file:description URL map resources.
+
+locals {
+ backend_ids = {
+ for k, v in google_compute_region_backend_service.default : k => v.id
+ }
+}
+
+resource "google_compute_region_url_map" "default" {
+ provider = google-beta
+ project = var.project_id
+ region = var.region
+ name = var.name
+ description = var.description
+ default_service = (
+ var.urlmap_config.default_service == null ? null : lookup(
+ local.backend_ids,
+ var.urlmap_config.default_service,
+ var.urlmap_config.default_service
+ )
+ )
+
+ dynamic "default_url_redirect" {
+ for_each = (
+ var.urlmap_config.default_url_redirect == null
+ ? []
+ : [var.urlmap_config.default_url_redirect]
+ )
+ iterator = r
+ content {
+ host_redirect = r.value.host
+ https_redirect = r.value.https
+ path_redirect = r.value.path
+ prefix_redirect = r.value.prefix
+ redirect_response_code = r.value.response_code
+ strip_query = r.value.strip_query
+ }
+ }
+
+ dynamic "host_rule" {
+ for_each = coalesce(var.urlmap_config.host_rules, [])
+ iterator = r
+ content {
+ hosts = r.value.hosts
+ path_matcher = r.value.path_matcher
+ description = r.value.description
+ }
+ }
+
+ dynamic "path_matcher" {
+ for_each = coalesce(var.urlmap_config.path_matchers, {})
+ iterator = m
+ content {
+ default_service = m.value.default_service == null ? null : lookup(
+ local.backend_ids, m.value.default_service, m.value.default_service
+ )
+ description = m.value.description
+ name = m.key
+ dynamic "default_url_redirect" {
+ for_each = (
+ m.value.default_url_redirect == null
+ ? []
+ : [m.value.default_url_redirect]
+ )
+ content {
+ host_redirect = default_url_redirect.value.host
+ https_redirect = default_url_redirect.value.https
+ path_redirect = default_url_redirect.value.path
+ prefix_redirect = default_url_redirect.value.prefix
+ redirect_response_code = default_url_redirect.value.response_code
+ strip_query = default_url_redirect.value.strip_query
+ }
+ }
+ dynamic "path_rule" {
+ for_each = toset(coalesce(m.value.path_rules, []))
+ content {
+ paths = path_rule.value.paths
+ service = path_rule.value.service == null ? null : lookup(
+ local.backend_ids,
+ path_rule.value.service,
+ path_rule.value.service
+ )
+ dynamic "route_action" {
+ for_each = (
+ path_rule.value.route_action == null
+ ? []
+ : [path_rule.value.route_action]
+ )
+ content {
+ dynamic "cors_policy" {
+ for_each = (
+ route_action.value.cors_policy == null
+ ? []
+ : [route_action.value.cors_policy]
+ )
+ content {
+ allow_credentials = cors_policy.value.allow_credentials
+ allow_headers = cors_policy.value.allow_headers
+ allow_methods = cors_policy.value.allow_methods
+ allow_origin_regexes = cors_policy.value.allow_origin_regexes
+ allow_origins = cors_policy.value.allow_origins
+ disabled = cors_policy.value.disabled
+ expose_headers = cors_policy.value.expose_headers
+ max_age = cors_policy.value.max_age
+ }
+ }
+ dynamic "fault_injection_policy" {
+ for_each = (
+ route_action.value.fault_injection_policy == null
+ ? []
+ : [route_action.value.fault_injection_policy]
+ )
+ content {
+ dynamic "abort" {
+ for_each = (
+ fault_injection_policy.value.abort == null
+ ? []
+ : [fault_injection_policy.value.abort]
+ )
+ content {
+ http_status = abort.value.status
+ percentage = abort.value.percentage
+ }
+ }
+ dynamic "delay" {
+ for_each = (
+ fault_injection_policy.value.delay == null
+ ? []
+ : [fault_injection_policy.value.delay]
+ )
+ content {
+ percentage = delay.value.percentage
+ fixed_delay {
+ nanos = delay.value.fixed.nanos
+ seconds = delay.value.fixed.seconds
+ }
+ }
+ }
+ }
+ }
+ dynamic "request_mirror_policy" {
+ for_each = (
+ route_action.value.request_mirror_backend == null
+ ? []
+ : [""]
+ )
+ content {
+ backend_service = lookup(
+ local.backend_ids,
+ route_action.value.request_mirror_backend,
+ route_action.value.request_mirror_backend
+ )
+ }
+ }
+ dynamic "retry_policy" {
+ for_each = (
+ route_action.value.retry_policy == null
+ ? []
+ : [route_action.value.retry_policy]
+ )
+ content {
+ num_retries = retry_policy.value.num_retries
+ retry_conditions = retry_policy.value.retry_conditions
+ dynamic "per_try_timeout" {
+ for_each = (
+ retry_policy.value.per_try_timeout == null
+ ? []
+ : [retry_policy.value.per_try_timeout]
+ )
+ content {
+ nanos = per_try_timeout.value.nanos
+ seconds = per_try_timeout.value.seconds
+ }
+ }
+ }
+ }
+ dynamic "timeout" {
+ for_each = (
+ route_action.value.timeout == null
+ ? []
+ : [route_action.value.timeout]
+ )
+ content {
+ nanos = timeout.value.nanos
+ seconds = timeout.value.seconds
+ }
+ }
+ dynamic "url_rewrite" {
+ for_each = (
+ route_action.value.url_rewrite == null
+ ? []
+ : [route_action.value.url_rewrite]
+ )
+ content {
+ host_rewrite = url_rewrite.value.host
+ path_prefix_rewrite = url_rewrite.value.path_prefix
+ }
+ }
+ dynamic "weighted_backend_services" {
+ for_each = coalesce(
+ route_action.value.weighted_backend_services, {}
+ )
+ iterator = service
+ content {
+ backend_service = lookup(
+ local.backend_ids, service.key, service.key
+ )
+ weight = service.value.weight
+ dynamic "header_action" {
+ for_each = (
+ service.value.header_action == null
+ ? []
+ : [service.value.header_action]
+ )
+ iterator = h
+ content {
+ request_headers_to_remove = h.value.request_remove
+ response_headers_to_remove = h.value.response_remove
+ dynamic "request_headers_to_add" {
+ for_each = coalesce(h.value.request_add, {})
+ content {
+ header_name = request_headers_to_add.key
+ header_value = request_headers_to_add.value.value
+ replace = request_headers_to_add.value.replace
+ }
+ }
+ dynamic "response_headers_to_add" {
+ for_each = coalesce(h.value.response_add, {})
+ content {
+ header_name = response_headers_to_add.key
+ header_value = response_headers_to_add.value.value
+ replace = response_headers_to_add.value.replace
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ dynamic "url_redirect" {
+ for_each = (
+ path_rule.value.url_redirect == null
+ ? []
+ : [path_rule.value.url_redirect]
+ )
+ content {
+ host_redirect = url_redirect.value.host
+ https_redirect = url_redirect.value.https
+ path_redirect = url_redirect.value.path
+ prefix_redirect = url_redirect.value.prefix
+ redirect_response_code = url_redirect.value.response_code
+ strip_query = url_redirect.value.strip_query
+ }
+ }
+ }
+ }
+ dynamic "route_rules" {
+ for_each = toset(coalesce(m.value.route_rules, []))
+ content {
+ priority = route_rules.value.priority
+ service = route_rules.value.service == null ? null : lookup(
+ local.backend_ids,
+ route_rules.value.service,
+ route_rules.value.service
+ )
+ dynamic "header_action" {
+ for_each = (
+ route_rules.value.header_action == null
+ ? []
+ : [route_rules.value.header_action]
+ )
+ iterator = h
+ content {
+ request_headers_to_remove = h.value.request_remove
+ response_headers_to_remove = h.value.response_remove
+ dynamic "request_headers_to_add" {
+ for_each = coalesce(h.value.request_add, {})
+ content {
+ header_name = request_headers_to_add.key
+ header_value = request_headers_to_add.value.value
+ replace = request_headers_to_add.value.replace
+ }
+ }
+ dynamic "response_headers_to_add" {
+ for_each = coalesce(h.value.response_add, {})
+ content {
+ header_name = response_headers_to_add.key
+ header_value = response_headers_to_add.value.value
+ replace = response_headers_to_add.value.replace
+ }
+ }
+ }
+ }
+ dynamic "match_rules" {
+ for_each = toset(coalesce(route_rules.value.match_rules, []))
+ content {
+ ignore_case = match_rules.value.ignore_case
+ full_path_match = (
+ try(match_rules.value.path.type, null) == "full"
+ ? match_rules.value.path.value
+ : null
+ )
+ prefix_match = (
+ try(match_rules.value.path.type, null) == "prefix"
+ ? match_rules.value.path.value
+ : null
+ )
+ regex_match = (
+ try(match_rules.value.path.type, null) == "regex"
+ ? match_rules.value.path.value
+ : null
+ )
+ dynamic "header_matches" {
+ for_each = toset(coalesce(match_rules.value.headers, []))
+ iterator = h
+ content {
+ header_name = h.value.name
+ exact_match = h.value.type == "exact" ? h.value.value : null
+ invert_match = h.value.invert_match
+ prefix_match = h.value.type == "prefix" ? h.value.value : null
+ present_match = h.value.type == "present" ? h.value.value : null
+ regex_match = h.value.type == "regex" ? h.value.value : null
+ suffix_match = h.value.type == "suffix" ? h.value.value : null
+ dynamic "range_match" {
+ for_each = (
+ h.value.type != "range" || h.value.range_value == null
+ ? []
+ : [""]
+ )
+ content {
+ range_end = h.value.range_value.end
+ range_start = h.value.range_value.start
+ }
+ }
+ }
+ }
+ dynamic "metadata_filters" {
+ for_each = toset(coalesce(match_rules.value.metadata_filters, []))
+ iterator = m
+ content {
+ filter_match_criteria = (
+ m.value.match_all ? "MATCH_ALL" : "MATCH_ANY"
+ )
+ dynamic "filter_labels" {
+ for_each = m.value.labels
+ content {
+ name = filter_labels.key
+ value = filter_labels.value
+ }
+ }
+ }
+ }
+ dynamic "query_parameter_matches" {
+ for_each = toset(coalesce(match_rules.value.query_params, []))
+ iterator = q
+ content {
+ name = q.value.name
+ exact_match = (
+ q.value.type == "exact" ? q.value.value : null
+ )
+ present_match = (
+ q.value.type == "present" ? q.value.value : null
+ )
+ regex_match = (
+ q.value.type == "regex" ? q.value.value : null
+ )
+ }
+ }
+ }
+ }
+ dynamic "route_action" {
+ for_each = (
+ route_rules.value.route_action == null
+ ? []
+ : [route_rules.value.route_action]
+ )
+ content {
+ dynamic "cors_policy" {
+ for_each = (
+ route_action.value.cors_policy == null
+ ? []
+ : [route_action.value.cors_policy]
+ )
+ content {
+ allow_credentials = cors_policy.value.allow_credentials
+ allow_headers = cors_policy.value.allow_headers
+ allow_methods = cors_policy.value.allow_methods
+ allow_origin_regexes = cors_policy.value.allow_origin_regexes
+ allow_origins = cors_policy.value.allow_origins
+ disabled = cors_policy.value.disabled
+ expose_headers = cors_policy.value.expose_headers
+ max_age = cors_policy.value.max_age
+ }
+ }
+ dynamic "fault_injection_policy" {
+ for_each = (
+ route_action.value.fault_injection_policy == null
+ ? []
+ : [route_action.value.fault_injection_policy]
+ )
+ content {
+ dynamic "abort" {
+ for_each = (
+ fault_injection_policy.value.abort == null
+ ? []
+ : [fault_injection_policy.value.abort]
+ )
+ content {
+ http_status = abort.value.status
+ percentage = abort.value.percentage
+ }
+ }
+ dynamic "delay" {
+ for_each = (
+ fault_injection_policy.value.delay == null
+ ? []
+ : [fault_injection_policy.value.delay]
+ )
+ content {
+ percentage = delay.value.percentage
+ fixed_delay {
+ nanos = delay.value.fixed.nanos
+ seconds = delay.value.fixed.seconds
+ }
+ }
+ }
+ }
+ }
+ dynamic "request_mirror_policy" {
+ for_each = (
+ route_action.value.request_mirror_backend == null
+ ? []
+ : [""]
+ )
+ content {
+ backend_service = lookup(
+ local.backend_ids,
+ route_action.value.request_mirror_backend,
+ route_action.value.request_mirror_backend
+ )
+ }
+ }
+ dynamic "retry_policy" {
+ for_each = (
+ route_action.value.retry_policy == null
+ ? []
+ : [route_action.value.retry_policy]
+ )
+ content {
+ num_retries = retry_policy.value.num_retries
+ retry_conditions = retry_policy.value.retry_conditions
+ dynamic "per_try_timeout" {
+ for_each = (
+ retry_policy.value.per_try_timeout == null
+ ? []
+ : [retry_policy.value.per_try_timeout]
+ )
+ content {
+ nanos = per_try_timeout.value.nanos
+ seconds = per_try_timeout.value.seconds
+ }
+ }
+ }
+ }
+ dynamic "timeout" {
+ for_each = (
+ route_action.value.timeout == null
+ ? []
+ : [route_action.value.timeout]
+ )
+ content {
+ nanos = timeout.value.nanos
+ seconds = timeout.value.seconds
+ }
+ }
+ dynamic "url_rewrite" {
+ for_each = (
+ route_action.value.url_rewrite == null
+ ? []
+ : [route_action.value.url_rewrite]
+ )
+ content {
+ host_rewrite = url_rewrite.value.host
+ path_prefix_rewrite = url_rewrite.value.path_prefix
+ }
+ }
+ dynamic "weighted_backend_services" {
+ for_each = coalesce(
+ route_action.value.weighted_backend_services, {}
+ )
+ iterator = service
+ content {
+ backend_service = lookup(
+ local.backend_ids, service.key, service.key
+ )
+ weight = service.value.weight
+ dynamic "header_action" {
+ for_each = (
+ service.value.header_action == null
+ ? []
+ : [service.value.header_action]
+ )
+ iterator = h
+ content {
+ request_headers_to_remove = h.value.request_remove
+ response_headers_to_remove = h.value.response_remove
+ dynamic "request_headers_to_add" {
+ for_each = coalesce(h.value.request_add, {})
+ content {
+ header_name = request_headers_to_add.key
+ header_value = request_headers_to_add.value.value
+ replace = request_headers_to_add.value.replace
+ }
+ }
+ dynamic "response_headers_to_add" {
+ for_each = coalesce(h.value.response_add, {})
+ content {
+ header_name = response_headers_to_add.key
+ header_value = response_headers_to_add.value.value
+ replace = response_headers_to_add.value.replace
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ dynamic "url_redirect" {
+ for_each = (
+ route_rules.value.default_url_redirect == null
+ ? []
+ : [route_rules.value.default_url_redirect]
+ )
+ content {
+ host_redirect = url_redirect.value.host
+ https_redirect = url_redirect.value.https
+ path_redirect = url_redirect.value.path
+ prefix_redirect = url_redirect.value.prefix
+ redirect_response_code = url_redirect.value.response_code
+ strip_query = url_redirect.value.strip_query
+ }
+ }
+ }
+ }
+ }
+ }
+
+ dynamic "test" {
+ for_each = toset(coalesce(var.urlmap_config.test, []))
+ content {
+ host = test.value.host
+ path = test.value.path
+ service = test.value.service
+ description = test.value.description
+ }
+ }
+
+}
diff --git a/modules/net-ilb-l7/variables-backend-service.tf b/modules/net-ilb-l7/variables-backend-service.tf
new file mode 100644
index 0000000000..0119d1b393
--- /dev/null
+++ b/modules/net-ilb-l7/variables-backend-service.tf
@@ -0,0 +1,131 @@
+/**
+ * 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:file:description Backend services variables.
+
+variable "backend_service_configs" {
+ description = "Backend service level configuration."
+ type = map(object({
+ affinity_cookie_ttl_sec = optional(number)
+ connection_draining_timeout_sec = optional(number)
+ health_checks = optional(list(string), ["default"])
+ locality_lb_policy = optional(string)
+ log_sample_rate = optional(number)
+ port_name = optional(string)
+ project_id = optional(string)
+ protocol = optional(string)
+ session_affinity = optional(string)
+ timeout_sec = optional(number)
+ backends = list(object({
+ group = string
+ balancing_mode = optional(string, "UTILIZATION")
+ capacity_scaler = optional(number, 1)
+ description = optional(string, "Terraform managed.")
+ failover = optional(bool, false)
+ max_connections = optional(object({
+ per_endpoint = optional(number)
+ per_group = optional(number)
+ per_instance = optional(number)
+ }))
+ max_rate = optional(object({
+ per_endpoint = optional(number)
+ per_group = optional(number)
+ per_instance = optional(number)
+ }))
+ max_utilization = optional(number)
+ }))
+ circuit_breakers = optional(object({
+ max_connections = optional(number)
+ max_pending_requests = optional(number)
+ max_requests = optional(number)
+ max_requests_per_connection = optional(number)
+ max_retries = optional(number)
+ connect_timeout = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ }))
+ connection_tracking = optional(object({
+ idle_timeout_sec = optional(number)
+ persist_conn_on_unhealthy = optional(string)
+ track_per_session = optional(bool)
+ }))
+ consistent_hash = optional(object({
+ http_header_name = optional(string)
+ minimum_ring_size = optional(number)
+ http_cookie = optional(object({
+ name = optional(string)
+ path = optional(string)
+ ttl = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ }))
+ }))
+ enable_subsetting = optional(bool)
+ failover_config = optional(object({
+ disable_conn_drain = optional(bool)
+ drop_traffic_if_unhealthy = optional(bool)
+ ratio = optional(number)
+ }))
+ iap_config = optional(object({
+ oauth2_client_id = string
+ oauth2_client_secret = string
+ oauth2_client_secret_sha256 = optional(string)
+ }))
+ outlier_detection = optional(object({
+ consecutive_errors = optional(number)
+ consecutive_gateway_failure = optional(number)
+ enforcing_consecutive_errors = optional(number)
+ enforcing_consecutive_gateway_failure = optional(number)
+ enforcing_success_rate = optional(number)
+ max_ejection_percent = optional(number)
+ success_rate_minimum_hosts = optional(number)
+ success_rate_request_volume = optional(number)
+ success_rate_stdev_factor = optional(number)
+ base_ejection_time = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ interval = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ }))
+ }))
+ default = {}
+ nullable = false
+ validation {
+ condition = contains(
+ [
+ "-", "ROUND_ROBIN", "LEAST_REQUEST", "RING_HASH",
+ "RANDOM", "ORIGINAL_DESTINATION", "MAGLEV"
+ ],
+ try(var.backend_service_configs.locality_lb_policy, "-")
+ )
+ error_message = "Invalid locality lb policy value."
+ }
+ validation {
+ condition = contains(
+ [
+ "NONE", "CLIENT_IP", "CLIENT_IP_NO_DESTINATION",
+ "CLIENT_IP_PORT_PROTO", "CLIENT_IP_PROTO"
+ ],
+ try(var.backend_service_configs.session_affinity, "NONE")
+ )
+ error_message = "Invalid session affinity value."
+ }
+}
diff --git a/modules/net-ilb-l7/variables-health-check.tf b/modules/net-ilb-l7/variables-health-check.tf
new file mode 100644
index 0000000000..f56d02304d
--- /dev/null
+++ b/modules/net-ilb-l7/variables-health-check.tf
@@ -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.
+ */
+
+# tfdoc:file:description Health check variable.
+
+variable "health_check_configs" {
+ description = "Optional auto-created health check configurations, use the output self-link to set it in the auto healing policy. Refer to examples for usage."
+ type = map(object({
+ check_interval_sec = optional(number)
+ description = optional(string, "Terraform managed.")
+ enable_logging = optional(bool, false)
+ healthy_threshold = optional(number)
+ project_id = optional(string)
+ timeout_sec = optional(number)
+ unhealthy_threshold = optional(number)
+ grpc = optional(object({
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ service_name = optional(string)
+ }))
+ http = optional(object({
+ host = optional(string)
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request_path = optional(string)
+ response = optional(string)
+ }))
+ http2 = optional(object({
+ host = optional(string)
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request_path = optional(string)
+ response = optional(string)
+ }))
+ https = optional(object({
+ host = optional(string)
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request_path = optional(string)
+ response = optional(string)
+ }))
+ tcp = optional(object({
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request = optional(string)
+ response = optional(string)
+ }))
+ ssl = optional(object({
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request = optional(string)
+ response = optional(string)
+ }))
+ }))
+ default = {
+ default = {
+ http = {
+ port_specification = "USE_SERVING_PORT"
+ }
+ }
+ }
+ validation {
+ condition = alltrue([
+ for k, v in var.health_check_configs : (
+ (try(v.grpc, null) == null ? 0 : 1) +
+ (try(v.http, null) == null ? 0 : 1) +
+ (try(v.tcp, null) == null ? 0 : 1) <= 1
+ )
+ ])
+ error_message = "Only one health check type can be configured at a time."
+ }
+ validation {
+ condition = alltrue(flatten([
+ for k, v in var.health_check_configs : [
+ for kk, vv in v : contains([
+ "-", "USE_FIXED_PORT", "USE_NAMED_PORT", "USE_SERVING_PORT"
+ ], coalesce(try(vv.port_specification, null), "-"))
+ ]
+ ]))
+ error_message = "Invalid 'port_specification' value. Supported values are 'USE_FIXED_PORT', 'USE_NAMED_PORT', 'USE_SERVING_PORT'."
+ }
+}
diff --git a/modules/net-ilb-l7/variables-urlmap.tf b/modules/net-ilb-l7/variables-urlmap.tf
new file mode 100644
index 0000000000..cd1869f5ae
--- /dev/null
+++ b/modules/net-ilb-l7/variables-urlmap.tf
@@ -0,0 +1,234 @@
+/**
+ * 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:file:description URLmap variable.
+
+variable "urlmap_config" {
+ description = "The URL map configuration."
+ type = object({
+ default_service = optional(string)
+ default_url_redirect = optional(object({
+ host = optional(string)
+ https = optional(bool)
+ path = optional(string)
+ prefix = optional(string)
+ response_code = optional(string)
+ strip_query = optional(bool)
+ }))
+ host_rules = optional(list(object({
+ hosts = list(string)
+ path_matcher = string
+ description = optional(string)
+ })))
+ path_matchers = optional(map(object({
+ description = optional(string)
+ default_service = optional(string)
+ default_url_redirect = optional(object({
+ host = optional(string)
+ https = optional(bool)
+ path = optional(string)
+ prefix = optional(string)
+ response_code = optional(string)
+ strip_query = optional(bool)
+ }))
+ path_rules = optional(list(object({
+ paths = list(string)
+ service = optional(string)
+ route_action = optional(object({
+ request_mirror_backend = optional(string)
+ cors_policy = optional(object({
+ allow_credentials = optional(bool)
+ allow_headers = optional(string)
+ allow_methods = optional(string)
+ allow_origin_regexes = list(string)
+ allow_origins = list(string)
+ disabled = optional(bool)
+ expose_headers = optional(string)
+ max_age = optional(string)
+ }))
+ fault_injection_policy = optional(object({
+ abort = optional(object({
+ percentage = number
+ status = number
+ }))
+ delay = optional(object({
+ fixed = object({
+ seconds = number
+ nanos = number
+ })
+ percentage = number
+ }))
+ }))
+ retry_policy = optional(object({
+ num_retries = number
+ retry_conditions = optional(list(string))
+ per_try_timeout = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ }))
+ timeout = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ url_rewrite = optional(object({
+ host = optional(string)
+ path_prefix = optional(string)
+ }))
+ weighted_backend_services = optional(map(object({
+ weight = number
+ header_action = optional(object({
+ request_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ request_remove = optional(list(string))
+ response_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ response_remove = optional(list(string))
+ }))
+ })))
+ }))
+ url_redirect = optional(object({
+ host = optional(string)
+ https = optional(bool)
+ path = optional(string)
+ prefix = optional(string)
+ response_code = optional(string)
+ strip_query = optional(bool)
+ }))
+ })))
+ route_rules = optional(list(object({
+ priority = number
+ service = optional(string)
+ header_action = optional(object({
+ request_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ request_remove = optional(list(string))
+ response_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ response_remove = optional(list(string))
+ }))
+ match_rules = optional(list(object({
+ ignore_case = optional(bool, false)
+ headers = optional(list(object({
+ name = string
+ invert_match = optional(bool, false)
+ type = optional(string, "present") # exact, prefix, suffix, regex, present, range
+ value = optional(string)
+ range_value = optional(object({
+ end = string
+ start = string
+ }))
+ })))
+ metadata_filters = optional(list(object({
+ labels = map(string)
+ match_all = bool # MATCH_ANY, MATCH_ALL
+ })))
+ path = optional(object({
+ value = string
+ type = optional(string, "prefix") # full, prefix, regex
+ }))
+ query_params = optional(list(object({
+ name = string
+ value = string
+ type = optional(string, "present") # exact, present, regex
+ })))
+ })))
+ route_action = optional(object({
+ request_mirror_backend = optional(string)
+ cors_policy = optional(object({
+ allow_credentials = optional(bool)
+ allow_headers = optional(string)
+ allow_methods = optional(string)
+ allow_origin_regexes = list(string)
+ allow_origins = list(string)
+ disabled = optional(bool)
+ expose_headers = optional(string)
+ max_age = optional(string)
+ }))
+ fault_injection_policy = optional(object({
+ abort = optional(object({
+ percentage = number
+ status = number
+ }))
+ delay = optional(object({
+ fixed = object({
+ seconds = number
+ nanos = number
+ })
+ percentage = number
+ }))
+ }))
+ retry_policy = optional(object({
+ num_retries = number
+ retry_conditions = optional(list(string))
+ per_try_timeout = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ }))
+ timeout = optional(object({
+ seconds = number
+ nanos = optional(number)
+ }))
+ url_rewrite = optional(object({
+ host = optional(string)
+ path_prefix = optional(string)
+ }))
+ weighted_backend_services = optional(map(object({
+ weight = number
+ header_action = optional(object({
+ request_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ request_remove = optional(list(string))
+ response_add = optional(map(object({
+ value = string
+ replace = optional(bool, true)
+ })))
+ response_remove = optional(list(string))
+ }))
+ })))
+ }))
+ url_redirect = optional(object({
+ host = optional(string)
+ https = optional(bool)
+ path = optional(string)
+ prefix = optional(string)
+ response_code = optional(string)
+ strip_query = optional(bool)
+ }))
+ })))
+ })))
+ test = optional(list(object({
+ host = string
+ path = string
+ service = string
+ description = optional(string)
+ })))
+ })
+ default = {
+ default_service = "default"
+ }
+}
diff --git a/modules/net-ilb-l7/variables.tf b/modules/net-ilb-l7/variables.tf
new file mode 100644
index 0000000000..09b3f7ac74
--- /dev/null
+++ b/modules/net-ilb-l7/variables.tf
@@ -0,0 +1,184 @@
+/**
+ * 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 "address" {
+ description = "Optional IP address used for the forwarding rule."
+ type = string
+ default = null
+}
+
+variable "description" {
+ description = "Optional description used for resources."
+ type = string
+ default = "Terraform managed."
+}
+
+# during the preview phase you cannot change this attribute on an existing rule
+variable "global_access" {
+ description = "Allow client access from all regions."
+ type = bool
+ default = null
+}
+
+variable "group_configs" {
+ description = "Optional unmanaged groups to create. Can be referenced in backends via key or outputs."
+ type = map(object({
+ zone = string
+ instances = optional(list(string), [])
+ named_ports = optional(map(number), {})
+ project_id = optional(string)
+ }))
+ default = {}
+ nullable = false
+}
+
+variable "labels" {
+ description = "Labels set on resources."
+ type = map(string)
+ default = {}
+}
+
+variable "name" {
+ description = "Load balancer name."
+ type = string
+}
+
+variable "neg_configs" {
+ description = "Optional network endpoint groups to create. Can be referenced in backends via key or outputs."
+ type = map(object({
+ project_id = optional(string)
+ cloudrun = optional(object({
+ region = string
+ target_service = optional(object({
+ name = string
+ tag = optional(string)
+ }))
+ target_urlmask = optional(string)
+ }))
+ gce = optional(object({
+ zone = string
+ # default_port = optional(number)
+ network = optional(string)
+ subnetwork = optional(string)
+ endpoints = optional(map(object({
+ instance = string
+ ip_address = string
+ port = number
+ })))
+
+ }))
+ hybrid = optional(object({
+ zone = string
+ network = optional(string)
+ # re-enable once provider properly support this
+ # default_port = optional(number)
+ endpoints = optional(map(object({
+ ip_address = string
+ port = number
+ })))
+ }))
+ # psc = optional(object({}))
+ }))
+ default = {}
+ nullable = false
+ validation {
+ condition = alltrue([
+ for k, v in var.neg_configs : (
+ (try(v.cloudrun, null) == null ? 0 : 1) +
+ (try(v.gce, null) == null ? 0 : 1) +
+ (try(v.hybrid, null) == null ? 0 : 1) == 1
+ )
+ ])
+ error_message = "Only one type of neg can be configured at a time."
+ }
+ validation {
+ condition = alltrue([
+ for k, v in var.neg_configs : (
+ v.cloudrun == null
+ ? true
+ : v.cloudrun.target_urlmask != null || v.cloudrun.target_service != null
+ )
+ ])
+ error_message = "Cloud Run negs need either target type or target urlmask defined."
+ }
+}
+
+variable "network_tier_premium" {
+ description = "Use premium network tier. Defaults to true."
+ type = bool
+ default = true
+ nullable = false
+}
+
+variable "ports" {
+ description = "Optional ports for HTTP load balancer, valid ports are 80 and 8080."
+ type = list(string)
+ default = null
+}
+
+variable "project_id" {
+ description = "Project id."
+ type = string
+}
+
+variable "protocol" {
+ description = "Protocol supported by this load balancer."
+ type = string
+ default = "HTTP"
+ nullable = false
+ validation {
+ condition = (
+ var.protocol == null || var.protocol == "HTTP" || var.protocol == "HTTPS"
+ )
+ error_message = "Protocol must be HTTP or HTTPS"
+ }
+}
+
+variable "region" {
+ description = "The region where to allocate the ILB resources."
+ type = string
+}
+
+variable "service_directory_registration" {
+ description = "Service directory namespace and service used to register this load balancer."
+ type = object({
+ namespace = string
+ service = string
+ })
+ default = null
+}
+
+variable "ssl_certificates" {
+ description = "SSL target proxy certificates (only if protocol is HTTPS)."
+ type = object({
+ certificate_ids = optional(list(string), [])
+ create_configs = optional(map(object({
+ certificate = string
+ private_key = string
+ })), {})
+ })
+ default = {}
+ nullable = false
+}
+
+variable "vpc_config" {
+ description = "VPC-level configuration."
+ type = object({
+ network = string
+ subnetwork = string
+ })
+ nullable = false
+}
diff --git a/modules/net-ilb-l7/versions.tf b/modules/net-ilb-l7/versions.tf
new file mode 100644
index 0000000000..90b632f6d4
--- /dev/null
+++ b/modules/net-ilb-l7/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.47.0" # tftest
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.47.0" # tftest
+ }
+ }
+}
+
+
diff --git a/modules/net-ilb/README.md b/modules/net-ilb/README.md
index a48f94afbd..c284c4c631 100644
--- a/modules/net-ilb/README.md
+++ b/modules/net-ilb/README.md
@@ -18,16 +18,18 @@ This examples shows how to create an ILB by combining externally managed instanc
```hcl
module "ilb" {
- source = "./modules/net-ilb"
+ source = "./fabric/modules/net-ilb"
project_id = var.project_id
region = "europe-west1"
name = "ilb-test"
service_label = "ilb-test"
- network = var.vpc.self_link
- subnetwork = var.subnet.self_link
+ vpc_config = {
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ }
group_configs = {
my-group = {
- zone = "europe-west1-b", named_ports = null
+ zone = "europe-west1-b"
instances = [
"instance-1-self-link",
"instance-2-self-link"
@@ -35,12 +37,12 @@ module "ilb" {
}
}
backends = [{
- failover = false
- group = module.ilb.groups.my-group.self_link
- balancing_mode = "CONNECTION"
+ group = module.ilb.groups.my-group.self_link
}]
health_check_config = {
- type = "http", check = { port = 80 }, config = {}, logging = true
+ http = {
+ port = 80
+ }
}
}
# tftest modules=1 resources=4
@@ -58,14 +60,14 @@ Note that the example uses the GCE default service account. You might want to cr
```hcl
module "cos-nginx" {
- source = "./modules/cloud-config-container/nginx"
+ source = "./fabric/modules/cloud-config-container/nginx"
}
module "instance-group" {
- source = "./modules/compute-vm"
- for_each = toset(["b", "c"])
+ source = "./fabric/modules/compute-vm"
+ for_each = toset(["b", "c"])
project_id = var.project_id
- zone = "europe-west1-${each.key}"
+ zone = "europe-west1-${each.key}"
name = "ilb-test-${each.key}"
network_interfaces = [{
network = var.vpc.self_link
@@ -86,23 +88,26 @@ module "instance-group" {
}
module "ilb" {
- source = "./modules/net-ilb"
+ source = "./fabric/modules/net-ilb"
project_id = var.project_id
region = "europe-west1"
name = "ilb-test"
service_label = "ilb-test"
- network = var.vpc.self_link
- subnetwork = var.subnet.self_link
- ports = [80]
+ vpc_config = {
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ }
+ ports = [80]
backends = [
for z, mod in module.instance-group : {
- failover = false
group = mod.group.self_link
- balancing_mode = "CONNECTION"
+ balancing_mode = "UTILIZATION"
}
]
health_check_config = {
- type = "http", check = { port = 80 }, config = {}, logging = true
+ http = {
+ port = 80
+ }
}
}
# tftest modules=3 resources=7
@@ -113,31 +118,30 @@ module "ilb" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [backends](variables.tf#L33) | Load balancer backends, balancing mode is one of 'CONNECTION' or 'UTILIZATION'. | list(object({…}))
| ✓ | |
-| [name](variables.tf#L98) | Name used for all resources. | string
| ✓ | |
-| [network](variables.tf#L103) | Network used for resources. | string
| ✓ | |
-| [project_id](variables.tf#L114) | Project id where resources will be created. | string
| ✓ | |
-| [region](variables.tf#L125) | GCP region. | string
| ✓ | |
-| [subnetwork](variables.tf#L136) | Subnetwork used for the forwarding rule. | string
| ✓ | |
+| [name](variables.tf#L184) | Name used for all resources. | string
| ✓ | |
+| [project_id](variables.tf#L195) | Project id where resources will be created. | string
| ✓ | |
+| [region](variables.tf#L206) | GCP region. | string
| ✓ | |
+| [vpc_config](variables.tf#L217) | VPC-level configuration. | object({…})
| ✓ | |
| [address](variables.tf#L17) | Optional IP address used for the forwarding rule. | string
| | null
|
-| [backend_config](variables.tf#L23) | Optional backend configuration. | object({…})
| | null
|
-| [failover_config](variables.tf#L42) | Optional failover configuration. | object({…})
| | null
|
-| [global_access](variables.tf#L52) | Global access, defaults to false if not set. | bool
| | null
|
-| [group_configs](variables.tf#L58) | Optional unmanaged groups to create. Can be referenced in backends via outputs. | map(object({…}))
| | {}
|
-| [health_check](variables.tf#L68) | Name of existing health check to use, disables auto-created health check. | string
| | null
|
-| [health_check_config](variables.tf#L74) | Configuration of the auto-created helth check. | object({…})
| | {…}
|
-| [labels](variables.tf#L92) | Labels set on resources. | map(string)
| | {}
|
-| [ports](variables.tf#L108) | Comma-separated ports, leave null to use all ports. | list(string)
| | null
|
-| [protocol](variables.tf#L119) | IP protocol used, defaults to TCP. | string
| | "TCP"
|
-| [service_label](variables.tf#L130) | Optional prefix of the fully qualified forwarding rule name. | string
| | null
|
+| [backend_service_config](variables.tf#L23) | Backend service level configuration. | object({…})
| | {}
|
+| [backends](variables.tf#L56) | Load balancer backends, balancing mode is one of 'CONNECTION' or 'UTILIZATION'. | list(object({…}))
| | []
|
+| [description](variables.tf#L75) | Optional description used for resources. | string
| | "Terraform managed."
|
+| [global_access](variables.tf#L81) | Global access, defaults to false if not set. | bool
| | null
|
+| [group_configs](variables.tf#L87) | Optional unmanaged groups to create. Can be referenced in backends via outputs. | map(object({…}))
| | {}
|
+| [health_check](variables.tf#L98) | Name of existing health check to use, disables auto-created health check. | string
| | 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({…})
| | {…}
|
+| [labels](variables.tf#L178) | Labels set on resources. | map(string)
| | {}
|
+| [ports](variables.tf#L189) | Comma-separated ports, leave null to use all ports. | list(string)
| | null
|
+| [protocol](variables.tf#L200) | IP protocol used, defaults to TCP. | string
| | "TCP"
|
+| [service_label](variables.tf#L211) | Optional prefix of the fully qualified forwarding rule name. | string
| | null
|
## Outputs
| name | description | sensitive |
|---|---|:---:|
-| [backend](outputs.tf#L17) | Backend resource. | |
-| [backend_id](outputs.tf#L22) | Backend id. | |
-| [backend_self_link](outputs.tf#L27) | Backend self link. | |
+| [backend_service](outputs.tf#L17) | Backend resource. | |
+| [backend_service_id](outputs.tf#L22) | Backend id. | |
+| [backend_service_self_link](outputs.tf#L27) | Backend self link. | |
| [forwarding_rule](outputs.tf#L32) | Forwarding rule resource. | |
| [forwarding_rule_address](outputs.tf#L37) | Forwarding rule address. | |
| [forwarding_rule_id](outputs.tf#L42) | Forwarding rule id. | |
diff --git a/modules/net-ilb/groups.tf b/modules/net-ilb/groups.tf
new file mode 100644
index 0000000000..fe8bf13d58
--- /dev/null
+++ b/modules/net-ilb/groups.tf
@@ -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.
+ */
+
+# tfdoc:file:description Optional instance group resources.
+
+resource "google_compute_instance_group" "unmanaged" {
+ for_each = var.group_configs
+ project = var.project_id
+ zone = each.value.zone
+ name = each.key
+ description = "Terraform-managed."
+ instances = each.value.instances
+ dynamic "named_port" {
+ for_each = each.value.named_ports
+ content {
+ name = named_port.key
+ port = named_port.value
+ }
+ }
+}
diff --git a/modules/net-ilb/health-check.tf b/modules/net-ilb/health-check.tf
new file mode 100644
index 0000000000..4a4ed40def
--- /dev/null
+++ b/modules/net-ilb/health-check.tf
@@ -0,0 +1,119 @@
+/**
+ * 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:file:description Health check resource.
+
+locals {
+ hc = var.health_check_config
+ hc_grpc = try(local.hc.grpc, null) != null
+ hc_http = try(local.hc.http, null) != null
+ hc_http2 = try(local.hc.http2, null) != null
+ hc_https = try(local.hc.https, null) != null
+ hc_ssl = try(local.hc.ssl, null) != null
+ hc_tcp = try(local.hc.tcp, null) != null
+}
+
+resource "google_compute_health_check" "default" {
+ provider = google-beta
+ count = local.hc != null ? 1 : 0
+ project = var.project_id
+ name = var.name
+ description = local.hc.description
+ check_interval_sec = local.hc.check_interval_sec
+ healthy_threshold = local.hc.healthy_threshold
+ timeout_sec = local.hc.timeout_sec
+ unhealthy_threshold = local.hc.unhealthy_threshold
+
+ dynamic "grpc_health_check" {
+ for_each = local.hc_grpc ? [""] : []
+ content {
+ port = local.hc.grpc.port
+ port_name = local.hc.grpc.port_name
+ port_specification = local.hc.grpc.port_specification
+ grpc_service_name = local.hc.grpc.service_name
+ }
+ }
+
+ dynamic "http_health_check" {
+ for_each = local.hc_http ? [""] : []
+ content {
+ host = local.hc.http.host
+ port = local.hc.http.port
+ port_name = local.hc.http.port_name
+ port_specification = local.hc.http.port_specification
+ proxy_header = local.hc.http.proxy_header
+ request_path = local.hc.http.request_path
+ response = local.hc.http.response
+ }
+ }
+
+ dynamic "http2_health_check" {
+ for_each = local.hc_http2 ? [""] : []
+ content {
+ host = local.hc.http.host
+ port = local.hc.http.port
+ port_name = local.hc.http.port_name
+ port_specification = local.hc.http.port_specification
+ proxy_header = local.hc.http.proxy_header
+ request_path = local.hc.http.request_path
+ response = local.hc.http.response
+ }
+ }
+
+ dynamic "https_health_check" {
+ for_each = local.hc_https ? [""] : []
+ content {
+ host = local.hc.http.host
+ port = local.hc.http.port
+ port_name = local.hc.http.port_name
+ port_specification = local.hc.http.port_specification
+ proxy_header = local.hc.http.proxy_header
+ request_path = local.hc.http.request_path
+ response = local.hc.http.response
+ }
+ }
+
+ dynamic "ssl_health_check" {
+ for_each = local.hc_ssl ? [""] : []
+ content {
+ port = local.hc.tcp.port
+ port_name = local.hc.tcp.port_name
+ port_specification = local.hc.tcp.port_specification
+ proxy_header = local.hc.tcp.proxy_header
+ request = local.hc.tcp.request
+ response = local.hc.tcp.response
+ }
+ }
+
+ dynamic "tcp_health_check" {
+ for_each = local.hc_tcp ? [""] : []
+ content {
+ port = local.hc.tcp.port
+ port_name = local.hc.tcp.port_name
+ port_specification = local.hc.tcp.port_specification
+ proxy_header = local.hc.tcp.proxy_header
+ request = local.hc.tcp.request
+ response = local.hc.tcp.response
+ }
+ }
+
+ dynamic "log_config" {
+ for_each = try(local.hc.enable_logging, null) == true ? [""] : []
+ content {
+ enable = true
+ }
+ }
+}
diff --git a/modules/net-ilb/main.tf b/modules/net-ilb/main.tf
index aa4addcc0c..be4c578690 100644
--- a/modules/net-ilb/main.tf
+++ b/modules/net-ilb/main.tf
@@ -16,252 +16,100 @@
locals {
+ bs_conntrack = var.backend_service_config.connection_tracking
+ bs_failover = var.backend_service_config.failover_config
health_check = (
var.health_check != null
? var.health_check
- : try(local.health_check_resource.self_link, null)
+ : google_compute_health_check.default.0.self_link
)
- health_check_resource = try(
- google_compute_health_check.http.0,
- google_compute_health_check.https.0,
- google_compute_health_check.tcp.0,
- google_compute_health_check.ssl.0,
- google_compute_health_check.http2.0,
- {}
- )
- health_check_type = try(var.health_check_config.type, null)
}
resource "google_compute_forwarding_rule" "default" {
- provider = google-beta
- project = var.project_id
- name = var.name
- description = "Terraform managed."
+ provider = google-beta
+ project = var.project_id
+ region = var.region
+ name = var.name
+ description = var.description
+ ip_address = var.address
+ ip_protocol = var.protocol # TCP | UDP
+ backend_service = (
+ google_compute_region_backend_service.default.self_link
+ )
load_balancing_scheme = "INTERNAL"
- region = var.region
- network = var.network
- subnetwork = var.subnetwork
- ip_address = var.address
- ip_protocol = var.protocol # TCP | UDP
- ports = var.ports # "nnnnn" or "nnnnn,nnnnn,nnnnn" max 5
- service_label = var.service_label
- all_ports = var.ports == null ? true : null
+ network = var.vpc_config.network
+ ports = var.ports # "nnnnn" or "nnnnn,nnnnn,nnnnn" max 5
+ subnetwork = var.vpc_config.subnetwork
allow_global_access = var.global_access
- backend_service = google_compute_region_backend_service.default.self_link
+ labels = var.labels
+ all_ports = var.ports == null ? true : null
+ service_label = var.service_label
# is_mirroring_collector = false
- labels = var.labels
}
resource "google_compute_region_backend_service" "default" {
- provider = google-beta
- project = var.project_id
- name = var.name
- description = "Terraform managed."
- load_balancing_scheme = "INTERNAL"
- region = var.region
- network = var.network
- health_checks = [local.health_check]
- protocol = var.protocol
-
- session_affinity = try(var.backend_config.session_affinity, null)
- timeout_sec = try(var.backend_config.timeout_sec, null)
- connection_draining_timeout_sec = try(var.backend_config.connection_draining_timeout_sec, null)
+ provider = google-beta
+ project = var.project_id
+ region = var.region
+ name = var.name
+ description = var.description
+ load_balancing_scheme = "INTERNAL"
+ protocol = var.protocol
+ network = var.vpc_config.network
+ health_checks = [local.health_check]
+ connection_draining_timeout_sec = var.backend_service_config.connection_draining_timeout_sec
+ session_affinity = var.backend_service_config.session_affinity
+ timeout_sec = var.backend_service_config.timeout_sec
dynamic "backend" {
for_each = { for b in var.backends : b.group => b }
- iterator = backend
content {
balancing_mode = backend.value.balancing_mode
- description = "Terraform managed."
+ description = backend.value.description
failover = backend.value.failover
group = backend.key
}
}
- dynamic "failover_policy" {
- for_each = var.failover_config == null ? [] : [var.failover_config]
- iterator = config
- content {
- disable_connection_drain_on_failover = config.value.disable_connection_drain
- drop_traffic_if_unhealthy = config.value.drop_traffic_if_unhealthy
- failover_ratio = config.value.ratio
- }
- }
-
-}
-
-resource "google_compute_instance_group" "unmanaged" {
- for_each = var.group_configs
- project = var.project_id
- zone = each.value.zone
- name = each.key
- description = "Terraform-managed."
- instances = each.value.instances
- dynamic "named_port" {
- for_each = each.value.named_ports != null ? each.value.named_ports : {}
- iterator = config
- content {
- name = config.key
- port = config.value
- }
- }
-}
-
-resource "google_compute_health_check" "http" {
- provider = google-beta
- count = (
- var.health_check == null && local.health_check_type == "http" ? 1 : 0
- )
- project = var.project_id
- name = var.name
- description = "Terraform managed."
-
- check_interval_sec = try(var.health_check_config.config.check_interval_sec, null)
- healthy_threshold = try(var.health_check_config.config.healthy_threshold, null)
- timeout_sec = try(var.health_check_config.config.timeout_sec, null)
- unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null)
-
- http_health_check {
- host = try(var.health_check_config.check.host, null)
- port = try(var.health_check_config.check.port, null)
- port_name = try(var.health_check_config.check.port_name, null)
- port_specification = try(var.health_check_config.check.port_specification, null)
- proxy_header = try(var.health_check_config.check.proxy_header, null)
- request_path = try(var.health_check_config.check.request_path, null)
- response = try(var.health_check_config.check.response, null)
- }
-
- dynamic "log_config" {
- for_each = try(var.health_check_config.logging, false) ? [""] : []
+ dynamic "connection_tracking_policy" {
+ for_each = local.bs_conntrack == null ? [] : [""]
content {
- enable = true
+ connection_persistence_on_unhealthy_backends = (
+ local.bs_conntrack.persist_conn_on_unhealthy != null
+ ? local.bs_conntrack.persist_conn_on_unhealthy
+ : null
+ )
+ idle_timeout_sec = local.bs_conntrack.idle_timeout_sec
+ tracking_mode = (
+ local.bs_conntrack.track_per_session != null
+ ? local.bs_conntrack.track_per_session
+ : null
+ )
}
}
-}
-
-resource "google_compute_health_check" "https" {
- provider = google-beta
- count = (
- var.health_check == null && local.health_check_type == "https" ? 1 : 0
- )
- project = var.project_id
- name = var.name
- description = "Terraform managed."
- check_interval_sec = try(var.health_check_config.config.check_interval_sec, null)
- healthy_threshold = try(var.health_check_config.config.healthy_threshold, null)
- timeout_sec = try(var.health_check_config.config.timeout_sec, null)
- unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null)
-
- https_health_check {
- host = try(var.health_check_config.check.host, null)
- port = try(var.health_check_config.check.port, null)
- port_name = try(var.health_check_config.check.port_name, null)
- port_specification = try(var.health_check_config.check.port_specification, null)
- proxy_header = try(var.health_check_config.check.proxy_header, null)
- request_path = try(var.health_check_config.check.request_path, null)
- response = try(var.health_check_config.check.response, null)
- }
-
- dynamic "log_config" {
- for_each = try(var.health_check_config.logging, false) ? [""] : []
+ dynamic "failover_policy" {
+ for_each = local.bs_failover == null ? [] : [""]
content {
- enable = true
+ disable_connection_drain_on_failover = local.bs_failover.disable_conn_drain
+ drop_traffic_if_unhealthy = local.bs_failover.drop_traffic_if_unhealthy
+ failover_ratio = local.bs_failover.ratio
}
}
-}
-
-resource "google_compute_health_check" "tcp" {
- provider = google-beta
- count = (
- var.health_check == null && local.health_check_type == "tcp" ? 1 : 0
- )
- project = var.project_id
- name = var.name
- description = "Terraform managed."
-
- check_interval_sec = try(var.health_check_config.config.check_interval_sec, null)
- healthy_threshold = try(var.health_check_config.config.healthy_threshold, null)
- timeout_sec = try(var.health_check_config.config.timeout_sec, null)
- unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null)
-
- tcp_health_check {
- port = try(var.health_check_config.check.port, null)
- port_name = try(var.health_check_config.check.port_name, null)
- port_specification = try(var.health_check_config.check.port_specification, null)
- proxy_header = try(var.health_check_config.check.proxy_header, null)
- request = try(var.health_check_config.check.request, null)
- response = try(var.health_check_config.check.response, null)
- }
dynamic "log_config" {
- for_each = try(var.health_check_config.logging, false) ? [""] : []
+ for_each = var.backend_service_config.log_sample_rate == null ? [] : [""]
content {
- enable = true
+ enable = true
+ sample_rate = var.backend_service_config.log_sample_rate
}
}
-}
-
-resource "google_compute_health_check" "ssl" {
- provider = google-beta
- count = (
- var.health_check == null && local.health_check_type == "ssl" ? 1 : 0
- )
- project = var.project_id
- name = var.name
- description = "Terraform managed."
- check_interval_sec = try(var.health_check_config.config.check_interval_sec, null)
- healthy_threshold = try(var.health_check_config.config.healthy_threshold, null)
- timeout_sec = try(var.health_check_config.config.timeout_sec, null)
- unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null)
-
- ssl_health_check {
- port = try(var.health_check_config.check.port, null)
- port_name = try(var.health_check_config.check.port_name, null)
- port_specification = try(var.health_check_config.check.port_specification, null)
- proxy_header = try(var.health_check_config.check.proxy_header, null)
- request = try(var.health_check_config.check.request, null)
- response = try(var.health_check_config.check.response, null)
- }
-
- dynamic "log_config" {
- for_each = try(var.health_check_config.logging, false) ? [""] : []
+ dynamic "subsetting" {
+ for_each = var.backend_service_config.enable_subsetting == true ? [""] : []
content {
- enable = true
+ policy = "CONSISTENT_HASH_SUBSETTING"
}
}
-}
-
-resource "google_compute_health_check" "http2" {
- provider = google-beta
- count = (
- var.health_check == null && local.health_check_type == "http2" ? 1 : 0
- )
- project = var.project_id
- name = var.name
- description = "Terraform managed."
- check_interval_sec = try(var.health_check_config.config.check_interval_sec, null)
- healthy_threshold = try(var.health_check_config.config.healthy_threshold, null)
- timeout_sec = try(var.health_check_config.config.timeout_sec, null)
- unhealthy_threshold = try(var.health_check_config.config.unhealthy_threshold, null)
-
- http2_health_check {
- host = try(var.health_check_config.check.host, null)
- port = try(var.health_check_config.check.port, null)
- port_name = try(var.health_check_config.check.port_name, null)
- port_specification = try(var.health_check_config.check.port_specification, null)
- proxy_header = try(var.health_check_config.check.proxy_header, null)
- request_path = try(var.health_check_config.check.request_path, null)
- response = try(var.health_check_config.check.response, null)
- }
-
- dynamic "log_config" {
- for_each = try(var.health_check_config.logging, false) ? [""] : []
- content {
- enable = true
- }
- }
}
-
diff --git a/modules/net-ilb/outputs.tf b/modules/net-ilb/outputs.tf
index 55b454e181..c97612f53d 100644
--- a/modules/net-ilb/outputs.tf
+++ b/modules/net-ilb/outputs.tf
@@ -14,17 +14,17 @@
* limitations under the License.
*/
-output "backend" {
+output "backend_service" {
description = "Backend resource."
value = google_compute_region_backend_service.default
}
-output "backend_id" {
+output "backend_service_id" {
description = "Backend id."
value = google_compute_region_backend_service.default.id
}
-output "backend_self_link" {
+output "backend_service_self_link" {
description = "Backend self link."
value = google_compute_region_backend_service.default.self_link
}
@@ -63,15 +63,15 @@ output "groups" {
output "health_check" {
description = "Auto-created health-check resource."
- value = local.health_check_resource
+ value = try(google_compute_health_check.default.0, null)
}
output "health_check_self_id" {
description = "Auto-created health-check self id."
- value = try(local.health_check_resource.id, null)
+ value = try(google_compute_health_check.default.0.id, null)
}
output "health_check_self_link" {
description = "Auto-created health-check self link."
- value = try(local.health_check_resource.self_link, null)
+ value = try(google_compute_health_check.default.0.self_link, null)
}
diff --git a/modules/net-ilb/variables.tf b/modules/net-ilb/variables.tf
index 638aee5219..d2ffc5a672 100644
--- a/modules/net-ilb/variables.tf
+++ b/modules/net-ilb/variables.tf
@@ -20,33 +20,62 @@ variable "address" {
default = null
}
-variable "backend_config" {
- description = "Optional backend configuration."
+variable "backend_service_config" {
+ description = "Backend service level configuration."
type = object({
- session_affinity = string
- timeout_sec = number
- connection_draining_timeout_sec = number
+ connection_draining_timeout_sec = optional(number)
+ connection_tracking = optional(object({
+ idle_timeout_sec = optional(number)
+ persist_conn_on_unhealthy = optional(string)
+ track_per_session = optional(bool)
+ }))
+ enable_subsetting = optional(bool)
+ failover_config = optional(object({
+ disable_conn_drain = optional(bool)
+ drop_traffic_if_unhealthy = optional(bool)
+ ratio = optional(number)
+ }))
+ log_sample_rate = optional(number)
+ session_affinity = optional(string)
+ timeout_sec = optional(number)
})
- default = null
+ default = {}
+ nullable = false
+ validation {
+ condition = contains(
+ [
+ "NONE", "CLIENT_IP", "CLIENT_IP_NO_DESTINATION",
+ "CLIENT_IP_PORT_PROTO", "CLIENT_IP_PROTO"
+ ],
+ coalesce(var.backend_service_config.session_affinity, "NONE")
+ )
+ error_message = "Invalid session affinity value."
+ }
}
variable "backends" {
description = "Load balancer backends, balancing mode is one of 'CONNECTION' or 'UTILIZATION'."
type = list(object({
- failover = bool
group = string
- balancing_mode = string
+ balancing_mode = optional(string, "CONNECTION")
+ description = optional(string, "Terraform managed.")
+ failover = optional(bool, false)
}))
+ default = []
+ nullable = false
+ validation {
+ condition = alltrue([
+ for b in var.backends : contains(
+ ["CONNECTION", "UTILIZATION"], coalesce(b.balancing_mode, "CONNECTION")
+ )])
+ error_message = "When specified balancing mode needs to be 'CONNECTION' or 'UTILIZATION'."
+ }
}
-variable "failover_config" {
- description = "Optional failover configuration."
- type = object({
- disable_connection_drain = bool
- drop_traffic_if_unhealthy = bool
- ratio = number
- })
- default = null
+variable "description" {
+ description = "Optional description used for resources."
+ type = string
+ default = "Terraform managed."
}
variable "global_access" {
@@ -58,11 +87,12 @@ variable "global_access" {
variable "group_configs" {
description = "Optional unmanaged groups to create. Can be referenced in backends via outputs."
type = map(object({
- instances = list(string)
- named_ports = map(number)
zone = string
+ instances = optional(list(string), [])
+ named_ports = optional(map(number), {})
}))
- default = {}
+ default = {}
+ nullable = false
}
variable "health_check" {
@@ -72,20 +102,76 @@ variable "health_check" {
}
variable "health_check_config" {
- description = "Configuration of the auto-created helth check."
+ description = "Optional auto-created health check configuration, use the output self-link to set it in the auto healing policy. Refer to examples for usage."
type = object({
- type = string # http https tcp ssl http2
- check = map(any) # actual health check block attributes
- config = map(number) # interval, thresholds, timeout
- logging = bool
+ check_interval_sec = optional(number)
+ description = optional(string, "Terraform managed.")
+ enable_logging = optional(bool, false)
+ healthy_threshold = optional(number)
+ timeout_sec = optional(number)
+ unhealthy_threshold = optional(number)
+ grpc = optional(object({
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ service_name = optional(string)
+ }))
+ http = optional(object({
+ host = optional(string)
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request_path = optional(string)
+ response = optional(string)
+ }))
+ http2 = optional(object({
+ host = optional(string)
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request_path = optional(string)
+ response = optional(string)
+ }))
+ https = optional(object({
+ host = optional(string)
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request_path = optional(string)
+ response = optional(string)
+ }))
+ tcp = optional(object({
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request = optional(string)
+ response = optional(string)
+ }))
+ ssl = optional(object({
+ port = optional(number)
+ port_name = optional(string)
+ port_specification = optional(string) # USE_FIXED_PORT USE_NAMED_PORT USE_SERVING_PORT
+ proxy_header = optional(string)
+ request = optional(string)
+ response = optional(string)
+ }))
})
default = {
- type = "http"
- check = {
+ tcp = {
port_specification = "USE_SERVING_PORT"
}
- config = {}
- logging = false
+ }
+ validation {
+ 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
+ )
+ error_message = "Only one health check type can be configured at a time."
}
}
@@ -100,11 +186,6 @@ variable "name" {
type = string
}
-variable "network" {
- description = "Network used for resources."
- type = string
-}
-
variable "ports" {
description = "Comma-separated ports, leave null to use all ports."
type = list(string)
@@ -133,7 +214,11 @@ variable "service_label" {
default = null
}
-variable "subnetwork" {
- description = "Subnetwork used for the forwarding rule."
- type = string
+variable "vpc_config" {
+ description = "VPC-level configuration."
+ type = object({
+ network = string
+ subnetwork = string
+ })
+ nullable = false
}
diff --git a/modules/net-ilb/versions.tf b/modules/net-ilb/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/net-ilb/versions.tf
+++ b/modules/net-ilb/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/net-interconnect-attachment-direct/README.md b/modules/net-interconnect-attachment-direct/README.md
index 3b77372e20..6a5244c79d 100644
--- a/modules/net-interconnect-attachment-direct/README.md
+++ b/modules/net-interconnect-attachment-direct/README.md
@@ -8,7 +8,7 @@ This module allows creation of a VLAN attachment for Direct Interconnect and rou
```hcl
module "vlan-attachment-1" {
- source = "./modules/net-interconnect-attachment-direct"
+ source = "./fabric/modules/net-interconnect-attachment-direct"
project_id = "dedicated-ic-5-8492"
region = "us-west2"
router_network = "myvpc"
@@ -25,7 +25,7 @@ module "vlan-attachment-1" {
```hcl
module "vlan-attachment-1" {
- source = "./modules/net-interconnect-attachment-direct"
+ source = "./fabric/modules/net-interconnect-attachment-direct"
project_id = "dedicated-ic-3-8386"
region = "us-west2"
router_name = "router-1"
@@ -63,7 +63,7 @@ module "vlan-attachment-1" {
}
module "vlan-attachment-2" {
- source = "./modules/net-interconnect-attachment-direct"
+ source = "./fabric/modules/net-interconnect-attachment-direct"
project_id = "dedicated-ic-3-8386"
region = "us-west2"
router_name = "router-2"
diff --git a/modules/net-interconnect-attachment-direct/versions.tf b/modules/net-interconnect-attachment-direct/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/net-interconnect-attachment-direct/versions.tf
+++ b/modules/net-interconnect-attachment-direct/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/net-vpc-firewall/README.md b/modules/net-vpc-firewall/README.md
index 75f6d48c43..f886035ba7 100644
--- a/modules/net-vpc-firewall/README.md
+++ b/modules/net-vpc-firewall/README.md
@@ -2,11 +2,10 @@
This module allows creation and management of different types of firewall rules for a single VPC network:
-- blanket ingress rules based on IP ranges that allow all traffic via the `admin_ranges` variable
-- simplified tag-based ingress rules for the HTTP, HTTPS and SSH protocols via the `xxx_source_ranges` variables; HTTP and HTTPS tags match those set by the console via the "Allow HTTP(S) traffic" instance flags
-- custom rules via the `custom_rules` variables
+- custom rules via the `egress_rules` and `ingress_rules` variables
+- optional predefined rules that simplify prototyping via the `default_rules_config` variable
-The simplified tag-based rules are enabled by default, set to the ranges of the GCP health checkers for HTTP/HTTPS, and the IAP forwarders for SSH. To disable them set the corresponding variables to empty lists.
+The predefined rules are enabled by default and set to the ranges of the GCP health checkers for HTTP/HTTPS, and the IAP forwarders for SSH. See the relevant section below on how to configure or disable them.
## Examples
@@ -16,10 +15,12 @@ This is often useful for prototyping or testing infrastructure, allowing open in
```hcl
module "firewall" {
- source = "./modules/net-vpc-firewall"
- project_id = "my-project"
- network = "my-network"
- admin_ranges = ["10.0.0.0/8"]
+ source = "./fabric/modules/net-vpc-firewall"
+ project_id = "my-project"
+ network = "my-network"
+ default_rules_config = {
+ admin_ranges = ["10.0.0.0/8"]
+ }
}
# tftest modules=1 resources=4
```
@@ -28,100 +29,172 @@ module "firewall" {
This is an example of how to define custom rules, with a sample rule allowing open ingress for the NTP protocol to instances with the `ntp-svc` tag.
+Some implicit defaults are used in the rules variable types and can be controlled by explicitly setting specific attributes:
+
+- 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 or set to `null`, to disable the behaviour set ranges to the empty list (`[]`)
+- rules default to all protocols if not set
+
```hcl
module "firewall" {
- source = "./modules/net-vpc-firewall"
- project_id = "my-project"
- network = "my-network"
- admin_ranges = ["10.0.0.0/8"]
- custom_rules = {
- ntp-svc = {
- description = "NTP service."
- direction = "INGRESS"
- action = "allow"
- sources = []
- ranges = ["0.0.0.0/0"]
- targets = ["ntp-svc"]
- use_service_accounts = false
- rules = [{ protocol = "udp", ports = [123] }]
- extra_attributes = {}
+ source = "./fabric/modules/net-vpc-firewall"
+ project_id = "my-project"
+ network = "my-network"
+ default_rules_config = {
+ admin_ranges = ["10.0.0.0/8"]
+ }
+ egress_rules = {
+ # implicit deny action
+ 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 = {
+ # implicit allow action
+ 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"]
}
}
}
-# tftest modules=1 resources=5
+# tftest modules=1 resources=9
```
-### No predefined rules
+### Controlling or turning off default rules
+
+Predefined rules can be controlled or turned off via the `default_rules_config` variable.
+
+#### Overriding default tags and ranges
+
+Each protocol rule has a default set of tags and ranges:
-If you don't want any predefined rules set `admin_ranges`, `http_source_ranges`, `https_source_ranges` and `ssh_source_ranges` to an empty list.
+- the health check range and the `http-server`/`https-server` tag for HTTP/HTTPS, matching tags set via GCP console flags on GCE instances
+- the IAP forwarders range and `ssh` tag for SSH
+
+Default tags and ranges can be overridden for each protocol, like shown here for SSH:
```hcl
module "firewall" {
- source = "./modules/net-vpc-firewall"
- project_id = "my-project"
- network = "my-network"
- admin_ranges = []
- http_source_ranges = []
- https_source_ranges = []
- ssh_source_ranges = []
- custom_rules = {
- allow-https = {
- description = "Allow HTTPS from internal networks."
- direction = "INGRESS"
- action = "allow"
- sources = []
- ranges = ["rfc1918"]
- targets = []
- use_service_accounts = false
- rules = [{ protocol = "tcp", ports = [443] }]
- extra_attributes = {}
- }
+ source = "./fabric/modules/net-vpc-firewall"
+ project_id = "my-project"
+ network = "my-network"
+ default_rules_config = {
+ ssh_ranges = ["10.0.0.0/8"]
+ ssh_tags = ["ssh-default"]
}
}
-# tftest modules=1 resources=1
+# tftest modules=1 resources=3
```
+#### Disabling predefined rules
+
+Default rules can be disabled individually by specifying an empty set of ranges:
+
+```hcl
+module "firewall" {
+ source = "./fabric/modules/net-vpc-firewall"
+ project_id = "my-project"
+ network = "my-network"
+ default_rules_config = {
+ ssh_ranges = []
+ }
+}
+# tftest modules=1 resources=2
+```
+
+Or the entire set of rules can be disabled via the `disabled` attribute:
+
+```hcl
+module "firewall" {
+ source = "./fabric/modules/net-vpc-firewall"
+ project_id = "my-project"
+ network = "my-network"
+ default_rules_config = {
+ disabled = true
+ }
+}
+# tftest modules=0 resources=0
+```
### Rules Factory
-The module includes a rules factory (see [Resource Factories](../../examples/factories/)) for the massive creation of rules leveraging YaML configuration files. Each configuration file can optionally contain more than one rule which a structure that reflects the `custom_rules` variable.
+
+The module includes a rules factory (see [Resource Factories](../../blueprints/factories/)) for the massive creation of rules leveraging YaML configuration files. Each configuration file can optionally contain more than one rule which a structure that reflects the `custom_rules` variable.
```hcl
module "firewall" {
- source = "./modules/net-vpc-firewall"
- project_id = "my-project"
- network = "my-network"
- data_folder = "config/firewall"
- cidr_template_file = "config/cidr_template.yaml"
+ source = "./fabric/modules/net-vpc-firewall"
+ project_id = "my-project"
+ network = "my-network"
+ factories_config = {
+ rules_folder = "configs/firewall/rules"
+ cidr_tpl_file = "configs/firewall/cidrs.yaml"
+ }
+ default_rules_config = { disabled = true }
}
-# tftest skip
+# tftest modules=1 resources=3 files=lbs,cidrs
```
```yaml
-# ./config/firewall/load_balancers.yaml
-allow-healthchecks:
- description: Allow ingress from healthchecks.
- direction: INGRESS
- action: allow
- sources: []
- ranges:
- - $healthchecks
- targets: ["lb-backends"]
- use_service_accounts: false
- 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
-# ./config/cidr_template.yaml
+# tftest-file id=cidrs path=configs/firewall/cidrs.yaml
healthchecks:
- 35.191.0.0/16
- 130.211.0.0/22
- 209.85.152.0/22
- 209.85.204.0/22
-
```
@@ -129,27 +202,19 @@ healthchecks:
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [network](variables.tf#L80) | Name of the network this set of firewall rules applies to. | string
| ✓ | |
-| [project_id](variables.tf#L85) | Project id of the project that holds the network. | string
| ✓ | |
-| [admin_ranges](variables.tf#L17) | IP CIDR ranges that have complete access to all subnets. | list(string)
| | []
|
-| [cidr_template_file](variables.tf#L23) | Path for optional file containing name->cidr_list map to be used by the rules factory. | string
| | null
|
-| [custom_rules](variables.tf#L29) | List of custom rule definitions (refer to variables file for syntax). | map(object({…}))
| | {}
|
-| [data_folder](variables.tf#L48) | Path for optional folder containing firewall rules defined as YaML objects used by the rules factory. | string
| | null
|
-| [http_source_ranges](variables.tf#L54) | List of IP CIDR ranges for tag-based HTTP rule, defaults to the health checkers ranges. | list(string)
| | ["35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"]
|
-| [https_source_ranges](variables.tf#L60) | List of IP CIDR ranges for tag-based HTTPS rule, defaults to the health checkers ranges. | list(string)
| | ["35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"]
|
-| [named_ranges](variables.tf#L66) | Names that can be used of valid values for the `ranges` field of `custom_rules`. | map(list(string))
| | {…}
|
-| [ssh_source_ranges](variables.tf#L90) | List of IP CIDR ranges for tag-based SSH rule, defaults to the IAP forwarders range. | list(string)
| | ["35.235.240.0/20"]
|
+| [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. 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
| name | description | sensitive |
|---|---|:---:|
-| [admin_ranges](outputs.tf#L17) | Admin ranges data.
- value = { enabled = length(var.admin_ranges) > 0 ranges = join(",", var.admin_ranges) } | |
-| [custom_egress_allow_rules](outputs.tf#L26) | Custom egress rules with allow blocks. | |
-| [custom_egress_deny_rules](outputs.tf#L34) | Custom egress rules with allow blocks. | |
-| [custom_ingress_allow_rules](outputs.tf#L42) | Custom ingress rules with allow blocks. | |
-| [custom_ingress_deny_rules](outputs.tf#L50) | Custom ingress rules with deny blocks. | |
-| [rules](outputs.tf#L58) | All google_compute_firewall resources created. | |
+| [default_rules](outputs.tf#L17) | Default rule resources. | |
+| [rules](outputs.tf#L27) | Custom rule resources. | |
diff --git a/modules/net-vpc-firewall/default-rules.tf b/modules/net-vpc-firewall/default-rules.tf
new file mode 100644
index 0000000000..bbca6dd57a
--- /dev/null
+++ b/modules/net-vpc-firewall/default-rules.tf
@@ -0,0 +1,77 @@
+/**
+ * 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:file:description Optional default rule resources.
+
+locals {
+ default_rules = {
+ for k, v in var.default_rules_config :
+ k => var.default_rules_config.disabled == true || v == null ? [] : v
+ if k != "disabled"
+ }
+}
+
+resource "google_compute_firewall" "allow-admins" {
+ count = length(local.default_rules.admin_ranges) > 0 ? 1 : 0
+ name = "${var.network}-ingress-admins"
+ description = "Access from the admin subnet to all subnets."
+ network = var.network
+ project = var.project_id
+ source_ranges = local.default_rules.admin_ranges
+ allow { protocol = "all" }
+}
+
+resource "google_compute_firewall" "allow-tag-http" {
+ count = length(local.default_rules.http_ranges) > 0 ? 1 : 0
+ name = "${var.network}-ingress-tag-http"
+ description = "Allow http to machines with matching tags."
+ network = var.network
+ project = var.project_id
+ source_ranges = local.default_rules.http_ranges
+ target_tags = local.default_rules.http_tags
+ allow {
+ protocol = "tcp"
+ ports = ["80"]
+ }
+}
+
+resource "google_compute_firewall" "allow-tag-https" {
+ count = length(local.default_rules.https_ranges) > 0 ? 1 : 0
+ name = "${var.network}-ingress-tag-https"
+ description = "Allow http to machines with matching tags."
+ network = var.network
+ project = var.project_id
+ source_ranges = local.default_rules.https_ranges
+ target_tags = local.default_rules.https_tags
+ allow {
+ protocol = "tcp"
+ ports = ["443"]
+ }
+}
+
+resource "google_compute_firewall" "allow-tag-ssh" {
+ count = length(local.default_rules.ssh_ranges) > 0 ? 1 : 0
+ name = "${var.network}-ingress-tag-ssh"
+ description = "Allow SSH to machines with matching tags."
+ network = var.network
+ project = var.project_id
+ source_ranges = local.default_rules.ssh_ranges
+ target_tags = local.default_rules.ssh_tags
+ allow {
+ protocol = "tcp"
+ ports = ["22"]
+ }
+}
diff --git a/modules/net-vpc-firewall/main.tf b/modules/net-vpc-firewall/main.tf
index c08b2d1651..e525ceb4b4 100644
--- a/modules/net-vpc-firewall/main.tf
+++ b/modules/net-vpc-firewall/main.tf
@@ -15,149 +15,135 @@
*/
locals {
- _custom_rules = {
- for id, rule in var.custom_rules :
- id => merge(rule, {
- # make rules a map so we use it in a for_each
- rules = { for index, ports in rule.rules : index => ports }
- # lookup any named ranges references
- ranges = flatten([
- for range in rule.ranges :
- try(var.named_ranges[range], range)
- ])
- })
- }
-
- cidrs = try({
- for name, cidrs in yamldecode(file(var.cidr_template_file)) :
- name => cidrs
- }, {})
-
- _factory_rules_raw = flatten([
- for file in try(fileset(var.data_folder, "**/*.yaml"), []) : [
- for key, ruleset in yamldecode(file("${var.data_folder}/${file}")) :
- merge(ruleset, {
- name = "${key}"
- rules = { for index, ports in ruleset.rules : index => ports }
- ranges = try(ruleset.ranges, null) == null ? null : flatten(
- [for cidr in ruleset.ranges :
- can(regex("^\\$", cidr))
- ? local.cidrs[trimprefix(cidr, "$")]
- : [cidr]
- ])
- extra_attributes = try(ruleset.extra_attributes, {})
- })
+ # define list of rule files
+ _factory_rule_files = [
+ for f in try(fileset(var.factories_config.rules_folder, "**/*.yaml"), []) :
+ "${var.factories_config.rules_folder}/${f}"
+ ]
+ # decode rule files and account for optional attributes
+ _factory_rule_list = flatten([
+ for f in local._factory_rule_files : [
+ for direction, ruleset in yamldecode(file(f)) : [
+ for name, rule in ruleset : {
+ name = name
+ deny = try(rule.deny, false)
+ rules = try(rule.rules, [{ protocol = "all" }])
+ description = try(rule.description, null)
+ destination_ranges = try(rule.destination_ranges, null)
+ direction = upper(direction)
+ disabled = try(rule.disabled, null)
+ enable_logging = try(rule.enable_logging, null)
+ priority = try(rule.priority, 1000)
+ source_ranges = try(rule.source_ranges, null)
+ sources = try(rule.sources, null)
+ targets = try(rule.targets, null)
+ use_service_accounts = try(rule.use_service_accounts, false)
+ }
+ ]
]
])
-
_factory_rules = {
- for d in local._factory_rules_raw : d["name"] => d
+ for r in local._factory_rule_list : r.name => r
+ if contains(["EGRESS", "INGRESS"], r.direction)
}
-
- custom_rules = merge(local._custom_rules, local._factory_rules)
-}
-
-
-###############################################################################
-# rules based on IP ranges
-###############################################################################
-
-resource "google_compute_firewall" "allow-admins" {
- count = length(var.admin_ranges) > 0 ? 1 : 0
- name = "${var.network}-ingress-admins"
- description = "Access from the admin subnet to all subnets"
- network = var.network
- project = var.project_id
- source_ranges = var.admin_ranges
- allow { protocol = "all" }
-}
-
-###############################################################################
-# rules based on tags
-###############################################################################
-
-resource "google_compute_firewall" "allow-tag-ssh" {
- count = length(var.ssh_source_ranges) > 0 ? 1 : 0
- name = "${var.network}-ingress-tag-ssh"
- description = "Allow SSH to machines with the 'ssh' tag"
- network = var.network
- project = var.project_id
- source_ranges = var.ssh_source_ranges
- target_tags = ["ssh"]
- allow {
- protocol = "tcp"
- ports = ["22"]
+ _named_ranges = merge(
+ try(yamldecode(file(var.factories_config.cidr_tpl_file)), {}),
+ var.named_ranges
+ )
+ _rules = merge(
+ local._factory_rules, local._rules_egress, local._rules_ingress
+ )
+ _rules_egress = {
+ for name, rule in merge(var.egress_rules) :
+ name => merge(rule, { direction = "EGRESS" })
}
-}
-
-resource "google_compute_firewall" "allow-tag-http" {
- count = length(var.http_source_ranges) > 0 ? 1 : 0
- name = "${var.network}-ingress-tag-http"
- description = "Allow HTTP to machines with the 'http-server' tag"
- network = var.network
- project = var.project_id
- source_ranges = var.http_source_ranges
- target_tags = ["http-server"]
- allow {
- protocol = "tcp"
- ports = ["80"]
+ _rules_ingress = {
+ for name, rule in merge(var.ingress_rules) :
+ name => merge(rule, { direction = "INGRESS" })
}
-}
-
-resource "google_compute_firewall" "allow-tag-https" {
- count = length(var.https_source_ranges) > 0 ? 1 : 0
- name = "${var.network}-ingress-tag-https"
- description = "Allow HTTPS to machines with the 'https' tag"
- network = var.network
- project = var.project_id
- source_ranges = var.https_source_ranges
- target_tags = ["https-server"]
- allow {
- protocol = "tcp"
- ports = ["443"]
+ # convert rules data to resource format and replace range template variables
+ rules = {
+ for name, rule in local._rules :
+ name => merge(rule, {
+ action = rule.deny == true ? "DENY" : "ALLOW"
+ 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 = (
+ try(rule.source_ranges, null) == null
+ ? null
+ : flatten([
+ for range in rule.source_ranges :
+ try(local._named_ranges[range], range)
+ ])
+ )
+ })
}
}
-################################################################################
-# dynamic rules #
-################################################################################
-
resource "google_compute_firewall" "custom-rules" {
- # provider = "google-beta"
- for_each = local.custom_rules
+ for_each = local.rules
+ project = var.project_id
+ network = var.network
name = each.key
description = each.value.description
direction = each.value.direction
- network = var.network
- project = var.project_id
source_ranges = (
each.value.direction == "INGRESS"
- ? coalesce(each.value.ranges, []) == [] ? ["0.0.0.0/0"] : each.value.ranges
+ ? (
+ each.value.source_ranges == null
+ ? ["0.0.0.0/0"]
+ : each.value.source_ranges
+ )
: null
)
destination_ranges = (
each.value.direction == "EGRESS"
- ? coalesce(each.value.ranges, []) == [] ? ["0.0.0.0/0"] : each.value.ranges
+ ? (
+ each.value.destination_ranges == null
+ ? ["0.0.0.0/0"]
+ : each.value.destination_ranges
+ )
+ : null
+ )
+ source_tags = (
+ each.value.use_service_accounts || each.value.direction == "EGRESS"
+ ? null
+ : each.value.sources
+ )
+ source_service_accounts = (
+ each.value.use_service_accounts && each.value.direction == "INGRESS"
+ ? each.value.sources
: null
)
- source_tags = each.value.use_service_accounts || each.value.direction == "EGRESS" ? null : each.value.sources
- source_service_accounts = each.value.use_service_accounts && each.value.direction == "INGRESS" ? each.value.sources : null
- target_tags = each.value.use_service_accounts ? null : each.value.targets
- target_service_accounts = each.value.use_service_accounts ? each.value.targets : null
- disabled = lookup(each.value.extra_attributes, "disabled", false)
- priority = lookup(each.value.extra_attributes, "priority", 1000)
+ target_tags = (
+ each.value.use_service_accounts ? null : each.value.targets
+ )
+ target_service_accounts = (
+ each.value.use_service_accounts ? each.value.targets : null
+ )
+ disabled = each.value.disabled == true
+ priority = each.value.priority
dynamic "log_config" {
- for_each = lookup(each.value.extra_attributes, "logging", null) != null ? [each.value.extra_attributes.logging] : []
- iterator = logging_config
+ for_each = each.value.enable_logging == null ? [] : [""]
content {
- metadata = logging_config.value
+ metadata = (
+ try(each.value.enable_logging.include_metadata, null) == true
+ ? "INCLUDE_ALL_METADATA"
+ : "EXCLUDE_ALL_METADATA"
+ )
}
}
dynamic "deny" {
- for_each = each.value.action == "deny" ? each.value.rules : {}
-
+ for_each = each.value.action == "DENY" ? each.value.rules : {}
iterator = rule
content {
protocol = rule.value.protocol
@@ -166,8 +152,7 @@ resource "google_compute_firewall" "custom-rules" {
}
dynamic "allow" {
- for_each = each.value.action == "allow" ? each.value.rules : {}
-
+ for_each = each.value.action == "ALLOW" ? each.value.rules : {}
iterator = rule
content {
protocol = rule.value.protocol
diff --git a/modules/net-vpc-firewall/outputs.tf b/modules/net-vpc-firewall/outputs.tf
index f784583c53..9206ab5464 100644
--- a/modules/net-vpc-firewall/outputs.tf
+++ b/modules/net-vpc-firewall/outputs.tf
@@ -14,54 +14,17 @@
* limitations under the License.
*/
-output "admin_ranges" {
- description = "Admin ranges data."
-
+output "default_rules" {
+ description = "Default rule resources."
value = {
- enabled = length(var.admin_ranges) > 0
- ranges = join(",", var.admin_ranges)
+ admin = try(google_compute_firewall.allow-admins, null)
+ http = try(google_compute_firewall.allow-tag-http, null)
+ https = try(google_compute_firewall.allow-tag-https, null)
+ ssh = try(google_compute_firewall.allow-tag-ssh, null)
}
}
-output "custom_egress_allow_rules" {
- description = "Custom egress rules with allow blocks."
- value = [
- for rule in google_compute_firewall.custom-rules :
- rule.name if rule.direction == "EGRESS" && try(length(rule.allow), 0) > 0
- ]
-}
-
-output "custom_egress_deny_rules" {
- description = "Custom egress rules with allow blocks."
- value = [
- for rule in google_compute_firewall.custom-rules :
- rule.name if rule.direction == "EGRESS" && try(length(rule.deny), 0) > 0
- ]
-}
-
-output "custom_ingress_allow_rules" {
- description = "Custom ingress rules with allow blocks."
- value = [
- for rule in google_compute_firewall.custom-rules :
- rule.name if rule.direction == "INGRESS" && try(length(rule.allow), 0) > 0
- ]
-}
-
-output "custom_ingress_deny_rules" {
- description = "Custom ingress rules with deny blocks."
- value = [
- for rule in google_compute_firewall.custom-rules :
- rule.name if rule.direction == "INGRESS" && try(length(rule.deny), 0) > 0
- ]
-}
-
output "rules" {
- description = "All google_compute_firewall resources created."
- value = merge(
- google_compute_firewall.custom-rules,
- try({ (google_compute_firewall.allow-admins.0.name) = google_compute_firewall.allow-admins.0 }, {}),
- try({ (google_compute_firewall.allow-tag-ssh.0.name) = google_compute_firewall.allow-tag-ssh.0 }, {}),
- try({ (google_compute_firewall.allow-tag-http.0.name) = google_compute_firewall.allow-tag-http.0 }, {}),
- try({ (google_compute_firewall.allow-tag-https.0.name) = google_compute_firewall.allow-tag-https.0 }, {})
- )
+ description = "Custom rule resources."
+ value = google_compute_firewall.custom-rules
}
diff --git a/modules/net-vpc-firewall/variables.tf b/modules/net-vpc-firewall/variables.tf
index b0a2a9d792..9f750cc839 100644
--- a/modules/net-vpc-firewall/variables.tf
+++ b/modules/net-vpc-firewall/variables.tf
@@ -14,67 +14,95 @@
* limitations under the License.
*/
-variable "admin_ranges" {
- description = "IP CIDR ranges that have complete access to all subnets."
- type = list(string)
- default = []
+variable "default_rules_config" {
+ description = "Optionally created convenience rules. Set the 'disabled' attribute to true, or individual rule attributes to empty lists to disable."
+ type = object({
+ admin_ranges = optional(list(string))
+ disabled = optional(bool, false)
+ http_ranges = optional(list(string), [
+ "35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"]
+ )
+ http_tags = optional(list(string), ["http-server"])
+ https_ranges = optional(list(string), [
+ "35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"]
+ )
+ https_tags = optional(list(string), ["https-server"])
+ ssh_ranges = optional(list(string), ["35.235.240.0/20"])
+ ssh_tags = optional(list(string), ["ssh"])
+ })
+ default = {}
+ nullable = false
}
-variable "cidr_template_file" {
- description = "Path for optional file containing name->cidr_list map to be used by the rules factory."
- type = string
- default = null
-}
-
-variable "custom_rules" {
- description = "List of custom rule definitions (refer to variables file for syntax)."
+variable "egress_rules" {
+ description = "List of egress rule definitions, default to deny action. Null destination ranges will be replaced with 0/0."
type = map(object({
- description = string
- direction = string
- action = string # (allow|deny)
- ranges = list(string)
- sources = list(string)
- targets = list(string)
- use_service_accounts = bool
- rules = list(object({
- protocol = string
- ports = list(string)
+ deny = optional(bool, true)
+ description = optional(string)
+ destination_ranges = optional(list(string))
+ disabled = optional(bool, false)
+ enable_logging = optional(object({
+ include_metadata = optional(bool)
}))
- extra_attributes = map(string)
+ priority = optional(number, 1000)
+ targets = optional(list(string))
+ use_service_accounts = optional(bool, false)
+ rules = optional(list(object({
+ protocol = string
+ ports = optional(list(string))
+ })), [{ protocol = "all" }])
}))
- default = {}
-}
-
-variable "data_folder" {
- description = "Path for optional folder containing firewall rules defined as YaML objects used by the rules factory."
- type = string
- default = null
+ default = {}
+ nullable = false
}
-variable "http_source_ranges" {
- description = "List of IP CIDR ranges for tag-based HTTP rule, defaults to the health checkers ranges."
- type = list(string)
- default = ["35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"]
+variable "factories_config" {
+ description = "Paths to data files and folders that enable factory functionality."
+ type = object({
+ cidr_tpl_file = optional(string)
+ rules_folder = string
+ })
+ default = null
}
-variable "https_source_ranges" {
- description = "List of IP CIDR ranges for tag-based HTTPS rule, defaults to the health checkers ranges."
- type = list(string)
- default = ["35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"]
+variable "ingress_rules" {
+ 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)
+ disabled = optional(bool, false)
+ enable_logging = optional(object({
+ include_metadata = optional(bool)
+ }))
+ priority = optional(number, 1000)
+ source_ranges = optional(list(string))
+ sources = optional(list(string))
+ targets = optional(list(string))
+ use_service_accounts = optional(bool, false)
+ rules = optional(list(object({
+ protocol = string
+ ports = optional(list(string))
+ })), [{ protocol = "all" }])
+ }))
+ default = {}
+ nullable = false
}
variable "named_ranges" {
- description = "Names that can be used of valid values for the `ranges` field of `custom_rules`."
+ description = "Define mapping of names to ranges that can be used in custom rules."
type = map(list(string))
default = {
- any = ["0.0.0.0/0"]
- dns-forwarders = ["35.199.192.0/19"]
- health-checkers = ["35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"]
+ any = ["0.0.0.0/0"]
+ dns-forwarders = ["35.199.192.0/19"]
+ health-checkers = [
+ "35.191.0.0/16", "130.211.0.0/22", "209.85.152.0/22", "209.85.204.0/22"
+ ]
iap-forwarders = ["35.235.240.0/20"]
private-googleapis = ["199.36.153.8/30"]
restricted-googleapis = ["199.36.153.4/30"]
rfc1918 = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
}
+ nullable = false
}
variable "network" {
@@ -86,10 +114,3 @@ variable "project_id" {
description = "Project id of the project that holds the network."
type = string
}
-
-variable "ssh_source_ranges" {
- description = "List of IP CIDR ranges for tag-based SSH rule, defaults to the IAP forwarders range."
- type = list(string)
- default = ["35.235.240.0/20"]
-}
-
diff --git a/modules/net-vpc-firewall/versions.tf b/modules/net-vpc-firewall/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/net-vpc-firewall/versions.tf
+++ b/modules/net-vpc-firewall/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/net-vpc-peering/README.md b/modules/net-vpc-peering/README.md
index 892f6ccae3..0555b994da 100644
--- a/modules/net-vpc-peering/README.md
+++ b/modules/net-vpc-peering/README.md
@@ -13,7 +13,7 @@ Basic usage of this module is as follows:
```hcl
module "peering" {
- source = "./modules/net-vpc-peering"
+ source = "./fabric/modules/net-vpc-peering"
prefix = "name-prefix"
local_network = "projects/project-1/global/networks/vpc-1"
peer_network = "projects/project-1/global/networks/vpc-2"
@@ -25,14 +25,14 @@ If you need to create more than one peering for the same VPC Network `(A -> B, A
```hcl
module "peering-a-b" {
- source = "./modules/net-vpc-peering"
+ source = "./fabric/modules/net-vpc-peering"
prefix = "name-prefix"
local_network = "projects/project-a/global/networks/vpc-a"
peer_network = "projects/project-b/global/networks/vpc-b"
}
module "peering-a-c" {
- source = "./modules/net-vpc-peering"
+ source = "./fabric/modules/net-vpc-peering"
prefix = "name-prefix"
local_network = "projects/project-a/global/networks/vpc-a"
peer_network = "projects/project-c/global/networks/vpc-c"
@@ -51,7 +51,7 @@ module "peering-a-c" {
| [export_local_custom_routes](variables.tf#L18) | Export custom routes to peer network from local network. | bool
| | false
|
| [export_peer_custom_routes](variables.tf#L24) | Export custom routes to local network from peer network. | bool
| | false
|
| [peer_create_peering](variables.tf#L35) | Create the peering on the remote side. If false, only the peering from this network to the remote network is created. | bool
| | true
|
-| [prefix](variables.tf#L46) | Name prefix for the network peerings. | string
| | "network-peering"
|
+| [prefix](variables.tf#L46) | Optional name prefix for the network peerings. | string
| | null
|
## Outputs
diff --git a/modules/net-vpc-peering/main.tf b/modules/net-vpc-peering/main.tf
index 1bade5f14f..f705df715b 100644
--- a/modules/net-vpc-peering/main.tf
+++ b/modules/net-vpc-peering/main.tf
@@ -17,10 +17,11 @@
locals {
local_network_name = element(reverse(split("/", var.local_network)), 0)
peer_network_name = element(reverse(split("/", var.peer_network)), 0)
+ prefix = var.prefix == null ? "" : "${var.prefix}-"
}
resource "google_compute_network_peering" "local_network_peering" {
- name = "${var.prefix}-${local.local_network_name}-${local.peer_network_name}"
+ name = "${local.prefix}${local.local_network_name}-${local.peer_network_name}"
network = var.local_network
peer_network = var.peer_network
export_custom_routes = var.export_local_custom_routes
@@ -29,7 +30,7 @@ resource "google_compute_network_peering" "local_network_peering" {
resource "google_compute_network_peering" "peer_network_peering" {
count = var.peer_create_peering ? 1 : 0
- name = "${var.prefix}-${local.peer_network_name}-${local.local_network_name}"
+ name = "${local.prefix}${local.peer_network_name}-${local.local_network_name}"
network = var.peer_network
peer_network = var.local_network
export_custom_routes = var.export_peer_custom_routes
diff --git a/modules/net-vpc-peering/variables.tf b/modules/net-vpc-peering/variables.tf
index 908578fa55..8f5f15f676 100644
--- a/modules/net-vpc-peering/variables.tf
+++ b/modules/net-vpc-peering/variables.tf
@@ -44,7 +44,11 @@ variable "peer_network" {
}
variable "prefix" {
- description = "Name prefix for the network peerings."
+ description = "Optional name prefix for the network peerings."
type = string
- default = "network-peering"
+ default = null
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty, please use null instead."
+ }
}
diff --git a/modules/net-vpc-peering/versions.tf b/modules/net-vpc-peering/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/net-vpc-peering/versions.tf
+++ b/modules/net-vpc-peering/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/net-vpc/README.md b/modules/net-vpc/README.md
index 4102d92ed1..53361009ee 100644
--- a/modules/net-vpc/README.md
+++ b/modules/net-vpc/README.md
@@ -10,7 +10,7 @@ The module allows for several different VPC configurations, some of the most com
```hcl
module "vpc" {
- source = "./modules/net-vpc"
+ source = "./fabric/modules/net-vpc"
project_id = "my-project"
name = "my-network"
subnets = [
@@ -18,7 +18,7 @@ module "vpc" {
ip_cidr_range = "10.0.0.0/24"
name = "production"
region = "europe-west1"
- secondary_ip_range = {
+ secondary_ip_ranges = {
pods = "172.16.0.0/20"
services = "192.168.0.0/24"
}
@@ -27,7 +27,6 @@ module "vpc" {
ip_cidr_range = "10.0.16.0/24"
name = "production"
region = "europe-west2"
- secondary_ip_range = {}
}
]
}
@@ -42,30 +41,27 @@ If you only want to create the "local" side of the peering, use `peering_create_
```hcl
module "vpc-hub" {
- source = "./modules/net-vpc"
+ source = "./fabric/modules/net-vpc"
project_id = "hub"
name = "vpc-hub"
subnets = [{
- ip_cidr_range = "10.0.0.0/24"
- name = "subnet-1"
- region = "europe-west1"
- secondary_ip_range = null
+ ip_cidr_range = "10.0.0.0/24"
+ name = "subnet-1"
+ region = "europe-west1"
}]
}
module "vpc-spoke-1" {
- source = "./modules/net-vpc"
+ source = "./fabric/modules/net-vpc"
project_id = "spoke1"
name = "vpc-spoke1"
subnets = [{
- ip_cidr_range = "10.0.1.0/24"
- name = "subnet-2"
- region = "europe-west1"
- secondary_ip_range = null
+ 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
- export_routes = false
import_routes = true
}
}
@@ -79,9 +75,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"
@@ -89,7 +85,7 @@ locals {
}
module "vpc-host" {
- source = "./modules/net-vpc"
+ source = "./fabric/modules/net-vpc"
project_id = "my-project"
name = "my-host-network"
subnets = [
@@ -108,7 +104,7 @@ module "vpc-host" {
local.service_project_1.project_id,
local.service_project_2.project_id
]
- iam = {
+ subnet_iam = {
"europe-west1/subnet-1" = {
"roles/compute.networkUser" = [
local.service_project_1.cloud_services_service_account,
@@ -127,20 +123,18 @@ module "vpc-host" {
```hcl
module "vpc" {
- source = "./modules/net-vpc"
+ source = "./fabric/modules/net-vpc"
project_id = "my-project"
name = "my-network"
subnets = [
{
- ip_cidr_range = "10.0.0.0/24"
- name = "production"
- region = "europe-west1"
- secondary_ip_range = null
+ ip_cidr_range = "10.0.0.0/24"
+ name = "production"
+ region = "europe-west1"
}
]
psa_config = {
ranges = { myrange = "10.0.1.0/24" }
- routes = null
}
}
# tftest modules=1 resources=5
@@ -152,35 +146,66 @@ Custom routes can be optionally exported/imported through the peering formed wit
```hcl
module "vpc" {
- source = "./modules/net-vpc"
+ source = "./fabric/modules/net-vpc"
project_id = "my-project"
name = "my-network"
subnets = [
{
- ip_cidr_range = "10.0.0.0/24"
- name = "production"
- region = "europe-west1"
- secondary_ip_range = null
+ ip_cidr_range = "10.0.0.0/24"
+ name = "production"
+ region = "europe-west1"
}
]
psa_config = {
- ranges = { myrange = "10.0.1.0/24" }
- routes = { export=true, import=true }
+ ranges = { myrange = "10.0.1.0/24" }
+ export_routes = true
+ import_routes = true
}
}
# tftest modules=1 resources=5
```
+### Subnets for Private Service Connect, Proxy-only subnets
+
+Along with common private subnets module supports creation more service specific subnets for the following purposes:
+
+- [Proxy-only subnets](https://cloud.google.com/load-balancing/docs/proxy-only-subnets) for Regional HTTPS Internal HTTPS Load Balancers
+- [Private Service Connect](https://cloud.google.com/vpc/docs/private-service-connect#psc-subnets) subnets
+
+```hcl
+module "vpc" {
+ source = "./fabric/modules/net-vpc"
+ project_id = "my-project"
+ name = "my-network"
+
+ subnets_proxy_only = [
+ {
+ ip_cidr_range = "10.0.1.0/24"
+ name = "regional-proxy"
+ region = "europe-west1"
+ active = true
+ }
+ ]
+ subnets_psc = [
+ {
+ ip_cidr_range = "10.0.3.0/24"
+ name = "psc"
+ region = "europe-west1"
+ }
+ ]
+}
+# tftest modules=1 resources=3
+```
+
### DNS Policies
```hcl
module "vpc" {
- source = "./modules/net-vpc"
+ source = "./fabric/modules/net-vpc"
project_id = "my-project"
name = "my-network"
dns_policy = {
- inbound = true
- logging = false
+ inbound = true
outbound = {
private_ns = ["10.0.0.1"]
public_ns = ["8.8.8.8"]
@@ -188,10 +213,9 @@ module "vpc" {
}
subnets = [
{
- ip_cidr_range = "10.0.0.0/24"
- name = "production"
- region = "europe-west1"
- secondary_ip_range = {}
+ ip_cidr_range = "10.0.0.0/24"
+ name = "production"
+ region = "europe-west1"
}
]
}
@@ -200,34 +224,35 @@ module "vpc" {
### Subnet Factory
-The `net-vpc` module includes a subnet factory (see [Resource Factories](../../examples/factories/)) for the massive creation of subnets leveraging one configuration file per subnet.
+The `net-vpc` module includes a subnet factory (see [Resource Factories](../../blueprints/factories/)) for the massive creation of subnets leveraging one configuration file per subnet.
```hcl
module "vpc" {
- source = "./modules/net-vpc"
+ source = "./fabric/modules/net-vpc"
project_id = "my-project"
name = "my-network"
data_folder = "config/subnets"
}
-# tftest skip
+# tftest modules=1 resources=2 files=subnets
```
```yaml
-# ./config/subnets/subnet-name.yaml
+# tftest-file id=subnets path=config/subnets/subnet-name.yaml
region: europe-west1
description: Sample description
ip_cidr_range: 10.0.0.0/24
# optional attributes
-private_ip_google_access: false # defaults to true
+enable_private_access: false # defaults to true
iam_users: ["foobar@example.com"] # grant compute/networkUser to users
iam_groups: ["lorem@example.com"] # grant compute/networkUser to groups
iam_service_accounts: ["fbz@prj.iam.gserviceaccount.com"]
secondary_ip_ranges: # map of secondary ip ranges
- - secondary-range-a: 192.168.0.0/24
+ 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
```
@@ -235,30 +260,25 @@ flow_logs: # enable, set to empty map to use defaults
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [name](variables.tf#L85) | The name of the network being created. | string
| ✓ | |
-| [project_id](variables.tf#L106) | The ID of the project where this VPC will be created. | string
| ✓ | |
+| [name](variables.tf#L60) | The name of the network being created. | string
| ✓ | |
+| [project_id](variables.tf#L76) | The ID of the project where this VPC will be created. | string
| ✓ | |
| [auto_create_subnetworks](variables.tf#L17) | Set to true to create an auto mode subnet, defaults to custom mode. | bool
| | false
|
| [data_folder](variables.tf#L23) | An optional folder containing the subnet configurations in YaML format. | string
| | null
|
| [delete_default_routes_on_create](variables.tf#L29) | Set to true to delete the default routes at creation time. | bool
| | false
|
| [description](variables.tf#L35) | An optional description of this resource (triggers recreation on change). | string
| | "Terraform-managed."
|
-| [dns_policy](variables.tf#L41) | DNS policy setup for the VPC. | object({…})
| | null
|
-| [iam](variables.tf#L54) | Subnet IAM bindings in {REGION/NAME => {ROLE => [MEMBERS]} format. | map(map(list(string)))
| | {}
|
-| [log_config_defaults](variables.tf#L60) | Default configuration for flow logs when enabled. | object({…})
| | {…}
|
-| [log_configs](variables.tf#L74) | Map keyed by subnet 'region/name' of optional configurations for flow logs when enabled. | map(map(string))
| | {}
|
-| [mtu](variables.tf#L80) | Maximum Transmission Unit in bytes. The minimum value for this field is 1460 and the maximum value is 1500 bytes. |
| | null
|
-| [peering_config](variables.tf#L90) | VPC peering configuration. | object({…})
| | null
|
-| [peering_create_remote_end](variables.tf#L100) | Skip creation of peering on the remote end when using peering_config. | bool
| | true
|
-| [psa_config](variables.tf#L111) | The Private Service Access configuration for Service Networking. | object({…})
| | null
|
-| [routes](variables.tf#L123) | Network routes, keyed by name. | map(object({…}))
| | {}
|
-| [routing_mode](variables.tf#L135) | The network routing mode (default 'GLOBAL'). | string
| | "GLOBAL"
|
-| [shared_vpc_host](variables.tf#L145) | Enable shared VPC for this project. | bool
| | false
|
-| [shared_vpc_service_projects](variables.tf#L151) | Shared VPC service projects to register with this host. | list(string)
| | []
|
-| [subnet_descriptions](variables.tf#L157) | Optional map of subnet descriptions, keyed by subnet 'region/name'. | map(string)
| | {}
|
-| [subnet_flow_logs](variables.tf#L163) | Optional map of boolean to control flow logs (default is disabled), keyed by subnet 'region/name'. | map(bool)
| | {}
|
-| [subnet_private_access](variables.tf#L169) | Optional map of boolean to control private Google access (default is enabled), keyed by subnet 'region/name'. | map(bool)
| | {}
|
-| [subnets](variables.tf#L175) | List of subnets being created. | list(object({…}))
| | []
|
-| [subnets_l7ilb](variables.tf#L186) | List of subnets for private HTTPS load balancer. | list(object({…}))
| | []
|
-| [vpc_create](variables.tf#L197) | Create VPC. When set to false, uses a data source to reference existing VPC. | bool
| | true
|
+| [dns_policy](variables.tf#L41) | DNS policy setup for the VPC. | object({…})
| | null
|
+| [mtu](variables.tf#L54) | Maximum Transmission Unit in bytes. The minimum value for this field is 1460 (the default) and the maximum value is 1500 bytes. | number
| | null
|
+| [peering_config](variables.tf#L65) | VPC peering configuration. | object({…})
| | null
|
+| [psa_config](variables.tf#L81) | The Private Service Access configuration for Service Networking. | object({…})
| | null
|
+| [routes](variables.tf#L91) | Network routes, keyed by name. | map(object({…}))
| | {}
|
+| [routing_mode](variables.tf#L111) | The network routing mode (default 'GLOBAL'). | string
| | "GLOBAL"
|
+| [shared_vpc_host](variables.tf#L121) | Enable shared VPC for this project. | bool
| | false
|
+| [shared_vpc_service_projects](variables.tf#L127) | Shared VPC service projects to register with this host. | list(string)
| | []
|
+| [subnet_iam](variables.tf#L133) | Subnet IAM bindings in {REGION/NAME => {ROLE => [MEMBERS]} format. | map(map(list(string)))
| | {}
|
+| [subnets](variables.tf#L139) | Subnet configuration. | list(object({…}))
| | []
|
+| [subnets_proxy_only](variables.tf#L164) | List of proxy-only subnets for Regional HTTPS or Internal HTTPS load balancers. Note: Only one proxy-only subnet for each VPC network in each region can be active. | list(object({…}))
| | []
|
+| [subnets_psc](variables.tf#L176) | List of subnets for Private Service Connect service producers. | list(object({…}))
| | []
|
+| [vpc_create](variables.tf#L187) | Create VPC. When set to false, uses a data source to reference existing VPC. | bool
| | true
|
## Outputs
@@ -274,7 +294,8 @@ flow_logs: # enable, set to empty map to use defaults
| [subnet_secondary_ranges](outputs.tf#L85) | Map of subnet secondary ranges keyed by name. | |
| [subnet_self_links](outputs.tf#L96) | Map of subnet self links keyed by name. | |
| [subnets](outputs.tf#L102) | Subnet resources. | |
-| [subnets_l7ilb](outputs.tf#L107) | L7 ILB subnet resources. | |
+| [subnets_proxy_only](outputs.tf#L107) | L7 ILB or L7 Regional LB subnet resources. | |
+| [subnets_psc](outputs.tf#L112) | Private Service Connect subnet resources. | |
The key format is `subnet_region/subnet_name`. For example `europe-west1/my_subnet`.
diff --git a/modules/net-vpc/main.tf b/modules/net-vpc/main.tf
index 18388e0c1c..7eedc95ac3 100644
--- a/modules/net-vpc/main.tf
+++ b/modules/net-vpc/main.tf
@@ -55,8 +55,12 @@ resource "google_compute_network_peering" "local" {
}
resource "google_compute_network_peering" "remote" {
- provider = google-beta
- count = var.peering_config != null && var.peering_create_remote_end ? 1 : 0
+ provider = google-beta
+ count = (
+ var.peering_config != null && try(var.peering_config.create_remote_peer, true)
+ ? 1
+ : 0
+ )
name = "${local.peer_network}-${var.name}"
network = var.peering_config.peer_vpc_self_link
peer_network = local.network.self_link
@@ -74,10 +78,10 @@ resource "google_compute_shared_vpc_host_project" "shared_vpc_host" {
resource "google_compute_shared_vpc_service_project" "service_projects" {
provider = google-beta
- for_each = (
+ for_each = toset(
var.shared_vpc_host && var.shared_vpc_service_projects != null
- ? toset(var.shared_vpc_service_projects)
- : toset([])
+ ? var.shared_vpc_service_projects
+ : []
)
host_project = var.project_id
service_project = each.value
@@ -86,19 +90,23 @@ resource "google_compute_shared_vpc_service_project" "service_projects" {
resource "google_dns_policy" "default" {
count = var.dns_policy == null ? 0 : 1
- enable_inbound_forwarding = var.dns_policy.inbound
- enable_logging = var.dns_policy.logging
- name = var.name
project = var.project_id
+ name = var.name
+ enable_inbound_forwarding = try(var.dns_policy.inbound, null)
+ enable_logging = try(var.dns_policy.logging, null)
networks {
network_url = local.network.id
}
dynamic "alternative_name_server_config" {
- for_each = toset(var.dns_policy.outbound == null ? [] : [""])
+ for_each = var.dns_policy.outbound != null ? [""] : []
content {
dynamic "target_name_servers" {
- for_each = toset(var.dns_policy.outbound.private_ns)
+ for_each = (
+ var.dns_policy.outbound.private_ns != null
+ ? var.dns_policy.outbound.private_ns
+ : []
+ )
iterator = ns
content {
ipv4_address = ns.key
@@ -106,7 +114,11 @@ resource "google_dns_policy" "default" {
}
}
dynamic "target_name_servers" {
- for_each = toset(var.dns_policy.outbound.public_ns)
+ for_each = (
+ var.dns_policy.outbound.public_ns != null
+ ? var.dns_policy.outbound.public_ns
+ : []
+ )
iterator = ns
content {
ipv4_address = ns.key
diff --git a/modules/net-vpc/outputs.tf b/modules/net-vpc/outputs.tf
index d1e68c3439..fd79de6594 100644
--- a/modules/net-vpc/outputs.tf
+++ b/modules/net-vpc/outputs.tf
@@ -104,7 +104,12 @@ output "subnets" {
value = { for k, v in google_compute_subnetwork.subnetwork : k => v }
}
-output "subnets_l7ilb" {
- description = "L7 ILB subnet resources."
- value = { for k, v in google_compute_subnetwork.l7ilb : k => v }
+output "subnets_proxy_only" {
+ description = "L7 ILB or L7 Regional LB subnet resources."
+ value = { for k, v in google_compute_subnetwork.proxy_only : k => v }
+}
+
+output "subnets_psc" {
+ description = "Private Service Connect subnet resources."
+ value = { for k, v in google_compute_subnetwork.psc : k => v }
}
diff --git a/modules/net-vpc/psa.tf b/modules/net-vpc/psa.tf
index 4a10ca5c10..19c47d4d01 100644
--- a/modules/net-vpc/psa.tf
+++ b/modules/net-vpc/psa.tf
@@ -17,15 +17,11 @@
# tfdoc:file:description Private Service Access resources.
locals {
- psa_config = (
- var.psa_config == null
- ? { ranges = {}, routes = null }
- : var.psa_config
- )
+ psa_config_ranges = try(var.psa_config.ranges, {})
}
resource "google_compute_global_address" "psa_ranges" {
- for_each = local.psa_config.ranges
+ for_each = local.psa_config_ranges
project = var.project_id
name = each.key
purpose = "VPC_PEERING"
@@ -36,7 +32,7 @@ resource "google_compute_global_address" "psa_ranges" {
}
resource "google_service_networking_connection" "psa_connection" {
- for_each = var.psa_config == null ? {} : { 1 = 1 }
+ for_each = var.psa_config != null ? { 1 = 1 } : {}
network = local.network.id
service = "servicenetworking.googleapis.com"
reserved_peering_ranges = [
@@ -45,10 +41,10 @@ resource "google_service_networking_connection" "psa_connection" {
}
resource "google_compute_network_peering_routes_config" "psa_routes" {
- for_each = var.psa_config == null ? {} : { 1 = 1 }
+ for_each = var.psa_config != null ? { 1 = 1 } : {}
project = var.project_id
peering = google_service_networking_connection.psa_connection["1"].peering
network = local.network.name
- export_custom_routes = try(var.psa_config.routes.export, false)
- import_custom_routes = try(var.psa_config.routes.import, false)
+ export_custom_routes = var.psa_config.export_routes
+ import_custom_routes = var.psa_config.import_routes
}
diff --git a/modules/net-vpc/subnets.tf b/modules/net-vpc/subnets.tf
index 05ad0aa3fc..ae094ecfa0 100644
--- a/modules/net-vpc/subnets.tf
+++ b/modules/net-vpc/subnets.tf
@@ -21,47 +21,31 @@ locals {
for f in fileset(var.data_folder, "**/*.yaml") :
trimsuffix(basename(f), ".yaml") => yamldecode(file("${var.data_folder}/${f}"))
}
- _factory_descriptions = {
- for k, v in local._factory_data :
- "${v.region}/${k}" => try(v.description, null)
+ _factory_subnets = {
+ for k, v in local._factory_data : "${v.region}/${k}" => {
+ name = k
+ ip_cidr_range = v.ip_cidr_range
+ region = v.region
+ description = try(v.description, null)
+ enable_private_access = try(v.enable_private_access, true)
+ flow_logs_config = try(v.flow_logs, null)
+ ipv6 = try(v.ipv6, null)
+ secondary_ip_ranges = try(v.secondary_ip_ranges, null)
+ }
}
- _factory_iam_members = [
+ _factory_subnets_iam = [
for k, v in local._factory_subnets : {
subnet = k
role = "roles/compute.networkUser"
members = concat(
- formatlist("group:%s", try(v.iam_groups, [])),
- formatlist("user:%s", try(v.iam_users, [])),
- formatlist("serviceAccount:%s", try(v.iam_service_accounts, []))
+ formatlist("group:%s", lookup(v, "iam_groups", [])),
+ formatlist("user:%s", lookup(v, "iam_users", [])),
+ formatlist("serviceAccount:%s", lookup(v, "iam_service_accounts", []))
)
}
]
- _factory_flow_logs = {
- for k, v in local._factory_data : "${v.region}/${k}" => merge(
- var.log_config_defaults, try(v.flow_logs, {})
- ) if try(v.flow_logs, false)
- }
- _factory_private_access = {
- for k, v in local._factory_data : "${v.region}/${k}" => try(
- v.private_ip_google_access, true
- )
- }
- _factory_subnets = {
- for k, v in local._factory_data : "${v.region}/${k}" => {
- ip_cidr_range = v.ip_cidr_range
- name = k
- region = v.region
- secondary_ip_range = try(v.secondary_ip_range, {})
- }
- }
- _iam = var.iam == null ? {} : var.iam
- _subnet_flow_logs = {
- for k, v in var.subnet_flow_logs : k => merge(
- var.log_config_defaults, try(var.log_configs[k], {})
- )
- }
_subnet_iam_members = flatten([
- for subnet, roles in local._iam : [
+ for subnet, roles in(var.subnet_iam == null ? {} : var.subnet_iam) : [
for role, members in roles : {
members = members
role = role
@@ -69,24 +53,20 @@ locals {
}
]
])
- subnet_descriptions = merge(
- local._factory_descriptions, var.subnet_descriptions
- )
subnet_iam_members = concat(
- local._factory_iam_members, local._subnet_iam_members
- )
- subnet_flow_logs = merge(
- local._factory_flow_logs, local._subnet_flow_logs
- )
- subnet_private_access = merge(
- local._factory_private_access, var.subnet_private_access
+ [for k in local._factory_subnets_iam : k if length(k.members) > 0],
+ local._subnet_iam_members
)
subnets = merge(
{ for subnet in var.subnets : "${subnet.region}/${subnet.name}" => subnet },
local._factory_subnets
)
- subnets_l7ilb = {
- for subnet in var.subnets_l7ilb :
+ subnets_proxy_only = {
+ for subnet in var.subnets_proxy_only :
+ "${subnet.region}/${subnet.name}" => subnet
+ }
+ subnets_psc = {
+ for subnet in var.subnets_psc :
"${subnet.region}/${subnet.name}" => subnet
}
}
@@ -95,51 +75,66 @@ resource "google_compute_subnetwork" "subnetwork" {
for_each = local.subnets
project = var.project_id
network = local.network.name
- region = each.value.region
name = each.value.name
+ region = each.value.region
ip_cidr_range = each.value.ip_cidr_range
- secondary_ip_range = each.value.secondary_ip_range == null ? [] : [
- for name, range in each.value.secondary_ip_range :
+ description = (
+ each.value.description == null
+ ? "Terraform-managed."
+ : each.value.description
+ )
+ private_ip_google_access = each.value.enable_private_access
+ secondary_ip_range = each.value.secondary_ip_ranges == null ? [] : [
+ for name, range in each.value.secondary_ip_ranges :
{ range_name = name, ip_cidr_range = range }
]
- description = lookup(
- local.subnet_descriptions, each.key, "Terraform-managed."
- )
- private_ip_google_access = lookup(
- local.subnet_private_access, each.key, true
- )
dynamic "log_config" {
- for_each = toset(
- try(local.subnet_flow_logs[each.key], {}) != {}
- ? [local.subnet_flow_logs[each.key]]
- : []
- )
- iterator = config
+ for_each = each.value.flow_logs_config != null ? [""] : []
content {
- aggregation_interval = config.value.aggregation_interval
- flow_sampling = config.value.flow_sampling
- metadata = config.value.metadata
+ aggregation_interval = each.value.flow_logs_config.aggregation_interval
+ filter_expr = each.value.flow_logs_config.filter_expression
+ flow_sampling = each.value.flow_logs_config.flow_sampling
+ metadata = each.value.flow_logs_config.metadata
+ metadata_fields = (
+ each.value.flow_logs_config.metadata == "CUSTOM_METADATA"
+ ? each.value.flow_logs_config.metadata_fields
+ : null
+ )
}
}
}
-resource "google_compute_subnetwork" "l7ilb" {
- provider = google-beta
- for_each = local.subnets_l7ilb
+resource "google_compute_subnetwork" "proxy_only" {
+ for_each = local.subnets_proxy_only
project = var.project_id
network = local.network.name
- region = each.value.region
name = each.value.name
+ region = each.value.region
ip_cidr_range = each.value.ip_cidr_range
- purpose = "INTERNAL_HTTPS_LOAD_BALANCER"
+ description = (
+ each.value.description == null
+ ? "Terraform-managed proxy-only subnet for Regional HTTPS or Internal HTTPS LB."
+ : each.value.description
+ )
+ purpose = "REGIONAL_MANAGED_PROXY"
role = (
each.value.active || each.value.active == null ? "ACTIVE" : "BACKUP"
)
- description = lookup(
- local.subnet_descriptions,
- "${each.value.region}/${each.value.name}",
- "Terraform-managed."
+}
+
+resource "google_compute_subnetwork" "psc" {
+ for_each = local.subnets_psc
+ project = var.project_id
+ network = local.network.name
+ name = each.value.name
+ region = each.value.region
+ ip_cidr_range = each.value.ip_cidr_range
+ description = (
+ each.value.description == null
+ ? "Terraform-managed subnet for Private Service Connect (PSC NAT)."
+ : each.value.description
)
+ purpose = "PRIVATE_SERVICE_CONNECT"
}
resource "google_compute_subnetwork_iam_binding" "binding" {
diff --git a/modules/net-vpc/variables.tf b/modules/net-vpc/variables.tf
index 464ccfa035..a7aa207721 100644
--- a/modules/net-vpc/variables.tf
+++ b/modules/net-vpc/variables.tf
@@ -41,44 +41,19 @@ variable "description" {
variable "dns_policy" {
description = "DNS policy setup for the VPC."
type = object({
- inbound = bool
- logging = bool
- outbound = object({
+ inbound = optional(bool)
+ logging = optional(bool)
+ outbound = optional(object({
private_ns = list(string)
public_ns = list(string)
- })
+ }))
})
default = null
}
-variable "iam" {
- description = "Subnet IAM bindings in {REGION/NAME => {ROLE => [MEMBERS]} format."
- type = map(map(list(string)))
- default = {}
-}
-
-variable "log_config_defaults" {
- description = "Default configuration for flow logs when enabled."
- type = object({
- aggregation_interval = string
- flow_sampling = number
- metadata = string
- })
- default = {
- aggregation_interval = "INTERVAL_5_SEC"
- flow_sampling = 0.5
- metadata = "INCLUDE_ALL_METADATA"
- }
-}
-
-variable "log_configs" {
- description = "Map keyed by subnet 'region/name' of optional configurations for flow logs when enabled."
- type = map(map(string))
- default = {}
-}
-
variable "mtu" {
- description = "Maximum Transmission Unit in bytes. The minimum value for this field is 1460 and the maximum value is 1500 bytes."
+ description = "Maximum Transmission Unit in bytes. The minimum value for this field is 1460 (the default) and the maximum value is 1500 bytes."
+ type = number
default = null
}
@@ -91,18 +66,13 @@ variable "peering_config" {
description = "VPC peering configuration."
type = object({
peer_vpc_self_link = string
- export_routes = bool
- import_routes = bool
+ create_remote_peer = optional(bool, true)
+ export_routes = optional(bool)
+ import_routes = optional(bool)
})
default = null
}
-variable "peering_create_remote_end" {
- description = "Skip creation of peering on the remote end when using peering_config."
- type = bool
- default = true
-}
-
variable "project_id" {
description = "The ID of the project where this VPC will be created."
type = string
@@ -111,11 +81,9 @@ variable "project_id" {
variable "psa_config" {
description = "The Private Service Access configuration for Service Networking."
type = object({
- ranges = map(string)
- routes = object({
- export = bool
- import = bool
- })
+ ranges = map(string)
+ export_routes = optional(bool, false)
+ import_routes = optional(bool, false)
})
default = null
}
@@ -124,12 +92,20 @@ variable "routes" {
description = "Network routes, keyed by name."
type = map(object({
dest_range = string
- priority = number
- tags = list(string)
next_hop_type = string # gateway, instance, ip, vpn_tunnel, ilb
next_hop = string
+ priority = optional(number)
+ tags = optional(list(string))
}))
- default = {}
+ default = {}
+ nullable = false
+ validation {
+ condition = alltrue([
+ for r in var.routes :
+ contains(["gateway", "instance", "ip", "vpn_tunnel", "ilb"], r.next_hop_type)
+ ])
+ error_message = "Unsupported next hop type for route."
+ }
}
variable "routing_mode" {
@@ -154,42 +130,56 @@ variable "shared_vpc_service_projects" {
default = []
}
-variable "subnet_descriptions" {
- description = "Optional map of subnet descriptions, keyed by subnet 'region/name'."
- type = map(string)
- default = {}
-}
-
-variable "subnet_flow_logs" {
- description = "Optional map of boolean to control flow logs (default is disabled), keyed by subnet 'region/name'."
- type = map(bool)
- default = {}
-}
-
-variable "subnet_private_access" {
- description = "Optional map of boolean to control private Google access (default is enabled), keyed by subnet 'region/name'."
- type = map(bool)
+variable "subnet_iam" {
+ description = "Subnet IAM bindings in {REGION/NAME => {ROLE => [MEMBERS]} format."
+ type = map(map(list(string)))
default = {}
}
variable "subnets" {
- description = "List of subnets being created."
+ description = "Subnet configuration."
type = list(object({
- name = string
- ip_cidr_range = string
- region = string
- secondary_ip_range = map(string)
+ name = string
+ ip_cidr_range = string
+ region = string
+ description = optional(string)
+ enable_private_access = optional(bool, true)
+ flow_logs_config = optional(object({
+ aggregation_interval = optional(string)
+ filter_expression = optional(string)
+ flow_sampling = optional(number)
+ metadata = optional(string)
+ # only if metadata == "CUSTOM_METADATA"
+ metadata_fields = optional(list(string))
+ }))
+ ipv6 = optional(object({
+ access_type = optional(string)
+ enable_private_access = optional(bool, true)
+ }))
+ secondary_ip_ranges = optional(map(string))
}))
default = []
}
-variable "subnets_l7ilb" {
- description = "List of subnets for private HTTPS load balancer."
+variable "subnets_proxy_only" {
+ description = "List of proxy-only subnets for Regional HTTPS or Internal HTTPS load balancers. Note: Only one proxy-only subnet for each VPC network in each region can be active."
type = list(object({
+ name = string
+ ip_cidr_range = string
+ region = string
+ description = optional(string)
active = bool
+ }))
+ default = []
+}
+
+variable "subnets_psc" {
+ description = "List of subnets for Private Service Connect service producers."
+ type = list(object({
name = string
ip_cidr_range = string
region = string
+ description = optional(string)
}))
default = []
}
diff --git a/modules/net-vpc/versions.tf b/modules/net-vpc/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/net-vpc/versions.tf
+++ b/modules/net-vpc/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/net-vpn-dynamic/README.md b/modules/net-vpn-dynamic/README.md
index 00f1157b83..447e5652c0 100644
--- a/modules/net-vpn-dynamic/README.md
+++ b/modules/net-vpn-dynamic/README.md
@@ -8,35 +8,50 @@ This example shows how to configure a single VPN tunnel using a couple of extra
- internally generated shared secret, which can be fetched from the module's `random_secret` output for reuse; a predefined secret can be used instead by assigning it to the `shared_secret` attribute
```hcl
+module "vm" {
+ source = "./fabric/modules/compute-vm"
+ project_id = "my-project"
+ zone = "europe-west1-b"
+ name = "my-vm"
+ network_interfaces = [{
+ nat = true
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ }]
+ service_account_create = true
+}
+
+
module "vpn-dynamic" {
- source = "./modules/net-vpn-dynamic"
- project_id = "my-project"
- region = "europe-west1"
- network = "my-vpc"
- 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
+ }
+
tunnels = {
remote-1 = {
bgp_peer = {
address = "169.254.139.134"
asn = 64513
- }
- bgp_session_range = "169.254.139.133/30"
- ike_version = 2
- peer_ip = "1.1.1.1"
- router = null
- shared_secret = null
- bgp_peer_options = {
- advertise_groups = ["ALL_SUBNETS"]
- advertise_ip_ranges = {
- "192.168.0.0/24" = "Advertised range description"
+ custom_advertise = {
+ all_subnets = true
+ all_vpc_subnets = false
+ all_peer_vpc_subnets = false
+ ip_ranges = {
+ "192.168.0.0/24" = "Advertised range description"
+ }
}
- advertise_mode = "CUSTOM"
- route_priority = 1000
}
+ bgp_session_range = "169.254.139.133/30"
+ peer_ip = module.vm.external_ip
}
}
}
-# tftest modules=1 resources=10
+# tftest modules=2 resources=12
```
@@ -48,14 +63,10 @@ module "vpn-dynamic" {
| [network](variables.tf#L34) | VPC used for the gateway and routes. | string
| ✓ | |
| [project_id](variables.tf#L39) | Project where resources will be created. | string
| ✓ | |
| [region](variables.tf#L44) | Region used for resources. | string
| ✓ | |
-| [gateway_address](variables.tf#L17) | Optional address assigned to the VPN gateway. Ignored unless gateway_address_create is set to false. | string
| | ""
|
+| [router_config](variables.tf#L49) | Cloud Router configuration for the VPN. If you want to reuse an existing router, set create to false and use name to specify the desired router. | object({…})
| ✓ | |
+| [gateway_address](variables.tf#L17) | Optional address assigned to the VPN gateway. Ignored unless gateway_address_create is set to false. | string
| | null
|
| [gateway_address_create](variables.tf#L23) | Create external address assigned to the VPN gateway. Needs to be explicitly set to false to use address in gateway_address variable. | bool
| | true
|
-| [route_priority](variables.tf#L49) | Route priority, defaults to 1000. | number
| | 1000
|
-| [router_advertise_config](variables.tf#L55) | Router custom advertisement configuration, ip_ranges is a map of address ranges and descriptions. | object({…})
| | null
|
-| [router_asn](variables.tf#L65) | Router ASN used for auto-created router. | number
| | 64514
|
-| [router_create](variables.tf#L71) | Create router. | bool
| | true
|
-| [router_name](variables.tf#L77) | Router name used for auto created router, or to specify existing router to use. Leave blank to use VPN name for auto created router. | string
| | ""
|
-| [tunnels](variables.tf#L83) | VPN tunnel configurations, bgp_peer_options is usually null. | map(object({…}))
| | {}
|
+| [tunnels](variables.tf#L64) | VPN tunnel configurations. | map(object({…}))
| | {}
|
## Outputs
diff --git a/modules/net-vpn-dynamic/main.tf b/modules/net-vpn-dynamic/main.tf
index 4da48c12de..fcf26934bc 100644
--- a/modules/net-vpn-dynamic/main.tf
+++ b/modules/net-vpn-dynamic/main.tf
@@ -21,9 +21,9 @@ locals {
: var.gateway_address
)
router = (
- var.router_create
- ? google_compute_router.router[0].name
- : var.router_name
+ var.router_config.create
+ ? try(google_compute_router.router[0].name, null)
+ : var.router_config.name
)
secret = random_id.secret.b64_url
}
@@ -65,75 +65,56 @@ resource "google_compute_forwarding_rule" "udp-4500" {
}
resource "google_compute_router" "router" {
- count = var.router_create ? 1 : 0
- name = var.router_name == "" ? "vpn-${var.name}" : var.router_name
+ count = var.router_config.create ? 1 : 0
+ name = coalesce(var.router_config.name, "vpn-${var.name}")
project = var.project_id
region = var.region
network = var.network
bgp {
advertise_mode = (
- var.router_advertise_config == null
- ? null
- : var.router_advertise_config.mode
+ var.router_config.custom_advertise != null
+ ? "CUSTOM"
+ : "DEFAULT"
)
advertised_groups = (
- var.router_advertise_config == null ? null : (
- var.router_advertise_config.mode != "CUSTOM"
- ? null
- : var.router_advertise_config.groups
- )
+ try(var.router_config.custom_advertise.all_subnets, false)
+ ? ["ALL_SUBNETS"]
+ : []
)
dynamic "advertised_ip_ranges" {
- for_each = (
- var.router_advertise_config == null ? {} : (
- var.router_advertise_config.mode != "CUSTOM"
- ? null
- : var.router_advertise_config.ip_ranges
- )
- )
+ for_each = try(var.router_config.custom_advertise.ip_ranges, {})
iterator = range
content {
range = range.key
description = range.value
}
}
- asn = var.router_asn
+ keepalive_interval = try(var.router_config.keepalive, null)
+ asn = var.router_config.asn
}
}
resource "google_compute_router_peer" "bgp_peer" {
- for_each = var.tunnels
- region = var.region
- project = var.project_id
- name = "${var.name}-${each.key}"
- router = each.value.router == null ? local.router : each.value.router
- peer_ip_address = each.value.bgp_peer.address
- peer_asn = each.value.bgp_peer.asn
- advertised_route_priority = (
- each.value.bgp_peer_options == null ? var.route_priority : (
- each.value.bgp_peer_options.route_priority == null
- ? var.route_priority
- : each.value.bgp_peer_options.route_priority
- )
- )
+ for_each = var.tunnels
+ region = var.region
+ project = var.project_id
+ name = "${var.name}-${each.key}"
+ router = coalesce(each.value.router, local.router)
+ peer_ip_address = each.value.bgp_peer.address
+ peer_asn = each.value.bgp_peer.asn
+ advertised_route_priority = each.value.bgp_peer.route_priority
advertise_mode = (
- each.value.bgp_peer_options == null ? null : each.value.bgp_peer_options.advertise_mode
+ try(each.value.bgp_peer.custom_advertise, null) != null
+ ? "CUSTOM"
+ : "DEFAULT"
)
- advertised_groups = (
- each.value.bgp_peer_options == null ? null : (
- each.value.bgp_peer_options.advertise_mode != "CUSTOM"
- ? null
- : each.value.bgp_peer_options.advertise_groups
- )
+ advertised_groups = concat(
+ try(each.value.bgp_peer.custom_advertise.all_subnets, false) ? ["ALL_SUBNETS"] : [],
+ try(each.value.bgp_peer.custom_advertise.all_vpc_subnets, false) ? ["ALL_VPC_SUBNETS"] : [],
+ try(each.value.bgp_peer.custom_advertise.all_peer_vpc_subnets, false) ? ["ALL_PEER_VPC_SUBNETS"] : []
)
dynamic "advertised_ip_ranges" {
- for_each = (
- each.value.bgp_peer_options == null ? {} : (
- each.value.bgp_peer_options.advertise_mode != "CUSTOM"
- ? {}
- : each.value.bgp_peer_options.advertise_ip_ranges
- )
- )
+ for_each = try(each.value.bgp_peer.custom_advertise.ip_ranges, {})
iterator = range
content {
range = range.key
@@ -144,11 +125,12 @@ resource "google_compute_router_peer" "bgp_peer" {
}
resource "google_compute_router_interface" "router_interface" {
- for_each = var.tunnels
- project = var.project_id
- region = var.region
- name = "${var.name}-${each.key}"
- router = each.value.router == null ? local.router : each.value.router
+ for_each = var.tunnels
+ project = var.project_id
+ region = var.region
+ name = "${var.name}-${each.key}"
+ router = coalesce(each.value.router, local.router)
+ # FIXME: can bgp_session_range be null?
ip_range = each.value.bgp_session_range == "" ? null : each.value.bgp_session_range
vpn_tunnel = google_compute_vpn_tunnel.tunnels[each.key].name
}
@@ -161,18 +143,14 @@ resource "google_compute_vpn_gateway" "gateway" {
}
resource "google_compute_vpn_tunnel" "tunnels" {
- for_each = var.tunnels
- project = var.project_id
- region = var.region
- name = "${var.name}-${each.key}"
- router = each.value.router == null ? local.router : each.value.router
- peer_ip = each.value.peer_ip
- ike_version = each.value.ike_version
- shared_secret = (
- each.value.shared_secret == "" || each.value.shared_secret == null
- ? local.secret
- : each.value.shared_secret
- )
+ for_each = var.tunnels
+ project = var.project_id
+ region = var.region
+ name = "${var.name}-${each.key}"
+ router = coalesce(each.value.router, local.router)
+ peer_ip = each.value.peer_ip
+ ike_version = each.value.ike_version
+ shared_secret = coalesce(each.value.shared_secret, local.secret)
target_vpn_gateway = google_compute_vpn_gateway.gateway.self_link
depends_on = [google_compute_forwarding_rule.esp]
}
diff --git a/modules/net-vpn-dynamic/outputs.tf b/modules/net-vpn-dynamic/outputs.tf
index 09e5959e52..f049df1d1a 100644
--- a/modules/net-vpn-dynamic/outputs.tf
+++ b/modules/net-vpn-dynamic/outputs.tf
@@ -37,7 +37,7 @@ output "random_secret" {
output "router" {
description = "Router resource (only if auto-created)."
- value = var.router_create ? google_compute_router.router[0] : null
+ value = one(google_compute_router.router[*])
}
output "router_name" {
@@ -54,7 +54,7 @@ output "tunnel_names" {
description = "VPN tunnel names."
value = {
for name in keys(var.tunnels) :
- name => google_compute_vpn_tunnel.tunnels[name].name
+ name => try(google_compute_vpn_tunnel.tunnels[name].name, null)
}
}
@@ -62,7 +62,7 @@ output "tunnel_self_links" {
description = "VPN tunnel self links."
value = {
for name in keys(var.tunnels) :
- name => google_compute_vpn_tunnel.tunnels[name].self_link
+ name => try(google_compute_vpn_tunnel.tunnels[name].self_link, null)
}
}
@@ -70,6 +70,6 @@ output "tunnels" {
description = "VPN tunnel resources."
value = {
for name in keys(var.tunnels) :
- name => google_compute_vpn_tunnel.tunnels[name]
+ name => try(google_compute_vpn_tunnel.tunnels[name], null)
}
}
diff --git a/modules/net-vpn-dynamic/variables.tf b/modules/net-vpn-dynamic/variables.tf
index 6da5c0ac43..33d23a040c 100644
--- a/modules/net-vpn-dynamic/variables.tf
+++ b/modules/net-vpn-dynamic/variables.tf
@@ -17,7 +17,7 @@
variable "gateway_address" {
description = "Optional address assigned to the VPN gateway. Ignored unless gateway_address_create is set to false."
type = string
- default = ""
+ default = null
}
variable "gateway_address_create" {
@@ -46,60 +46,43 @@ variable "region" {
type = string
}
-variable "route_priority" {
- description = "Route priority, defaults to 1000."
- type = number
- default = 1000
-}
-
-variable "router_advertise_config" {
- description = "Router custom advertisement configuration, ip_ranges is a map of address ranges and descriptions."
+variable "router_config" {
+ description = "Cloud Router configuration for the VPN. If you want to reuse an existing router, set create to false and use name to specify the desired router."
type = object({
- groups = list(string)
- ip_ranges = map(string)
- mode = string
+ create = optional(bool, true)
+ asn = number
+ name = optional(string)
+ keepalive = optional(number)
+ custom_advertise = optional(object({
+ all_subnets = bool
+ ip_ranges = map(string)
+ }))
})
- default = null
-}
-
-variable "router_asn" {
- description = "Router ASN used for auto-created router."
- type = number
- default = 64514
-}
-
-variable "router_create" {
- description = "Create router."
- type = bool
- default = true
-}
-
-variable "router_name" {
- description = "Router name used for auto created router, or to specify existing router to use. Leave blank to use VPN name for auto created router."
- type = string
- default = ""
+ nullable = false
}
variable "tunnels" {
- description = "VPN tunnel configurations, bgp_peer_options is usually null."
+ description = "VPN tunnel configurations."
type = map(object({
bgp_peer = object({
- address = string
- asn = number
- })
- bgp_peer_options = object({
- advertise_groups = list(string)
- advertise_ip_ranges = map(string)
- advertise_mode = string
- route_priority = number
+ address = string
+ asn = number
+ route_priority = optional(number, 1000)
+ custom_advertise = optional(object({
+ all_subnets = bool
+ all_vpc_subnets = bool
+ all_peer_vpc_subnets = bool
+ ip_ranges = map(string)
+ }))
})
# each BGP session on the same Cloud Router must use a unique /30 CIDR
# from the 169.254.0.0/16 block.
bgp_session_range = string
- ike_version = number
+ ike_version = optional(number, 2)
peer_ip = string
- router = string
- shared_secret = string
+ router = optional(string)
+ shared_secret = optional(string)
}))
- default = {}
+ default = {}
+ nullable = false
}
diff --git a/modules/net-vpn-dynamic/versions.tf b/modules/net-vpn-dynamic/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/net-vpn-dynamic/versions.tf
+++ b/modules/net-vpn-dynamic/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/net-vpn-ha/README.md b/modules/net-vpn-ha/README.md
index 45025f9904..0b7b52903a 100644
--- a/modules/net-vpn-ha/README.md
+++ b/modules/net-vpn-ha/README.md
@@ -5,20 +5,21 @@ This module makes it easy to deploy either GCP-to-GCP or GCP-to-On-prem [Cloud H
### GCP to GCP
```hcl
-module "vpn_ha-1" {
- source = "./modules/net-vpn-ha"
- project_id = "string
| ✓ | |
| [network](variables.tf#L22) | VPC used for the gateway and routes. | string
| ✓ | |
-| [project_id](variables.tf#L45) | Project where resources will be created. | string
| ✓ | |
-| [region](variables.tf#L50) | Region used for resources. | string
| ✓ | |
-| [peer_external_gateway](variables.tf#L27) | Configuration of an external VPN gateway to which this VPN is connected. | object({…})
| | null
|
-| [peer_gcp_gateway](variables.tf#L39) | Self Link URL of the peer side HA GCP VPN gateway to which this VPN tunnel is connected. | string
| | null
|
-| [route_priority](variables.tf#L55) | Route priority, defaults to 1000. | number
| | 1000
|
-| [router_advertise_config](variables.tf#L61) | Router custom advertisement configuration, ip_ranges is a map of address ranges and descriptions. | object({…})
| | null
|
-| [router_asn](variables.tf#L71) | Router ASN used for auto-created router. | number
| | 64514
|
-| [router_create](variables.tf#L77) | Create router. | bool
| | true
|
-| [router_name](variables.tf#L83) | Router name used for auto created router, or to specify an existing router to use if `router_create` is set to `true`. Leave blank to use VPN name for auto created router. | string
| | ""
|
-| [tunnels](variables.tf#L89) | VPN tunnel configurations, bgp_peer_options is usually null. | map(object({…}))
| | {}
|
-| [vpn_gateway](variables.tf#L114) | HA VPN Gateway Self Link for using an existing HA VPN Gateway, leave empty if `vpn_gateway_create` is set to `true`. | string
| | null
|
-| [vpn_gateway_create](variables.tf#L120) | Create HA VPN Gateway. | bool
| | true
|
+| [peer_gateway](variables.tf#L27) | Configuration of the (external or GCP) peer gateway. | object({…})
| ✓ | |
+| [project_id](variables.tf#L43) | Project where resources will be created. | string
| ✓ | |
+| [region](variables.tf#L48) | Region used for resources. | string
| ✓ | |
+| [router_config](variables.tf#L53) | Cloud Router configuration for the VPN. If you want to reuse an existing router, set create to false and use name to specify the desired router. | object({…})
| ✓ | |
+| [tunnels](variables.tf#L68) | VPN tunnel configurations. | map(object({…}))
| | {}
|
+| [vpn_gateway](variables.tf#L95) | HA VPN Gateway Self Link for using an existing HA VPN Gateway. Ignored if `vpn_gateway_create` is set to `true`. | string
| | null
|
+| [vpn_gateway_create](variables.tf#L101) | Create HA VPN Gateway. | bool
| | true
|
## Outputs
@@ -167,14 +138,14 @@ module "vpn_ha" {
|---|---|:---:|
| [bgp_peers](outputs.tf#L18) | BGP peer resources. | |
| [external_gateway](outputs.tf#L25) | External VPN gateway resource. | |
-| [gateway](outputs.tf#L34) | VPN gateway resource (only if auto-created). | |
-| [name](outputs.tf#L43) | VPN gateway name (only if auto-created). . | |
-| [random_secret](outputs.tf#L52) | Generated secret. | |
-| [router](outputs.tf#L57) | Router resource (only if auto-created). | |
-| [router_name](outputs.tf#L66) | Router name. | |
-| [self_link](outputs.tf#L71) | HA VPN gateway self link. | |
-| [tunnel_names](outputs.tf#L76) | VPN tunnel names. | |
-| [tunnel_self_links](outputs.tf#L84) | VPN tunnel self links. | |
-| [tunnels](outputs.tf#L92) | VPN tunnel resources. | |
+| [gateway](outputs.tf#L30) | VPN gateway resource (only if auto-created). | |
+| [name](outputs.tf#L35) | VPN gateway name (only if auto-created). . | |
+| [random_secret](outputs.tf#L40) | Generated secret. | |
+| [router](outputs.tf#L45) | Router resource (only if auto-created). | |
+| [router_name](outputs.tf#L50) | Router name. | |
+| [self_link](outputs.tf#L55) | HA VPN gateway self link. | |
+| [tunnel_names](outputs.tf#L60) | VPN tunnel names. | |
+| [tunnel_self_links](outputs.tf#L68) | VPN tunnel self links. | |
+| [tunnels](outputs.tf#L76) | VPN tunnel resources. | |
diff --git a/modules/net-vpn-ha/main.tf b/modules/net-vpn-ha/main.tf
index 8c4870318f..9d53ee080c 100644
--- a/modules/net-vpn-ha/main.tf
+++ b/modules/net-vpn-ha/main.tf
@@ -16,16 +16,10 @@
*/
locals {
- peer_external_gateway = (
- var.peer_external_gateway != null
- ? google_compute_external_vpn_gateway.external_gateway[0].self_link
- : null
-
- )
router = (
- var.router_create
+ var.router_config.create
? try(google_compute_router.router[0].name, null)
- : var.router_name
+ : var.router_config.name
)
vpn_gateway = (
var.vpn_gateway_create
@@ -36,100 +30,79 @@ locals {
}
resource "google_compute_ha_vpn_gateway" "ha_gateway" {
- provider = google-beta
- count = var.vpn_gateway_create ? 1 : 0
- name = var.name
- project = var.project_id
- region = var.region
- network = var.network
+ count = var.vpn_gateway_create ? 1 : 0
+ name = var.name
+ project = var.project_id
+ region = var.region
+ network = var.network
}
resource "google_compute_external_vpn_gateway" "external_gateway" {
- provider = google-beta
- count = var.peer_external_gateway != null ? 1 : 0
+ count = var.peer_gateway.external != null ? 1 : 0
name = "external-${var.name}"
project = var.project_id
- redundancy_type = var.peer_external_gateway.redundancy_type
+ redundancy_type = var.peer_gateway.external.redundancy_type
description = "Terraform managed external VPN gateway"
dynamic "interface" {
- for_each = var.peer_external_gateway.interfaces
+ for_each = var.peer_gateway.external.interfaces
content {
- id = interface.value.id
- ip_address = interface.value.ip_address
+ id = interface.key
+ ip_address = interface.value
}
}
}
resource "google_compute_router" "router" {
- count = var.router_create ? 1 : 0
- name = var.router_name == "" ? "vpn-${var.name}" : var.router_name
+ count = var.router_config.create ? 1 : 0
+ name = coalesce(var.router_config.name, "vpn-${var.name}")
project = var.project_id
region = var.region
network = var.network
bgp {
advertise_mode = (
- var.router_advertise_config == null
- ? null
- : var.router_advertise_config.mode
+ var.router_config.custom_advertise != null
+ ? "CUSTOM"
+ : "DEFAULT"
)
advertised_groups = (
- var.router_advertise_config == null ? null : (
- var.router_advertise_config.mode != "CUSTOM"
- ? null
- : var.router_advertise_config.groups
- )
+ try(var.router_config.custom_advertise.all_subnets, false)
+ ? ["ALL_SUBNETS"]
+ : []
)
dynamic "advertised_ip_ranges" {
- for_each = (
- var.router_advertise_config == null ? {} : (
- var.router_advertise_config.mode != "CUSTOM"
- ? null
- : var.router_advertise_config.ip_ranges
- )
- )
+ for_each = try(var.router_config.custom_advertise.ip_ranges, {})
iterator = range
content {
range = range.key
description = range.value
}
}
- asn = var.router_asn
+ keepalive_interval = try(var.router_config.keepalive, null)
+ asn = var.router_config.asn
}
}
resource "google_compute_router_peer" "bgp_peer" {
- for_each = var.tunnels
- region = var.region
- project = var.project_id
- name = "${var.name}-${each.key}"
- router = local.router
- peer_ip_address = each.value.bgp_peer.address
- peer_asn = each.value.bgp_peer.asn
- advertised_route_priority = (
- each.value.bgp_peer_options == null ? var.route_priority : (
- each.value.bgp_peer_options.route_priority == null
- ? var.route_priority
- : each.value.bgp_peer_options.route_priority
- )
- )
+ for_each = var.tunnels
+ region = var.region
+ project = var.project_id
+ name = "${var.name}-${each.key}"
+ router = coalesce(each.value.router, local.router)
+ peer_ip_address = each.value.bgp_peer.address
+ peer_asn = each.value.bgp_peer.asn
+ advertised_route_priority = each.value.bgp_peer.route_priority
advertise_mode = (
- each.value.bgp_peer_options == null ? null : each.value.bgp_peer_options.advertise_mode
+ try(each.value.bgp_peer.custom_advertise, null) != null
+ ? "CUSTOM"
+ : "DEFAULT"
)
- advertised_groups = (
- each.value.bgp_peer_options == null ? null : (
- each.value.bgp_peer_options.advertise_mode != "CUSTOM"
- ? null
- : each.value.bgp_peer_options.advertise_groups
- )
+ advertised_groups = concat(
+ try(each.value.bgp_peer.custom_advertise.all_subnets, false) ? ["ALL_SUBNETS"] : [],
+ try(each.value.bgp_peer.custom_advertise.all_vpc_subnets, false) ? ["ALL_VPC_SUBNETS"] : [],
+ try(each.value.bgp_peer.custom_advertise.all_peer_vpc_subnets, false) ? ["ALL_PEER_VPC_SUBNETS"] : []
)
dynamic "advertised_ip_ranges" {
- for_each = (
- each.value.bgp_peer_options == null ? {} : (
- each.value.bgp_peer_options.advertise_mode != "CUSTOM"
- ? {}
- : each.value.bgp_peer_options.advertise_ip_ranges
- )
- )
+ for_each = try(each.value.bgp_peer.custom_advertise.ip_ranges, {})
iterator = range
content {
range = range.key
@@ -140,33 +113,29 @@ resource "google_compute_router_peer" "bgp_peer" {
}
resource "google_compute_router_interface" "router_interface" {
- for_each = var.tunnels
- project = var.project_id
- region = var.region
- name = "${var.name}-${each.key}"
- router = local.router
+ for_each = var.tunnels
+ project = var.project_id
+ region = var.region
+ name = "${var.name}-${each.key}"
+ router = local.router
+ # FIXME: can bgp_session_range be null?
ip_range = each.value.bgp_session_range == "" ? null : each.value.bgp_session_range
vpn_tunnel = google_compute_vpn_tunnel.tunnels[each.key].name
}
resource "google_compute_vpn_tunnel" "tunnels" {
- provider = google-beta
for_each = var.tunnels
project = var.project_id
region = var.region
name = "${var.name}-${each.key}"
router = local.router
- peer_external_gateway = local.peer_external_gateway
+ peer_external_gateway = one(google_compute_external_vpn_gateway.external_gateway[*].self_link)
peer_external_gateway_interface = each.value.peer_external_gateway_interface
- peer_gcp_gateway = var.peer_gcp_gateway
+ peer_gcp_gateway = var.peer_gateway.gcp
vpn_gateway_interface = each.value.vpn_gateway_interface
ike_version = each.value.ike_version
- shared_secret = (
- each.value.shared_secret == "" || each.value.shared_secret == null
- ? local.secret
- : each.value.shared_secret
- )
- vpn_gateway = local.vpn_gateway
+ shared_secret = coalesce(each.value.shared_secret, local.secret)
+ vpn_gateway = local.vpn_gateway
}
resource "random_id" "secret" {
diff --git a/modules/net-vpn-ha/outputs.tf b/modules/net-vpn-ha/outputs.tf
index 94aac98296..98b8339499 100644
--- a/modules/net-vpn-ha/outputs.tf
+++ b/modules/net-vpn-ha/outputs.tf
@@ -24,29 +24,17 @@ output "bgp_peers" {
output "external_gateway" {
description = "External VPN gateway resource."
- value = (
- var.peer_external_gateway != null
- ? google_compute_external_vpn_gateway.external_gateway[0]
- : null
- )
+ value = one(google_compute_external_vpn_gateway.external_gateway[*])
}
output "gateway" {
description = "VPN gateway resource (only if auto-created)."
- value = (
- var.vpn_gateway_create
- ? google_compute_ha_vpn_gateway.ha_gateway[0]
- : null
- )
+ value = one(google_compute_ha_vpn_gateway.ha_gateway[*])
}
output "name" {
description = "VPN gateway name (only if auto-created). ."
- value = (
- var.vpn_gateway_create
- ? google_compute_ha_vpn_gateway.ha_gateway[0].name
- : null
- )
+ value = one(google_compute_ha_vpn_gateway.ha_gateway[*].name)
}
output "random_secret" {
@@ -56,11 +44,7 @@ output "random_secret" {
output "router" {
description = "Router resource (only if auto-created)."
- value = (
- var.router_name == ""
- ? google_compute_router.router[0]
- : null
- )
+ value = one(google_compute_router.router[*])
}
output "router_name" {
diff --git a/modules/net-vpn-ha/variables.tf b/modules/net-vpn-ha/variables.tf
index 4e8d17acfe..a423eab155 100644
--- a/modules/net-vpn-ha/variables.tf
+++ b/modules/net-vpn-ha/variables.tf
@@ -24,22 +24,20 @@ variable "network" {
type = string
}
-variable "peer_external_gateway" {
- description = "Configuration of an external VPN gateway to which this VPN is connected."
+variable "peer_gateway" {
+ description = "Configuration of the (external or GCP) peer gateway."
type = object({
- redundancy_type = string
- interfaces = list(object({
- id = number
- ip_address = string
+ external = optional(object({
+ redundancy_type = string
+ interfaces = list(string)
}))
+ gcp = optional(string)
})
- default = null
-}
-
-variable "peer_gcp_gateway" {
- description = "Self Link URL of the peer side HA GCP VPN gateway to which this VPN tunnel is connected."
- type = string
- default = null
+ nullable = false
+ validation {
+ condition = (var.peer_gateway.external != null) != (var.peer_gateway.gcp != null)
+ error_message = "Peer gateway configuration must define exactly one between `external` and `gcp`."
+ }
}
variable "project_id" {
@@ -52,67 +50,50 @@ variable "region" {
type = string
}
-variable "route_priority" {
- description = "Route priority, defaults to 1000."
- type = number
- default = 1000
-}
-
-variable "router_advertise_config" {
- description = "Router custom advertisement configuration, ip_ranges is a map of address ranges and descriptions."
+variable "router_config" {
+ description = "Cloud Router configuration for the VPN. If you want to reuse an existing router, set create to false and use name to specify the desired router."
type = object({
- groups = list(string)
- ip_ranges = map(string)
- mode = string
+ create = optional(bool, true)
+ asn = number
+ name = optional(string)
+ keepalive = optional(number)
+ custom_advertise = optional(object({
+ all_subnets = bool
+ ip_ranges = map(string)
+ }))
})
- default = null
-}
-
-variable "router_asn" {
- description = "Router ASN used for auto-created router."
- type = number
- default = 64514
-}
-
-variable "router_create" {
- description = "Create router."
- type = bool
- default = true
-}
-
-variable "router_name" {
- description = "Router name used for auto created router, or to specify an existing router to use if `router_create` is set to `true`. Leave blank to use VPN name for auto created router."
- type = string
- default = ""
+ nullable = false
}
variable "tunnels" {
- description = "VPN tunnel configurations, bgp_peer_options is usually null."
+ description = "VPN tunnel configurations."
type = map(object({
bgp_peer = object({
- address = string
- asn = number
- })
- bgp_peer_options = object({
- advertise_groups = list(string)
- advertise_ip_ranges = map(string)
- advertise_mode = string
- route_priority = number
+ address = string
+ asn = number
+ route_priority = optional(number, 1000)
+ custom_advertise = optional(object({
+ all_subnets = bool
+ all_vpc_subnets = bool
+ all_peer_vpc_subnets = bool
+ ip_ranges = map(string)
+ }))
})
# each BGP session on the same Cloud Router must use a unique /30 CIDR
# from the 169.254.0.0/16 block.
bgp_session_range = string
- ike_version = number
- peer_external_gateway_interface = number
- router = string
- shared_secret = string
+ ike_version = optional(number, 2)
+ peer_external_gateway_interface = optional(number)
+ router = optional(string)
+ shared_secret = optional(string)
vpn_gateway_interface = number
}))
- default = {}
+ default = {}
+ nullable = false
}
variable "vpn_gateway" {
- description = "HA VPN Gateway Self Link for using an existing HA VPN Gateway, leave empty if `vpn_gateway_create` is set to `true`."
+ description = "HA VPN Gateway Self Link for using an existing HA VPN Gateway. Ignored if `vpn_gateway_create` is set to `true`."
type = string
default = null
}
diff --git a/modules/net-vpn-ha/versions.tf b/modules/net-vpn-ha/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/net-vpn-ha/versions.tf
+++ b/modules/net-vpn-ha/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/net-vpn-static/README.md b/modules/net-vpn-static/README.md
index 1591d9e859..836746dcc2 100644
--- a/modules/net-vpn-static/README.md
+++ b/modules/net-vpn-static/README.md
@@ -4,7 +4,7 @@
```hcl
module "addresses" {
- source = "./modules/net-address"
+ source = "./fabric/modules/net-address"
project_id = var.project_id
external_addresses = {
vpn = "europe-west1"
@@ -12,17 +12,16 @@ module "addresses" {
}
module "vpn" {
- source = "./modules/net-vpn-static"
- project_id = var.project_id
- region = var.region
- network = var.vpc.self_link
- name = "remote"
+ source = "./fabric/modules/net-vpn-static"
+ project_id = var.project_id
+ region = var.region
+ network = var.vpc.self_link
+ name = "remote"
gateway_address_create = false
gateway_address = module.addresses.external_addresses["vpn"].address
- remote_ranges = ["10.10.0.0/24"]
+ remote_ranges = ["10.10.0.0/24"]
tunnels = {
remote-0 = {
- ike_version = 2
peer_ip = "1.1.1.1"
shared_secret = "mysecret"
traffic_selectors = { local = ["0.0.0.0/0"], remote = ["0.0.0.0/0"] }
@@ -41,11 +40,11 @@ module "vpn" {
| [network](variables.tf#L34) | VPC used for the gateway and routes. | string
| ✓ | |
| [project_id](variables.tf#L39) | Project where resources will be created. | string
| ✓ | |
| [region](variables.tf#L44) | Region used for resources. | string
| ✓ | |
-| [gateway_address](variables.tf#L17) | Optional address assigned to the VPN gateway. Ignored unless gateway_address_create is set to false. | string
| | ""
|
+| [gateway_address](variables.tf#L17) | Optional address assigned to the VPN gateway. Ignored unless gateway_address_create is set to false. | string
| | null
|
| [gateway_address_create](variables.tf#L23) | Create external address assigned to the VPN gateway. Needs to be explicitly set to false to use address in gateway_address variable. | bool
| | true
|
| [remote_ranges](variables.tf#L49) | Remote IP CIDR ranges. | list(string)
| | []
|
-| [route_priority](variables.tf#L55) | Route priority, defaults to 1000. | number
| | 1000
|
-| [tunnels](variables.tf#L61) | VPN tunnel configurations. | map(object({…}))
| | {}
|
+| [route_priority](variables.tf#L56) | Route priority, defaults to 1000. | number
| | 1000
|
+| [tunnels](variables.tf#L62) | VPN tunnel configurations. | map(object({…}))
| | {}
|
## Outputs
diff --git a/modules/net-vpn-static/main.tf b/modules/net-vpn-static/main.tf
index 3c8f5cb8d3..f05771c162 100644
--- a/modules/net-vpn-static/main.tf
+++ b/modules/net-vpn-static/main.tf
@@ -91,7 +91,7 @@ resource "google_compute_vpn_tunnel" "tunnels" {
local_traffic_selector = each.value.traffic_selectors.local
remote_traffic_selector = each.value.traffic_selectors.remote
ike_version = each.value.ike_version
- shared_secret = each.value.shared_secret == "" ? local.secret : each.value.shared_secret
+ shared_secret = coalesce(each.value.shared_secret, local.secret)
target_vpn_gateway = google_compute_vpn_gateway.gateway.self_link
depends_on = [google_compute_forwarding_rule.esp]
}
diff --git a/modules/net-vpn-static/variables.tf b/modules/net-vpn-static/variables.tf
index 90b15f531b..935c543a44 100644
--- a/modules/net-vpn-static/variables.tf
+++ b/modules/net-vpn-static/variables.tf
@@ -17,7 +17,7 @@
variable "gateway_address" {
description = "Optional address assigned to the VPN gateway. Ignored unless gateway_address_create is set to false."
type = string
- default = ""
+ default = null
}
variable "gateway_address_create" {
@@ -50,6 +50,7 @@ variable "remote_ranges" {
description = "Remote IP CIDR ranges."
type = list(string)
default = []
+ nullable = false
}
variable "route_priority" {
@@ -61,13 +62,14 @@ variable "route_priority" {
variable "tunnels" {
description = "VPN tunnel configurations."
type = map(object({
- ike_version = number
+ ike_version = optional(number, 2)
peer_ip = string
- shared_secret = string
+ shared_secret = optional(string)
traffic_selectors = object({
local = list(string)
remote = list(string)
})
}))
- default = {}
+ default = {}
+ nullable = false
}
diff --git a/modules/net-vpn-static/versions.tf b/modules/net-vpn-static/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/net-vpn-static/versions.tf
+++ b/modules/net-vpn-static/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/organization/README.md b/modules/organization/README.md
index 7aee01518b..da44046a9d 100644
--- a/modules/organization/README.md
+++ b/modules/organization/README.md
@@ -6,33 +6,69 @@ This module allows managing several organization properties:
- custom IAM roles
- audit logging configuration for services
- organization policies
+- organization policy custom constraints
+
+To manage organization policies, the `orgpolicy.googleapis.com` service should be enabled in the quota project.
## Example
```hcl
module "org" {
- source = "./modules/organization"
+ 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"]
}
- policy_boolean = {
- "constraints/compute.disableGuestAttributesAccess" = true
- "constraints/compute.skipDefaultNetworkCreation" = true
+ iam_additive_members = {
+ "user:compute@example.org" = ["roles/compute.admin", "roles/container.viewer"]
}
- policy_list = {
+
+ org_policies = {
+ "custom.gkeEnableAutoUpgrade" = {
+ enforce = true
+ }
+ "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 = "somewhere"
+ }
+ enforce = true
+ }
+ ]
+ }
+ "constraints/iam.allowedPolicyMemberDomains" = {
+ allow = {
+ values = ["C0xxxxxxx", "C0yyyyyyy"]
+ }
+ }
"constraints/compute.trustedImageProjects" = {
- inherit_from_parent = null
- suggested_value = null
- status = true
- values = ["projects/my-project"]
+ allow = {
+ values = ["projects/my-project"]
+ }
+ }
+ "constraints/compute.vmExternalIpAccess" = {
+ deny = { all = true }
}
}
}
-# tftest modules=1 resources=6
+# tftest modules=1 resources=13 inventory=basic.yaml
```
## IAM
@@ -45,14 +81,106 @@ There are several mutually exclusive ways of managing IAM in this module
If you set audit policies via the `iam_audit_config_authoritative` variable, be sure to also configure IAM bindings via `iam_bindings_authoritative`, as audit policies use the underlying `google_organization_iam_policy` resource, which is also authoritative for any role.
-Some care must also be takend with the `groups_iam` variable (and in some situations with the additive variables) to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
+Some care must also be taken with the `groups_iam` variable (and in some situations with the additive variables) to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
+
+### Organization policy factory
+
+See the [organization policy factory in the project module](../project#organization-policy-factory).
+
+### Org policy custom constraints
+
+Refer to the [Creating and managing custom constraints](https://cloud.google.com/resource-manager/docs/organization-policy/creating-managing-custom-constraints) documentation for details on usage.
+To manage organization policy custom constraints, the `orgpolicy.googleapis.com` service should be enabled in the quota project.
+
+```hcl
+module "org" {
+ source = "./fabric/modules/organization"
+ organization_id = var.organization_id
+
+ 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."
+ }
+ }
+
+ # not necessarily to enforce on the org level, policy may be applied on folder/project levels
+ org_policies = {
+ "custom.gkeEnableAutoUpgrade" = {
+ enforce = true
+ }
+ }
+}
+# tftest modules=1 resources=2 inventory=custom-constraints.yaml
+```
+
+### Org policy custom constraints factory
+
+Org policy custom constraints can be loaded from a directory containing YAML files where each file defines one or more custom constraints. The structure of the YAML files is exactly the same as the `org_policy_custom_constraints` variable.
+
+The example below deploys a few org policy custom constraints split between two YAML files.
+
+```hcl
+module "org" {
+ 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 inventory=custom-constraints.yaml
+```
+
+```yaml
+# tftest-file id=gke path=configs/custom-constraints/gke.yaml
+custom.gkeEnableLogging:
+ resource_types:
+ - container.googleapis.com/Cluster
+ method_types:
+ - CREATE
+ - UPDATE
+ condition: resource.loggingService == "none"
+ action_type: DENY
+ display_name: Do not disable Cloud Logging
+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.
+```
+
+
+```yaml
+# tftest-file id=dataproc path=configs/custom-constraints/dataproc.yaml
+custom.dataprocNoMoreThan10Workers:
+ resource_types:
+ - dataproc.googleapis.com/Cluster
+ method_types:
+ - CREATE
+ - UPDATE
+ condition: resource.config.workerConfig.numInstances + resource.config.secondaryWorkerConfig.numInstances > 10
+ action_type: DENY
+ display_name: Total number of worker instances cannot be larger than 10
+ description: Cluster cannot have more than 10 workers, including primary and secondary workers.
+```
## Hierarchical firewall policies
-Hirerarchical firewall policies can be managed in two ways:
+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](../../examples/factories) for more context on factories)
+- 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.
@@ -60,10 +188,21 @@ Once you have policies (either created via the module or externally), you can as
```hcl
module "org" {
- source = "./modules/organization"
+ source = "./fabric/modules/organization"
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"
@@ -83,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
@@ -92,23 +231,22 @@ The in-built factory allows you to define a single policy, using one file for ru
```hcl
module "org" {
- source = "./modules/organization"
+ source = "./fabric/modules/organization"
organization_id = var.organization_id
firewall_policy_factory = {
- cidr_file = "data/cidrs.yaml"
- policy_name = null
- rules_file = "data/rules.yaml"
+ cidr_file = "configs/firewall-policies/cidrs.yaml"
+ 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 skip
+# tftest modules=1 resources=4 files=cidrs,rules inventory=hfw.yaml
```
```yaml
-# cidrs.yaml
-
+# tftest-file id=cidrs path=configs/firewall-policies/cidrs.yaml
rfc1918:
- 10.0.0.0/8
- 172.16.0.0/12
@@ -116,8 +254,7 @@ rfc1918:
```
```yaml
-# 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
@@ -128,102 +265,92 @@ 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
```hcl
module "gcs" {
- source = "./modules/gcs"
+ source = "./fabric/modules/gcs"
project_id = var.project_id
name = "gcs_sink"
force_destroy = true
}
module "dataset" {
- source = "./modules/bigquery-dataset"
+ source = "./fabric/modules/bigquery-dataset"
project_id = var.project_id
id = "bq_sink"
}
module "pubsub" {
- source = "./modules/pubsub"
+ source = "./fabric/modules/pubsub"
project_id = var.project_id
name = "pubsub_sink"
}
module "bucket" {
- source = "./modules/logging-bucket"
+ source = "./fabric/modules/logging-bucket"
parent_type = "project"
parent = "my-project"
id = "bucket"
}
module "org" {
- source = "./modules/organization"
+ source = "./fabric/modules/organization"
organization_id = var.organization_id
logging_sinks = {
warnings = {
- type = "storage"
- destination = module.gcs.name
- filter = "severity=WARNING"
- include_children = true
- bq_partitioned_table = null
- exclusions = {}
+ destination = module.gcs.id
+ filter = "severity=WARNING"
+ type = "storage"
}
info = {
- type = "bigquery"
+ bq_partitioned_table = true
destination = module.dataset.id
filter = "severity=INFO"
- include_children = true
- bq_partitioned_table = true
- exclusions = {}
+ type = "bigquery"
}
notice = {
- type = "pubsub"
- destination = module.pubsub.id
- filter = "severity=NOTICE"
- include_children = true
- bq_partitioned_table = null
- exclusions = {}
+ destination = module.pubsub.id
+ filter = "severity=NOTICE"
+ type = "pubsub"
}
debug = {
- type = "logging"
- destination = module.bucket.id
- filter = "severity=DEBUG"
- include_children = false
- bq_partitioned_table = null
- exclusions = {
+ destination = module.bucket.id
+ filter = "severity=DEBUG"
+ exclusions = {
no-compute = "logName:compute"
}
+ type = "logging"
}
}
logging_exclusions = {
no-gce-instances = "resource.type=gce_instance"
}
}
-# tftest modules=5 resources=13
+# tftest modules=5 resources=13 inventory=logging.yaml
```
## Custom Roles
```hcl
module "org" {
- source = "./modules/organization"
+ source = "./fabric/modules/organization"
organization_id = var.organization_id
custom_roles = {
"myRole" = [
@@ -234,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
@@ -243,16 +370,16 @@ Refer to the [Creating and managing tags](https://cloud.google.com/resource-mana
```hcl
module "org" {
- source = "./modules/organization"
+ source = "./fabric/modules/organization"
organization_id = var.organization_id
tags = {
environment = {
- description = "Environment specification."
- iam = {
+ description = "Environment specification."
+ iam = {
"roles/resourcemanager.tagAdmin" = ["group:admins@example.com"]
}
values = {
- dev = null
+ dev = {}
prod = {
description = "Environment: production."
iam = {
@@ -267,7 +394,35 @@ 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*:
+
+```hcl
+module "org" {
+ source = "./fabric/modules/organization"
+ organization_id = var.organization_id
+ network_tags = {
+ net-environment = {
+ description = "This is a network tag."
+ network = "my_project/my_vpc"
+ iam = {
+ "roles/resourcemanager.tagAdmin" = ["group:admins@example.com"]
+ }
+ values = {
+ dev = null
+ prod = {
+ description = "Environment: production."
+ iam = {
+ "roles/resourcemanager.tagUser" = ["user:user1@example.com"]
+ }
+ }
+ }
+ }
+ }
+}
+# tftest modules=1 resources=5 inventory=network-tags.yaml
```
@@ -281,7 +436,8 @@ module "org" {
| [iam.tf](./iam.tf) | IAM bindings, roles and audit logging resources. | google_organization_iam_audit_config
· google_organization_iam_binding
· google_organization_iam_custom_role
· google_organization_iam_member
· google_organization_iam_policy
|
| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member
· google_logging_organization_exclusion
· google_logging_organization_sink
· google_project_iam_member
· google_pubsub_topic_iam_member
· google_storage_bucket_iam_member
|
| [main.tf](./main.tf) | Module-level locals and resources. | google_essential_contacts_contact
|
-| [organization-policies.tf](./organization-policies.tf) | Organization-level organization policies. | google_organization_policy
|
+| [org-policy-custom-constraints.tf](./org-policy-custom-constraints.tf) | None | google_org_policy_custom_constraint
|
+| [organization-policies.tf](./organization-policies.tf) | Organization-level organization policies. | google_org_policy_policy
|
| [outputs.tf](./outputs.tf) | Module outputs. | |
| [tags.tf](./tags.tf) | None | google_tags_tag_binding
· google_tags_tag_key
· google_tags_tag_key_iam_binding
· google_tags_tag_value
· google_tags_tag_value_iam_binding
|
| [variables.tf](./variables.tf) | Module variables. | |
@@ -291,7 +447,7 @@ module "org" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [organization_id](variables.tf#L151) | Organization id in organizations/nnnnnn format. | string
| ✓ | |
+| [organization_id](variables.tf#L246) | Organization id in organizations/nnnnnn format. | string
| ✓ | |
| [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string))
| | {}
|
| [custom_roles](variables.tf#L24) | Map of role name => list of permissions to create in this project. | map(list(string))
| | {}
|
| [firewall_policies](variables.tf#L31) | Hierarchical firewall policy rules created in the organization. | map(map(object({…})))
| | {}
|
@@ -305,23 +461,28 @@ module "org" {
| [iam_audit_config_authoritative](variables.tf#L105) | IAM Authoritative service audit logging configuration. Service as key, map of log permission (eg DATA_READ) and excluded members as value for each service. Audit config should also be authoritative when using authoritative bindings. Use with caution. | map(map(list(string)))
| | null
|
| [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 this organization. | map(object({…}))
| | {}
|
-| [policy_boolean](variables.tf#L160) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool)
| | {}
|
-| [policy_list](variables.tf#L167) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…}))
| | {}
|
-| [tag_bindings](variables.tf#L179) | Tag bindings for this organization, in key => tag value id format. | map(string)
| | null
|
-| [tags](variables.tf#L185) | Tags by key name. The `iam` attribute behaves like the similarly named one at module level. | map(object({…}))
| | null
|
+| [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({…}))
| | {}
|
+| [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({…}))
| | {}
|
## Outputs
| name | description | sensitive |
|---|---|:---:|
-| [custom_role_id](outputs.tf#L18) | Map of custom role IDs created in the organization. | |
-| [custom_roles](outputs.tf#L31) | Map of custom roles resources created in the organization. | |
-| [firewall_policies](outputs.tf#L36) | Map of firewall policy resources created in the organization. | |
-| [firewall_policy_id](outputs.tf#L41) | Map of firewall policy ids created in the organization. | |
-| [organization_id](outputs.tf#L46) | Organization id dependent on module resources. | |
-| [sink_writer_identities](outputs.tf#L64) | Writer identities created for each sink. | |
-| [tag_keys](outputs.tf#L72) | Tag key resources. | |
-| [tag_values](outputs.tf#L79) | Tag value resources. | |
+| [custom_role_id](outputs.tf#L17) | Map of custom role IDs created in the organization. | |
+| [custom_roles](outputs.tf#L30) | Map of custom roles resources created in the organization. | |
+| [firewall_policies](outputs.tf#L35) | Map of firewall policy resources created in the organization. | |
+| [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. | |
diff --git a/modules/organization/logging.tf b/modules/organization/logging.tf
index 0beeb0f876..e78a2c4d4e 100644
--- a/modules/organization/logging.tf
+++ b/modules/organization/logging.tf
@@ -29,13 +29,15 @@ locals {
resource "google_logging_organization_sink" "sink" {
for_each = var.logging_sinks
name = each.key
+ description = coalesce(each.value.description, "${each.key} (Terraform-managed).")
org_id = local.organization_id_numeric
destination = "${each.value.type}.googleapis.com/${each.value.destination}"
filter = each.value.filter
include_children = each.value.include_children
+ disabled = each.value.disabled
dynamic "bigquery_options" {
- for_each = each.value.bq_partitioned_table == true ? [""] : []
+ for_each = each.value.type == "biquery" && each.value.bq_partitioned_table != null ? [""] : []
content {
use_partitioned_tables = each.value.bq_partitioned_table
}
@@ -49,6 +51,7 @@ resource "google_logging_organization_sink" "sink" {
filter = exclusion.value
}
}
+
depends_on = [
google_organization_iam_binding.authoritative,
google_organization_iam_member.additive,
@@ -84,7 +87,12 @@ resource "google_project_iam_member" "bucket-sinks-binding" {
project = split("/", each.value.destination)[1]
role = "roles/logging.bucketWriter"
member = google_logging_organization_sink.sink[each.key].writer_identity
- # TODO(jccb): use a condition to limit writer-identity only to this bucket
+
+ condition {
+ title = "${each.key} bucket writer"
+ description = "Grants bucketWriter to ${google_logging_organization_sink.sink[each.key].writer_identity} used by log sink ${each.key} on ${var.organization_id}"
+ expression = "resource.name.endsWith('${each.value.destination}')"
+ }
}
resource "google_logging_organization_exclusion" "logging-exclusion" {
diff --git a/modules/organization/org-policy-custom-constraints.tf b/modules/organization/org-policy-custom-constraints.tf
new file mode 100644
index 0000000000..6a8cf5e6d4
--- /dev/null
+++ b/modules/organization/org-policy-custom-constraints.tf
@@ -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.
+ */
+
+locals {
+ _custom_constraints_factory_data_raw = merge([
+ for f in try(fileset(var.org_policy_custom_constraints_data_path, "*.yaml"), []) :
+ yamldecode(file("${var.org_policy_custom_constraints_data_path}/${f}"))
+ ]...)
+
+
+ _custom_constraints_factory_data = {
+ for k, v in local._custom_constraints_factory_data_raw :
+ k => {
+ display_name = try(v.display_name, null)
+ description = try(v.description, null)
+ action_type = v.action_type
+ condition = v.condition
+ method_types = v.method_types
+ resource_types = v.resource_types
+ }
+ }
+
+ _custom_constraints = merge(local._custom_constraints_factory_data, var.org_policy_custom_constraints)
+
+ custom_constraints = {
+ for k, v in local._custom_constraints :
+ k => merge(v, {
+ name = k
+ parent = var.organization_id
+ })
+ }
+}
+
+resource "google_org_policy_custom_constraint" "constraint" {
+ provider = google-beta
+
+ for_each = local.custom_constraints
+ name = each.value.name
+ parent = each.value.parent
+ display_name = each.value.display_name
+ description = each.value.description
+ action_type = each.value.action_type
+ condition = each.value.condition
+ method_types = each.value.method_types
+ resource_types = each.value.resource_types
+}
diff --git a/modules/organization/organization-policies.tf b/modules/organization/organization-policies.tf
index f23a98b488..1a99ef9a1c 100644
--- a/modules/organization/organization-policies.tf
+++ b/modules/organization/organization-policies.tf
@@ -16,83 +16,127 @@
# tfdoc:file:description Organization-level organization policies.
-resource "google_organization_policy" "boolean" {
- for_each = var.policy_boolean
- org_id = local.organization_id_numeric
- constraint = each.key
+locals {
+ _factory_data_raw = merge([
+ for f in try(fileset(var.org_policies_data_path, "*.yaml"), []) :
+ yamldecode(file("${var.org_policies_data_path}/${f}"))
+ ]...)
- dynamic "boolean_policy" {
- for_each = each.value == null ? [] : [each.value]
- iterator = policy
- content {
- enforced = policy.value
- }
- }
+ # simulate applying defaults to data coming from yaml files
+ _factory_data = {
+ for k, v in local._factory_data_raw :
+ k => {
+ inherit_from_parent = try(v.inherit_from_parent, null)
+ reset = try(v.reset, null)
+ allow = can(v.allow) ? {
+ all = try(v.allow.all, null)
+ values = try(v.allow.values, null)
+ } : null
+ deny = can(v.deny) ? {
+ all = try(v.deny.all, null)
+ values = try(v.deny.values, null)
+ } : null
+ enforce = try(v.enforce, true)
- dynamic "restore_policy" {
- for_each = each.value == null ? [""] : []
- content {
- default = true
+ rules = [
+ for r in try(v.rules, []) : {
+ allow = can(r.allow) ? {
+ all = try(r.allow.all, null)
+ values = try(r.allow.values, null)
+ } : null
+ deny = can(r.deny) ? {
+ all = try(r.deny.all, null)
+ values = try(r.deny.values, null)
+ } : null
+ enforce = try(r.enforce, true)
+ condition = {
+ description = try(r.condition.description, null)
+ expression = try(r.condition.expression, null)
+ location = try(r.condition.location, null)
+ title = try(r.condition.title, null)
+ }
+ }
+ ]
}
}
- depends_on = [
- google_organization_iam_audit_config.config,
- google_organization_iam_binding.authoritative,
- google_organization_iam_custom_role.roles,
- google_organization_iam_member.additive,
- google_organization_iam_policy.authoritative,
- ]
-}
+ _org_policies = merge(local._factory_data, var.org_policies)
-resource "google_organization_policy" "list" {
- for_each = var.policy_list
- org_id = local.organization_id_numeric
- constraint = each.key
+ org_policies = {
+ for k, v in local._org_policies :
+ k => merge(v, {
+ name = "${var.organization_id}/policies/${k}"
+ parent = var.organization_id
- dynamic "list_policy" {
- for_each = each.value.status == null ? [] : [each.value]
- iterator = policy
- content {
- inherit_from_parent = policy.value.inherit_from_parent
- suggested_value = policy.value.suggested_value
- dynamic "allow" {
- for_each = policy.value.status ? [""] : []
- content {
- values = (
- try(length(policy.value.values) > 0, false)
- ? policy.value.values
- : null
- )
- all = (
- try(length(policy.value.values) > 0, false)
- ? null
- : true
+ is_boolean_policy = v.allow == null && v.deny == null
+ has_values = (
+ length(coalesce(try(v.allow.values, []), [])) > 0 ||
+ length(coalesce(try(v.deny.values, []), [])) > 0
+ )
+ rules = [
+ for r in v.rules :
+ merge(r, {
+ has_values = (
+ length(coalesce(try(r.allow.values, []), [])) > 0 ||
+ length(coalesce(try(r.deny.values, []), [])) > 0
)
+ })
+ ]
+ })
+ }
+}
+
+resource "google_org_policy_policy" "default" {
+ for_each = local.org_policies
+ name = each.value.name
+ parent = each.value.parent
+
+ spec {
+ inherit_from_parent = each.value.inherit_from_parent
+ reset = each.value.reset
+
+ dynamic "rules" {
+ for_each = each.value.rules
+ iterator = rule
+ content {
+ allow_all = try(rule.value.allow.all, false) == true ? "TRUE" : null
+ deny_all = try(rule.value.deny.all, false) == true ? "TRUE" : null
+ enforce = (
+ each.value.is_boolean_policy && rule.value.enforce != null
+ ? upper(tostring(rule.value.enforce))
+ : null
+ )
+ condition {
+ description = rule.value.condition.description
+ expression = rule.value.condition.expression
+ location = rule.value.condition.location
+ title = rule.value.condition.title
}
- }
- dynamic "deny" {
- for_each = policy.value.status ? [] : [""]
- content {
- values = (
- try(length(policy.value.values) > 0, false)
- ? policy.value.values
- : null
- )
- all = (
- try(length(policy.value.values) > 0, false)
- ? null
- : true
- )
+ dynamic "values" {
+ for_each = rule.value.has_values ? [1] : []
+ content {
+ allowed_values = try(rule.value.allow.values, null)
+ denied_values = try(rule.value.deny.values, null)
+ }
}
}
}
- }
- dynamic "restore_policy" {
- for_each = each.value.status == null ? [true] : []
- content {
- default = true
+ 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)
+ }
+ }
}
}
@@ -102,5 +146,6 @@ resource "google_organization_policy" "list" {
google_organization_iam_custom_role.roles,
google_organization_iam_member.additive,
google_organization_iam_policy.authoritative,
+ google_org_policy_custom_constraint.constraint,
]
}
diff --git a/modules/organization/outputs.tf b/modules/organization/outputs.tf
index 1679a1d70b..40d84b473a 100644
--- a/modules/organization/outputs.tf
+++ b/modules/organization/outputs.tf
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-
output "custom_role_id" {
description = "Map of custom role IDs created in the organization."
value = {
@@ -43,6 +42,26 @@ output "firewall_policy_id" {
value = { for k, v in google_compute_firewall_policy.policy : k => v.id }
}
+output "network_tag_keys" {
+ description = "Tag key resources."
+ value = {
+ for k, v in google_tags_tag_key.default : k => v if(
+ v.purpose != null && v.purpose != ""
+ )
+ }
+}
+
+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 != ""
+ )
+ }
+}
+
output "organization_id" {
description = "Organization id dependent on module resources."
value = var.organization_id
@@ -52,8 +71,7 @@ output "organization_id" {
google_organization_iam_custom_role.roles,
google_organization_iam_member.additive,
google_organization_iam_policy.authoritative,
- google_organization_policy.boolean,
- google_organization_policy.list,
+ google_org_policy_policy.default,
google_tags_tag_key.default,
google_tags_tag_key_iam_binding.default,
google_tags_tag_value.default,
@@ -72,13 +90,19 @@ output "sink_writer_identities" {
output "tag_keys" {
description = "Tag key resources."
value = {
- for k, v in google_tags_tag_key.default : k => v
+ for k, v in google_tags_tag_key.default : k => v if(
+ v.purpose == null || v.purpose == ""
+ )
}
}
output "tag_values" {
description = "Tag value resources."
value = {
- for k, v in google_tags_tag_value.default : k => v
+ 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 == ""
+ )
}
}
diff --git a/modules/organization/tags.tf b/modules/organization/tags.tf
index 7aa94cd1bb..544b8989fc 100644
--- a/modules/organization/tags.tf
+++ b/modules/organization/tags.tf
@@ -55,10 +55,7 @@ locals {
tag_values_iam = {
for t in local._tag_values_iam : "${t.key}:${t.role}" => t
}
- tags = {
- for k, v in coalesce(var.tags, {}) :
- k => v == null ? { description = null, iam = {}, values = null } : v
- }
+ tags = merge(var.tags, var.network_tags)
tags_iam = {
for t in local._tags_iam : "${t.tag}:${t.role}" => t
}
@@ -67,13 +64,16 @@ locals {
# keys
resource "google_tags_tag_key" "default" {
- for_each = local.tags
- parent = var.organization_id
- short_name = each.key
- description = coalesce(
- each.value.description,
- "Managed by the Terraform organization module."
+ for_each = local.tags
+ parent = var.organization_id
+ purpose = (
+ lookup(each.value, "network", null) == null ? null : "GCE_FIREWALL"
+ )
+ purpose_data = (
+ lookup(each.value, "network", null) == null ? null : { network = each.value.network }
)
+ short_name = each.key
+ description = each.value.description
depends_on = [
google_organization_iam_binding.authoritative,
google_organization_iam_member.additive,
@@ -93,13 +93,10 @@ 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
- short_name = each.value.name
- description = coalesce(
- each.value.description,
- "Managed by the Terraform organization module."
- )
+ for_each = local.tag_values
+ parent = google_tags_tag_key.default[each.value.tag].id
+ short_name = each.value.name
+ description = each.value.description
}
resource "google_tags_tag_value_iam_binding" "default" {
diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf
index 9ffce95ce9..84c81ff5b5 100644
--- a/modules/organization/variables.tf
+++ b/modules/organization/variables.tf
@@ -127,55 +127,131 @@ variable "logging_exclusions" {
}
variable "logging_sinks" {
- description = "Logging sinks to create for this organization."
+ description = "Logging sinks to create for the organization."
type = map(object({
+ bq_partitioned_table = optional(bool)
+ description = optional(string)
destination = string
- type = string
+ disabled = optional(bool, false)
+ exclusions = optional(map(string), {})
filter = string
- include_children = bool
- bq_partitioned_table = bool
- # TODO exclusions also support description and disabled
- exclusions = map(string)
+ include_children = optional(bool, true)
+ type = string
}))
+ default = {}
+ nullable = false
validation {
condition = alltrue([
- for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) :
+ for k, v in var.logging_sinks :
contains(["bigquery", "logging", "pubsub", "storage"], v.type)
])
error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'."
}
- default = {}
- nullable = false
+ validation {
+ condition = alltrue([
+ for k, v in var.logging_sinks :
+ v.bq_partitioned_table != true || v.type == "bigquery"
+ ])
+ error_message = "Can only set bq_partitioned_table when type is `bigquery`."
+ }
}
-variable "organization_id" {
- description = "Organization id in organizations/nnnnnn format."
- type = string
+variable "network_tags" {
+ description = "Network tags by key name. 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)), {})
+ network = string # project_id/vpc_name
+ values = optional(map(object({
+ description = optional(string, "Managed by the Terraform organization module.")
+ iam = optional(map(list(string)), {})
+ })), {})
+ }))
+ nullable = false
+ default = {}
validation {
- condition = can(regex("^organizations/[0-9]+", var.organization_id))
- error_message = "The organization_id must in the form organizations/nnn."
+ condition = alltrue([
+ for k, v in var.network_tags : v != null
+ ])
+ error_message = "Use an empty map instead of null as value."
}
}
-variable "policy_boolean" {
- description = "Map of boolean org policies and enforcement value, set value to null for policy restore."
- type = map(bool)
- default = {}
- nullable = false
+variable "org_policies" {
+ description = "Organization policies applied to this organization keyed by policy name."
+ type = map(object({
+ inherit_from_parent = optional(bool) # for list policies only.
+ reset = optional(bool)
+
+ # default (unconditional) values
+ allow = optional(object({
+ all = optional(bool)
+ values = optional(list(string))
+ }))
+ deny = optional(object({
+ all = optional(bool)
+ values = optional(list(string))
+ }))
+ enforce = optional(bool, true) # for boolean policies only.
+
+ # conditional values
+ rules = optional(list(object({
+ allow = optional(object({
+ all = optional(bool)
+ values = optional(list(string))
+ }))
+ deny = optional(object({
+ all = optional(bool)
+ values = optional(list(string))
+ }))
+ enforce = optional(bool, true) # for boolean policies only.
+ condition = object({
+ description = optional(string)
+ expression = optional(string)
+ location = optional(string)
+ title = optional(string)
+ })
+ })), [])
+ }))
+ default = {}
+ nullable = false
+}
+
+variable "org_policies_data_path" {
+ description = "Path containing org policies in YAML format."
+ type = string
+ default = null
}
-variable "policy_list" {
- description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny."
+variable "org_policy_custom_constraints" {
+ description = "Organization policiy custom constraints keyed by constraint name."
type = map(object({
- inherit_from_parent = bool
- suggested_value = string
- status = bool
- values = list(string)
+ display_name = optional(string)
+ description = optional(string)
+ action_type = string
+ condition = string
+ method_types = list(string)
+ resource_types = list(string)
}))
default = {}
nullable = false
}
+variable "org_policy_custom_constraints_data_path" {
+ description = "Path containing org policy custom constraints in YAML format."
+ type = string
+ default = null
+}
+
+variable "organization_id" {
+ description = "Organization id in organizations/nnnnnn format."
+ type = string
+ validation {
+ condition = can(regex("^organizations/[0-9]+", var.organization_id))
+ error_message = "The organization_id must in the form organizations/nnn."
+ }
+}
+
variable "tag_bindings" {
description = "Tag bindings for this organization, in key => tag value id format."
type = map(string)
@@ -185,12 +261,19 @@ variable "tag_bindings" {
variable "tags" {
description = "Tags by key name. The `iam` attribute behaves like the similarly named one at module level."
type = map(object({
- description = string
- iam = map(list(string))
- values = map(object({
- description = string
- iam = map(list(string))
- }))
+ description = optional(string, "Managed by the Terraform organization module.")
+ iam = optional(map(list(string)), {})
+ values = optional(map(object({
+ description = optional(string, "Managed by the Terraform organization module.")
+ iam = optional(map(list(string)), {})
+ })), {})
}))
- default = null
+ nullable = false
+ default = {}
+ validation {
+ condition = alltrue([
+ for k, v in var.tags : v != null
+ ])
+ error_message = "Use an empty map instead of null as value."
+ }
}
diff --git a/modules/organization/versions.tf b/modules/organization/versions.tf
index e72a78007a..90b632f6d4 100644
--- a/modules/organization/versions.tf
+++ b/modules/organization/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.1.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/project/README.md b/modules/project/README.md
index 42d435d9ee..3753a5da27 100644
--- a/modules/project/README.md
+++ b/modules/project/README.md
@@ -1,8 +1,36 @@
# Project Module
-## Examples
+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.
-### Minimal example with IAM
+# 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:
+
+- `group_iam` and `iam` configure authoritative bindings that manage individual roles exclusively, mapping to the [`google_project_iam_binding`](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_iam#google_project_iam_binding) resource
+- `iam_additive` and `iam_additive_members` configure additive bindings that only manage individual role/member pairs, mapping to the [`google_project_iam_member`](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_iam#google_project_iam_member) resource
+
+Be mindful about service identity roles when using authoritative IAM, as you might inadvertently remove a role from a [service identity](https://cloud.google.com/iam/docs/service-accounts#google-managed) or default service account. For example, using `roles/editor` with `iam` or `group_iam` will remove the default permissions for the Cloud Services identity. A simple workaround for these scenarios is described below.
+
+### Authoritative IAM
+
+The `iam` variable is based on role keys and is typically used for service accounts, or where member values can be dynamic and would create potential problems in the underlying `for_each` cycle.
```hcl
locals {
@@ -10,12 +38,12 @@ locals {
}
module "project" {
- source = "./modules/project"
+ source = "./fabric/modules/project"
billing_account = "123456-123456-123456"
name = "project-example"
parent = "folders/1234567890"
prefix = "foo"
- services = [
+ services = [
"container.googleapis.com",
"stackdriver.googleapis.com"
]
@@ -25,46 +53,117 @@ module "project" {
]
}
}
-# tftest modules=1 resources=4
+# tftest modules=1 resources=4 inventory=iam-authoritative.yaml
```
-### Minimal example with IAM additive roles
+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.
```hcl
module "project" {
- source = "./modules/project"
+ source = "./fabric/modules/project"
+ billing_account = "123456-123456-123456"
name = "project-example"
+ parent = "folders/1234567890"
+ prefix = "foo"
+ group_iam = {
+ "gcp-security-admins@example.com" = [
+ "roles/cloudasset.owner",
+ "roles/cloudsupport.techSupportEditor",
+ "roles/iam.securityReviewer",
+ "roles/logging.admin",
+ ]
+ }
+}
+# tftest modules=1 resources=5 inventory=iam-group.yaml
+```
+
+### Additive IAM
+Additive IAM is typically used where bindings for specific roles are controlled by different modules or in different Terraform stages. One example is when the project is created by one team but a different team manages service account creation for the project, and some of the project-level roles overlap in the two configurations.
+
+```hcl
+module "project" {
+ source = "./fabric/modules/project"
+ name = "project-example"
iam_additive = {
- "roles/viewer" = [
- "group:one@example.org", "group:two@xample.org"
+ "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
```
-### Shared VPC service
+### Additive IAM by members
```hcl
module "project" {
- source = "./modules/project"
- name = "project-example"
+ 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
+
+As mentioned above, there are cases where authoritative management of specific IAM roles results in removal of default bindings from service identities. One example is outlined below, with a simple workaround leveraging the `service_accounts` output to identify the service identity. A full list of service identities and their roles can be found [here](https://cloud.google.com/iam/docs/service-agents).
+
+```hcl
+module "project" {
+ source = "./fabric/modules/project"
+ name = "project-example"
+ group_iam = {
+ "foo@example.com" = [
+ "roles/editor"
+ ]
+ }
+ iam = {
+ "roles/editor" = [
+ "serviceAccount:${module.project.service_accounts.cloud_services}"
+ ]
+ }
+}
+# tftest modules=1 resources=2
+```
+
+## 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 "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" = [
@@ -73,123 +172,193 @@ module "project" {
}
}
}
-# tftest modules=1 resources=6
+# tftest modules=2 resources=8 inventory=shared-vpc.yaml
```
-### Organization policies
+## Organization policies
+
+To manage organization policies, the `orgpolicy.googleapis.com` service should be enabled in the quota project.
```hcl
module "project" {
- source = "./modules/project"
+ source = "./fabric/modules/project"
billing_account = "123456-123456-123456"
name = "project-example"
parent = "folders/1234567890"
prefix = "foo"
- services = [
- "container.googleapis.com",
- "stackdriver.googleapis.com"
- ]
- policy_boolean = {
- "constraints/compute.disableGuestAttributesAccess" = true
- "constraints/compute.skipDefaultNetworkCreation" = true
- }
- policy_list = {
+ org_policies = {
+ "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 = "somewhere"
+ }
+ enforce = true
+ }
+ ]
+ }
+ "constraints/iam.allowedPolicyMemberDomains" = {
+ allow = {
+ values = ["C0xxxxxxx", "C0yyyyyyy"]
+ }
+ }
"constraints/compute.trustedImageProjects" = {
- inherit_from_parent = null
- suggested_value = null
- status = true
- values = ["projects/my-project"]
+ allow = {
+ values = ["projects/my-project"]
+ }
+ }
+ "constraints/compute.vmExternalIpAccess" = {
+ deny = { all = true }
}
}
}
-# tftest modules=1 resources=6
+# tftest modules=1 resources=8 inventory=org-policies.yaml
```
+### Organization policy factory
+
+Organization policies can be loaded from a directory containing YAML files where each file defines one or more constraints. The structure of the YAML files is exactly the same as the `org_policies` variable.
+
+Note that contraints defined via `org_policies` take precedence over those in `org_policies_data_path`. In other words, if you specify the same contraint in a YAML file *and* in the `org_policies` variable, the latter will take priority.
+
+The example below deploys a few organization policies split between two YAML files.
+
+```hcl
+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=8 files=boolean,list inventory=org-policies.yaml
+```
+
+```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:
+ description: test condition
+ expression: resource.matchTagId("tagKeys/1234", "tagValues/1234")
+ location: somewhere
+ title: condition
+ enforce: true
+```
+
+```yaml
+# tftest-file id=list path=configs/org-policies/list.yaml
+constraints/compute.trustedImageProjects:
+ allow:
+ values:
+ - projects/my-project
+constraints/compute.vmExternalIpAccess:
+ deny:
+ all: true
+constraints/iam.allowedPolicyMemberDomains:
+ allow:
+ values:
+ - C0xxxxxxx
+ - C0yyyyyyy
+```
+
+
## Logging Sinks
```hcl
module "gcs" {
- source = "./modules/gcs"
+ source = "./fabric/modules/gcs"
project_id = var.project_id
name = "gcs_sink"
force_destroy = true
}
module "dataset" {
- source = "./modules/bigquery-dataset"
+ source = "./fabric/modules/bigquery-dataset"
project_id = var.project_id
id = "bq_sink"
}
module "pubsub" {
- source = "./modules/pubsub"
+ source = "./fabric/modules/pubsub"
project_id = var.project_id
name = "pubsub_sink"
}
module "bucket" {
- source = "./modules/logging-bucket"
+ source = "./fabric/modules/logging-bucket"
parent_type = "project"
parent = "my-project"
id = "bucket"
}
module "project-host" {
- source = "./modules/project"
+ source = "./fabric/modules/project"
name = "my-project"
billing_account = "123456-123456-123456"
parent = "folders/1234567890"
logging_sinks = {
warnings = {
- type = "storage"
- destination = module.gcs.name
- filter = "severity=WARNING"
- iam = false
- unique_writer = false
- exclusions = {}
+ destination = module.gcs.id
+ filter = "severity=WARNING"
+ type = "storage"
}
info = {
- type = "bigquery"
- destination = module.dataset.id
- filter = "severity=INFO"
- iam = false
- unique_writer = false
- exclusions = {}
+ destination = module.dataset.id
+ filter = "severity=INFO"
+ type = "bigquery"
}
notice = {
- type = "pubsub"
- destination = module.pubsub.id
- filter = "severity=NOTICE"
- iam = true
- unique_writer = false
- exclusions = {}
+ destination = module.pubsub.id
+ filter = "severity=NOTICE"
+ type = "pubsub"
}
debug = {
- type = "logging"
- destination = module.bucket.id
- filter = "severity=DEBUG"
- iam = true
- unique_writer = false
+ destination = module.bucket.id
+ filter = "severity=DEBUG"
exclusions = {
no-compute = "logName:compute"
}
+ type = "logging"
}
}
logging_exclusions = {
no-gce-instances = "resource.type=gce_instance"
}
}
-# tftest modules=5 resources=12
+# tftest modules=5 resources=14 inventory=logging.yaml
```
## Cloud KMS encryption keys
+The module offers a simple, centralized way to assign `roles/cloudkms.cryptoKeyEncrypterDecrypter` to service identities.
+
```hcl
module "project" {
- source = "./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"
@@ -213,12 +382,12 @@ Refer to the [Creating and managing tags](https://cloud.google.com/resource-mana
```hcl
module "org" {
- source = "./modules/organization"
+ source = "./fabric/modules/organization"
organization_id = var.organization_id
tags = {
environment = {
- description = "Environment specification."
- iam = null
+ description = "Environment specification."
+ iam = null
values = {
dev = null
prod = null
@@ -228,7 +397,7 @@ module "org" {
}
module "project" {
- source = "./modules/project"
+ source = "./fabric/modules/project"
name = "test-project"
tag_bindings = {
env-prod = module.org.tag_values["environment/prod"].id
@@ -238,6 +407,27 @@ module "project" {
# tftest modules=2 resources=6
```
+## Outputs
+
+Most of this module's outputs depend on its resources, to allow Terraform to compute all dependencies required for the project to be correctly configured. This allows you to reference outputs like `project_id` in other modules or resources without having to worry about setting `depends_on` blocks manually.
+
+One non-obvious output is `service_accounts`, which offers a simple way to discover service identities and default service accounts, and guarantees that service identities that require an API call to trigger creation (like GCS or BigQuery) exist before use.
+
+```hcl
+module "project" {
+ source = "./fabric/modules/project"
+ name = "project-example"
+ services = [
+ "compute.googleapis.com"
+ ]
+}
+
+output "compute_robot" {
+ value = module.project.service_accounts.robots.compute
+}
+# tftest modules=1 resources=2 inventory:outputs.yaml
+```
+
@@ -248,9 +438,9 @@ module "project" {
| [iam.tf](./iam.tf) | Generic and OSLogin-specific IAM bindings and roles. | google_project_iam_binding
· google_project_iam_custom_role
· google_project_iam_member
|
| [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member
· google_logging_project_exclusion
· google_logging_project_sink
· google_project_iam_member
· google_pubsub_topic_iam_member
· google_storage_bucket_iam_member
|
| [main.tf](./main.tf) | Module-level locals and resources. | google_compute_project_metadata_item
· google_essential_contacts_contact
· google_monitoring_monitored_project
· google_project
· google_project_service
· google_resource_manager_lien
|
-| [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | google_project_organization_policy
|
+| [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | google_org_policy_policy
|
| [outputs.tf](./outputs.tf) | Module outputs. | |
-| [service-accounts.tf](./service-accounts.tf) | Service identities and supporting resources. | google_kms_crypto_key_iam_member
· google_project_iam_member
· google_project_service_identity
|
+| [service-accounts.tf](./service-accounts.tf) | Service identities and supporting resources. | google_kms_crypto_key_iam_member
· google_project_default_service_accounts
· google_project_iam_member
· google_project_service_identity
|
| [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | google_compute_shared_vpc_host_project
· google_compute_shared_vpc_service_project
· google_project_iam_member
|
| [tags.tf](./tags.tf) | None | google_tags_tag_binding
|
| [variables.tf](./variables.tf) | Module variables. | |
@@ -261,38 +451,39 @@ module "project" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [name](variables.tf#L125) | Project name and id suffix. | string
| ✓ | |
+| [name](variables.tf#L140) | Project name and id suffix. | string
| ✓ | |
| [auto_create_network](variables.tf#L17) | Whether to create the default network for the project. | bool
| | false
|
| [billing_account](variables.tf#L23) | Billing account id. | string
| | null
|
| [contacts](variables.tf#L29) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string))
| | {}
|
| [custom_roles](variables.tf#L36) | Map of role name => list of permissions to create in this project. | map(list(string))
| | {}
|
-| [descriptive_name](variables.tf#L43) | Name of the project name. Used for project name instead of `name` variable. | string
| | null
|
-| [group_iam](variables.tf#L49) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string))
| | {}
|
-| [iam](variables.tf#L56) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
-| [iam_additive](variables.tf#L63) | IAM additive bindings in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
-| [iam_additive_members](variables.tf#L70) | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | map(list(string))
| | {}
|
-| [labels](variables.tf#L76) | Resource labels. | map(string)
| | {}
|
-| [lien_reason](variables.tf#L83) | If non-empty, creates a project lien with this description. | string
| | ""
|
-| [logging_exclusions](variables.tf#L89) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string)
| | {}
|
-| [logging_sinks](variables.tf#L96) | Logging sinks to create for this project. | map(object({…}))
| | {}
|
-| [metric_scopes](variables.tf#L118) | List of projects that will act as metric scopes for this project. | list(string)
| | []
|
-| [oslogin](variables.tf#L130) | Enable OS Login. | bool
| | false
|
-| [oslogin_admins](variables.tf#L136) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string)
| | []
|
-| [oslogin_users](variables.tf#L144) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string)
| | []
|
-| [parent](variables.tf#L151) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string
| | null
|
-| [policy_boolean](variables.tf#L161) | Map of boolean org policies and enforcement value, set value to null for policy restore. | map(bool)
| | {}
|
-| [policy_list](variables.tf#L168) | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | map(object({…}))
| | {}
|
-| [prefix](variables.tf#L180) | Prefix used to generate project id and name. | string
| | null
|
-| [project_create](variables.tf#L186) | Create project. When set to false, uses a data source to reference existing project. | bool
| | true
|
-| [service_config](variables.tf#L192) | Configure service API activation. | object({…})
| | {…}
|
-| [service_encryption_key_ids](variables.tf#L204) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string))
| | {}
|
-| [service_perimeter_bridges](variables.tf#L211) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string)
| | null
|
-| [service_perimeter_standard](variables.tf#L218) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string
| | null
|
-| [services](variables.tf#L224) | Service APIs to enable. | list(string)
| | []
|
-| [shared_vpc_host_config](variables.tf#L230) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…})
| | null
|
-| [shared_vpc_service_config](variables.tf#L239) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…})
| | null
|
-| [skip_delete](variables.tf#L249) | Allows the underlying resources to be destroyed without destroying the project itself. | bool
| | false
|
-| [tag_bindings](variables.tf#L255) | Tag bindings for this project, in key => tag value id format. | map(string)
| | null
|
+| [default_service_account](variables.tf#L43) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string
| | "keep"
|
+| [descriptive_name](variables.tf#L49) | Name of the project name. Used for project name instead of `name` variable. | string
| | null
|
+| [group_iam](variables.tf#L55) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string))
| | {}
|
+| [iam](variables.tf#L62) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
+| [iam_additive](variables.tf#L69) | IAM additive bindings in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
+| [iam_additive_members](variables.tf#L76) | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | map(list(string))
| | {}
|
+| [labels](variables.tf#L82) | Resource labels. | map(string)
| | {}
|
+| [lien_reason](variables.tf#L89) | If non-empty, creates a project lien with this description. | string
| | ""
|
+| [logging_exclusions](variables.tf#L95) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string)
| | {}
|
+| [logging_sinks](variables.tf#L102) | Logging sinks to create for this project. | map(object({…}))
| | {}
|
+| [metric_scopes](variables.tf#L133) | List of projects that will act as metric scopes for this project. | list(string)
| | []
|
+| [org_policies](variables.tf#L145) | Organization policies applied to this project keyed by policy name. | map(object({…}))
| | {}
|
+| [org_policies_data_path](variables.tf#L185) | Path containing org policies in YAML format. | string
| | null
|
+| [oslogin](variables.tf#L191) | Enable OS Login. | bool
| | false
|
+| [oslogin_admins](variables.tf#L197) | List of IAM-style identities that will be granted roles necessary for OS Login administrators. | list(string)
| | []
|
+| [oslogin_users](variables.tf#L205) | List of IAM-style identities that will be granted roles necessary for OS Login users. | list(string)
| | []
|
+| [parent](variables.tf#L212) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string
| | null
|
+| [prefix](variables.tf#L222) | Optional prefix used to generate project id and name. | string
| | null
|
+| [project_create](variables.tf#L232) | Create project. When set to false, uses a data source to reference existing project. | bool
| | true
|
+| [service_config](variables.tf#L238) | Configure service API activation. | object({…})
| | {…}
|
+| [service_encryption_key_ids](variables.tf#L250) | Cloud KMS encryption key in {SERVICE => [KEY_URL]} format. | map(list(string))
| | {}
|
+| [service_perimeter_bridges](variables.tf#L257) | Name of VPC-SC Bridge perimeters to add project into. See comment in the variables file for format. | list(string)
| | null
|
+| [service_perimeter_standard](variables.tf#L264) | Name of VPC-SC Standard perimeter to add project into. See comment in the variables file for format. | string
| | null
|
+| [services](variables.tf#L270) | Service APIs to enable. | list(string)
| | []
|
+| [shared_vpc_host_config](variables.tf#L276) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…})
| | null
|
+| [shared_vpc_service_config](variables.tf#L285) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…})
| | null
|
+| [skip_delete](variables.tf#L295) | Allows the underlying resources to be destroyed without destroying the project itself. | bool
| | false
|
+| [tag_bindings](variables.tf#L301) | Tag bindings for this project, in key => tag value id format. | map(string)
| | null
|
## Outputs
@@ -300,9 +491,9 @@ module "project" {
|---|---|:---:|
| [custom_roles](outputs.tf#L17) | Ids of the created custom roles. | |
| [name](outputs.tf#L25) | Project name. | |
-| [number](outputs.tf#L38) | Project number. | |
-| [project_id](outputs.tf#L51) | Project id. | |
-| [service_accounts](outputs.tf#L68) | Product robot service accounts in project. | |
-| [sink_writer_identities](outputs.tf#L84) | Writer identities created for each sink. | |
+| [number](outputs.tf#L37) | Project number. | |
+| [project_id](outputs.tf#L54) | Project id. | |
+| [service_accounts](outputs.tf#L73) | Product robot service accounts in project. | |
+| [sink_writer_identities](outputs.tf#L89) | Writer identities created for each sink. | |
diff --git a/modules/project/logging.tf b/modules/project/logging.tf
index 04d7abf16c..1db60dca4e 100644
--- a/modules/project/logging.tf
+++ b/modules/project/logging.tf
@@ -27,13 +27,21 @@ locals {
}
resource "google_logging_project_sink" "sink" {
- for_each = var.logging_sinks
- name = each.key
- #description = "${each.key} (Terraform-managed)."
+ for_each = var.logging_sinks
+ name = each.key
+ description = coalesce(each.value.description, "${each.key} (Terraform-managed).")
project = local.project.project_id
destination = "${each.value.type}.googleapis.com/${each.value.destination}"
filter = each.value.filter
unique_writer_identity = each.value.unique_writer
+ disabled = each.value.disabled
+
+ dynamic "bigquery_options" {
+ for_each = each.value.type == "biquery" && each.value.bq_partitioned_table != null ? [""] : []
+ content {
+ use_partitioned_tables = each.value.bq_partitioned_table
+ }
+ }
dynamic "exclusions" {
for_each = each.value.exclusions
@@ -78,8 +86,12 @@ resource "google_project_iam_member" "bucket-sinks-binding" {
project = split("/", each.value.destination)[1]
role = "roles/logging.bucketWriter"
member = google_logging_project_sink.sink[each.key].writer_identity
- # TODO(jccb): use a condition to limit writer-identity only to this
- # bucket
+
+ condition {
+ title = "${each.key} bucket writer"
+ description = "Grants bucketWriter to ${google_logging_project_sink.sink[each.key].writer_identity} used by log sink ${each.key} on ${local.project.project_id}"
+ expression = "resource.name.endsWith('${each.value.destination}')"
+ }
}
resource "google_logging_project_exclusion" "logging-exclusion" {
diff --git a/modules/project/organization-policies.tf b/modules/project/organization-policies.tf
index 6870754897..4ff5bb9922 100644
--- a/modules/project/organization-policies.tf
+++ b/modules/project/organization-policies.tf
@@ -16,75 +16,127 @@
# tfdoc:file:description Project-level organization policies.
-resource "google_project_organization_policy" "boolean" {
- for_each = var.policy_boolean
- project = local.project.project_id
- constraint = each.key
+locals {
+ _factory_data_raw = merge([
+ for f in try(fileset(var.org_policies_data_path, "*.yaml"), []) :
+ yamldecode(file("${var.org_policies_data_path}/${f}"))
+ ]...)
- dynamic "boolean_policy" {
- for_each = each.value == null ? [] : [each.value]
- iterator = policy
- content {
- enforced = policy.value
+ # simulate applying defaults to data coming from yaml files
+ _factory_data = {
+ for k, v in local._factory_data_raw :
+ k => {
+ inherit_from_parent = try(v.inherit_from_parent, null)
+ reset = try(v.reset, null)
+ allow = can(v.allow) ? {
+ all = try(v.allow.all, null)
+ values = try(v.allow.values, null)
+ } : null
+ deny = can(v.deny) ? {
+ all = try(v.deny.all, null)
+ values = try(v.deny.values, null)
+ } : null
+ enforce = try(v.enforce, true)
+
+ rules = [
+ for r in try(v.rules, []) : {
+ allow = can(r.allow) ? {
+ all = try(r.allow.all, null)
+ values = try(r.allow.values, null)
+ } : null
+ deny = can(r.deny) ? {
+ all = try(r.deny.all, null)
+ values = try(r.deny.values, null)
+ } : null
+ enforce = try(r.enforce, true)
+ condition = {
+ description = try(r.condition.description, null)
+ expression = try(r.condition.expression, null)
+ location = try(r.condition.location, null)
+ title = try(r.condition.title, null)
+ }
+ }
+ ]
}
}
- dynamic "restore_policy" {
- for_each = each.value == null ? [""] : []
- content {
- default = true
- }
+ _org_policies = merge(local._factory_data, var.org_policies)
+
+ org_policies = {
+ for k, v in local._org_policies :
+ k => merge(v, {
+ name = "projects/${local.project.project_id}/policies/${k}"
+ parent = "projects/${local.project.project_id}"
+
+ is_boolean_policy = v.allow == null && v.deny == null
+ has_values = (
+ length(coalesce(try(v.allow.values, []), [])) > 0 ||
+ length(coalesce(try(v.deny.values, []), [])) > 0
+ )
+ rules = [
+ for r in v.rules :
+ merge(r, {
+ has_values = (
+ length(coalesce(try(r.allow.values, []), [])) > 0 ||
+ length(coalesce(try(r.deny.values, []), [])) > 0
+ )
+ })
+ ]
+ })
}
}
-resource "google_project_organization_policy" "list" {
- for_each = var.policy_list
- project = local.project.project_id
- constraint = each.key
+resource "google_org_policy_policy" "default" {
+ for_each = local.org_policies
+ name = each.value.name
+ parent = each.value.parent
- dynamic "list_policy" {
- for_each = each.value.status == null ? [] : [each.value]
- iterator = policy
- content {
- inherit_from_parent = policy.value.inherit_from_parent
- suggested_value = policy.value.suggested_value
- dynamic "allow" {
- for_each = policy.value.status ? [""] : []
- content {
- values = (
- try(length(policy.value.values) > 0, false)
- ? policy.value.values
- : null
- )
- all = (
- try(length(policy.value.values) > 0, false)
- ? null
- : true
- )
+ spec {
+ inherit_from_parent = each.value.inherit_from_parent
+ reset = each.value.reset
+
+ dynamic "rules" {
+ for_each = each.value.rules
+ iterator = rule
+ content {
+ allow_all = try(rule.value.allow.all, false) == true ? "TRUE" : null
+ deny_all = try(rule.value.deny.all, false) == true ? "TRUE" : null
+ enforce = (
+ each.value.is_boolean_policy && rule.value.enforce != null
+ ? upper(tostring(rule.value.enforce))
+ : null
+ )
+ condition {
+ description = rule.value.condition.description
+ expression = rule.value.condition.expression
+ location = rule.value.condition.location
+ title = rule.value.condition.title
}
- }
- dynamic "deny" {
- for_each = policy.value.status ? [] : [""]
- content {
- values = (
- try(length(policy.value.values) > 0, false)
- ? policy.value.values
- : null
- )
- all = (
- try(length(policy.value.values) > 0, false)
- ? null
- : true
- )
+ dynamic "values" {
+ for_each = rule.value.has_values ? [1] : []
+ content {
+ allowed_values = try(rule.value.allow.values, null)
+ denied_values = try(rule.value.deny.values, null)
+ }
}
}
}
- }
- dynamic "restore_policy" {
- for_each = each.value.status == null ? [true] : []
- content {
- default = true
+ 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/outputs.tf b/modules/project/outputs.tf
index 4644e1b72e..cb940d010d 100644
--- a/modules/project/outputs.tf
+++ b/modules/project/outputs.tf
@@ -26,8 +26,7 @@ output "name" {
description = "Project name."
value = local.project.name
depends_on = [
- google_project_organization_policy.boolean,
- google_project_organization_policy.list,
+ google_org_policy_policy.default,
google_project_service.project_services,
google_compute_shared_vpc_service_project.service_projects,
google_project_iam_member.shared_vpc_host_robots,
@@ -39,12 +38,16 @@ output "number" {
description = "Project number."
value = local.project.number
depends_on = [
- google_project_organization_policy.boolean,
- google_project_organization_policy.list,
+ google_org_policy_policy.default,
google_project_service.project_services,
+ google_compute_shared_vpc_host_project.shared_vpc_host,
+ google_compute_shared_vpc_service_project.shared_vpc_service,
google_compute_shared_vpc_service_project.service_projects,
google_project_iam_member.shared_vpc_host_robots,
- google_kms_crypto_key_iam_member.service_identity_cmek
+ google_kms_crypto_key_iam_member.service_identity_cmek,
+ google_project_service_identity.jit_si,
+ google_project_service_identity.servicenetworking,
+ google_project_iam_member.servicenetworking
]
}
@@ -54,12 +57,14 @@ output "project_id" {
depends_on = [
google_project.project,
data.google_project.project,
- google_project_organization_policy.boolean,
- google_project_organization_policy.list,
+ google_org_policy_policy.default,
google_project_service.project_services,
+ google_compute_shared_vpc_host_project.shared_vpc_host,
+ google_compute_shared_vpc_service_project.shared_vpc_service,
google_compute_shared_vpc_service_project.service_projects,
google_project_iam_member.shared_vpc_host_robots,
google_kms_crypto_key_iam_member.service_identity_cmek,
+ google_project_service_identity.jit_si,
google_project_service_identity.servicenetworking,
google_project_iam_member.servicenetworking
]
diff --git a/modules/project/service-accounts.tf b/modules/project/service-accounts.tf
index eae98e2374..e1f6cb71a5 100644
--- a/modules/project/service-accounts.tf
+++ b/modules/project/service-accounts.tf
@@ -25,6 +25,7 @@ locals {
"dataflow" : ["dataflow", "compute"]
}
_service_accounts_robot_services = {
+ apigee = "service-%s@gcp-sa-apigee"
artifactregistry = "service-%s@gcp-sa-artifactregistry"
bq = "bq-%s@bigquery-encryption"
cloudasset = "service-%s@gcp-sa-cloudasset"
@@ -37,12 +38,18 @@ locals {
containerregistry = "service-%s@containerregistry"
dataflow = "service-%s@dataflow-service-producer-prod"
dataproc = "service-%s@dataproc-accounts"
+ fleet = "service-%s@gcp-sa-gkehub"
gae-flex = "service-%s@gae-api-prod"
# TODO: deprecate gcf
- gcf = "service-%s@gcf-admin-robot"
- pubsub = "service-%s@gcp-sa-pubsub"
- secretmanager = "service-%s@gcp-sa-secretmanager"
- storage = "service-%s@gs-project-accounts"
+ gcf = "service-%s@gcf-admin-robot"
+ # TODO: jit?
+ gke-mcs = "service-%s@gcp-sa-mcsd"
+ monitoring-notifications = "service-%s@gcp-sa-monitoring-notification"
+ pubsub = "service-%s@gcp-sa-pubsub"
+ secretmanager = "service-%s@gcp-sa-secretmanager"
+ sql = "service-%s@gcp-sa-cloud-sql"
+ sqladmin = "service-%s@gcp-sa-cloud-sql"
+ storage = "service-%s@gs-project-accounts"
}
service_accounts_default = {
compute = "${local.project.number}-compute@developer.gserviceaccount.com"
@@ -51,14 +58,23 @@ locals {
service_account_cloud_services = (
"${local.project.number}@cloudservices.gserviceaccount.com"
)
- service_accounts_robots = {
- for k, v in local._service_accounts_robot_services :
- k => "${format(v, local.project.number)}.iam.gserviceaccount.com"
- }
+ service_accounts_robots = merge(
+ {
+ for k, v in local._service_accounts_robot_services :
+ k => "${format(v, local.project.number)}.iam.gserviceaccount.com"
+ },
+ {
+ gke-mcs-importer = "${local.project.project_id}.svc.id.goog[gke-mcs/gke-mcs-importer]"
+ }
+ )
service_accounts_jit_services = [
- "secretmanager.googleapis.com",
+ "apigee.googleapis.com",
+ "artifactregistry.googleapis.com",
+ "cloudasset.googleapis.com",
+ "gkehub.googleapis.com",
"pubsub.googleapis.com",
- "cloudasset.googleapis.com"
+ "secretmanager.googleapis.com",
+ "sqladmin.googleapis.com"
]
service_accounts_cmek_service_keys = distinct(flatten([
for s in keys(var.service_encryption_key_ids) : [
@@ -126,3 +142,11 @@ resource "google_kms_crypto_key_iam_member" "service_identity_cmek" {
data.google_storage_project_service_account.gcs_sa,
]
}
+
+resource "google_project_default_service_accounts" "default_service_accounts" {
+ count = upper(var.default_service_account) == "KEEP" ? 0 : 1
+ action = upper(var.default_service_account)
+ project = local.project.project_id
+ restore_policy = "REVERT_AND_IGNORE_FAILURE"
+ depends_on = [google_project_service.project_services]
+}
diff --git a/modules/project/shared-vpc.tf b/modules/project/shared-vpc.tf
index 865c7a5078..3894e5d780 100644
--- a/modules/project/shared-vpc.tf
+++ b/modules/project/shared-vpc.tf
@@ -18,14 +18,14 @@
locals {
# compute the host project IAM bindings for this project's service identities
- _svpc_service_identity_iam = coalesce(
- local.svpc_service_config.service_identity_iam, {}
- )
_svpc_service_iam = flatten([
for role, services in local._svpc_service_identity_iam : [
for service in services : { role = role, service = service }
]
])
+ _svpc_service_identity_iam = coalesce(
+ local.svpc_service_config.service_identity_iam, {}
+ )
svpc_host_config = {
enabled = coalesce(
try(var.shared_vpc_host_config.enabled, null), false
diff --git a/modules/project/variables.tf b/modules/project/variables.tf
index 578f9d2304..3769a1fb4e 100644
--- a/modules/project/variables.tf
+++ b/modules/project/variables.tf
@@ -40,6 +40,12 @@ variable "custom_roles" {
nullable = false
}
+variable "default_service_account" {
+ description = "Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`."
+ default = "keep"
+ type = string
+}
+
variable "descriptive_name" {
description = "Name of the project name. Used for project name instead of `name` variable."
type = string
@@ -96,23 +102,32 @@ variable "logging_exclusions" {
variable "logging_sinks" {
description = "Logging sinks to create for this project."
type = map(object({
- destination = string
- type = string
- filter = string
- iam = bool
- unique_writer = bool
- # TODO exclusions also support description and disabled
- exclusions = map(string)
+ bq_partitioned_table = optional(bool)
+ description = optional(string)
+ destination = string
+ disabled = optional(bool, false)
+ exclusions = optional(map(string), {})
+ filter = string
+ iam = optional(bool, true)
+ type = string
+ unique_writer = optional(bool)
}))
+ default = {}
+ nullable = false
validation {
condition = alltrue([
- for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) :
+ for k, v in var.logging_sinks :
contains(["bigquery", "logging", "pubsub", "storage"], v.type)
])
error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'."
}
- default = {}
- nullable = false
+ validation {
+ condition = alltrue([
+ for k, v in var.logging_sinks :
+ v.bq_partitioned_table != true || v.type == "bigquery"
+ ])
+ error_message = "Can only set bq_partitioned_table when type is `bigquery`."
+ }
}
variable "metric_scopes" {
@@ -127,6 +142,52 @@ variable "name" {
type = string
}
+variable "org_policies" {
+ description = "Organization policies applied to this project keyed by policy name."
+ type = map(object({
+ inherit_from_parent = optional(bool) # for list policies only.
+ reset = optional(bool)
+
+ # default (unconditional) values
+ allow = optional(object({
+ all = optional(bool)
+ values = optional(list(string))
+ }))
+ deny = optional(object({
+ all = optional(bool)
+ values = optional(list(string))
+ }))
+ enforce = optional(bool, true) # for boolean policies only.
+
+ # conditional values
+ rules = optional(list(object({
+ allow = optional(object({
+ all = optional(bool)
+ values = optional(list(string))
+ }))
+ deny = optional(object({
+ all = optional(bool)
+ values = optional(list(string))
+ }))
+ enforce = optional(bool, true) # for boolean policies only.
+ condition = object({
+ description = optional(string)
+ expression = optional(string)
+ location = optional(string)
+ title = optional(string)
+ })
+ })), [])
+ }))
+ default = {}
+ nullable = false
+}
+
+variable "org_policies_data_path" {
+ description = "Path containing org policies in YAML format."
+ type = string
+ default = null
+}
+
variable "oslogin" {
description = "Enable OS Login."
type = bool
@@ -158,29 +219,14 @@ variable "parent" {
}
}
-variable "policy_boolean" {
- description = "Map of boolean org policies and enforcement value, set value to null for policy restore."
- type = map(bool)
- default = {}
- nullable = false
-}
-
-variable "policy_list" {
- description = "Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny."
- type = map(object({
- inherit_from_parent = bool
- suggested_value = string
- status = bool
- values = list(string)
- }))
- default = {}
- nullable = false
-}
-
variable "prefix" {
- description = "Prefix used to generate project id and name."
+ description = "Optional prefix used to generate project id and name."
type = string
default = null
+ validation {
+ condition = var.prefix != ""
+ error_message = "Prefix cannot be empty, please use null instead."
+ }
}
variable "project_create" {
@@ -196,8 +242,8 @@ variable "service_config" {
disable_dependent_services = bool
})
default = {
- disable_on_destroy = true
- disable_dependent_services = true
+ disable_on_destroy = false
+ disable_dependent_services = false
}
}
@@ -231,7 +277,7 @@ variable "shared_vpc_host_config" {
description = "Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project)."
type = object({
enabled = bool
- service_projects = list(string)
+ service_projects = optional(list(string), [])
})
default = null
}
@@ -241,7 +287,7 @@ variable "shared_vpc_service_config" {
# the list of valid service identities is in service-accounts.tf
type = object({
host_project = string
- service_identity_iam = map(list(string))
+ service_identity_iam = optional(map(list(string)))
})
default = null
}
diff --git a/modules/project/versions.tf b/modules/project/versions.tf
index e72a78007a..90b632f6d4 100644
--- a/modules/project/versions.tf
+++ b/modules/project/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.1.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/projects-data-source/README.md b/modules/projects-data-source/README.md
index c755d85298..e48ac9770b 100644
--- a/modules/projects-data-source/README.md
+++ b/modules/projects-data-source/README.md
@@ -10,7 +10,7 @@ A good usage pattern would be when we want all the projects under a specific fol
```hcl
module "my-org" {
- source = "./modules/projects-data-source"
+ source = "./fabric/modules/projects-data-source"
parent = "organizations/123456789"
}
@@ -22,16 +22,16 @@ output "folders" {
value = module.my-org.folders
}
-# tftest skip
+# tftest skip (uses data sources)
```
### My dev projects based on parent and label
```hcl
module "my-dev" {
- source = "./modules/projects-data-source"
+ source = "./fabric/modules/projects-data-source"
parent = "folders/123456789"
- filter = "labels.env:DEV lifecycleState:ACTIVE"
+ filter = "labels.env:DEV lifecycleState:ACTIVE"
}
output "dev-projects" {
@@ -42,7 +42,7 @@ output "dev-folders" {
value = module.my-dev.folders
}
-# tftest skip
+# tftest skip (uses data sources)
```
@@ -50,15 +50,15 @@ output "dev-folders" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [parent](variables.tf#L17) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string
| ✓ | |
-| [filter](variables.tf#L26) | A string filter as defined in the [REST API](https://cloud.google.com/resource-manager/reference/rest/v1/projects/list#query-parameters). | string
| | "lifecycleState:ACTIVE"
|
+| [parent](variables.tf#L23) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string
| ✓ | |
+| [filter](variables.tf#L17) | A string filter as defined in the [REST API](https://cloud.google.com/resource-manager/reference/rest/v1/projects/list#query-parameters). | string
| | "lifecycleState:ACTIVE"
|
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [folders](outputs.tf#L17) | Map of folders attributes keyed by folder id. | |
-| [project_numbers](outputs.tf#L27) | List of project numbers. | |
-| [projects](outputs.tf#L22) | Map of projects attributes keyed by projects id. | |
+| [project_numbers](outputs.tf#L22) | List of project numbers. | |
+| [projects](outputs.tf#L27) | Map of projects attributes keyed by projects id. | |
diff --git a/modules/projects-data-source/outputs.tf b/modules/projects-data-source/outputs.tf
index d2b0a133f6..b7e38ae2cf 100644
--- a/modules/projects-data-source/outputs.tf
+++ b/modules/projects-data-source/outputs.tf
@@ -19,12 +19,12 @@ output "folders" {
value = local.all_folders
}
-output "projects" {
- description = "Map of projects attributes keyed by projects id."
- value = local.projects
-}
-
output "project_numbers" {
description = "List of project numbers."
value = [for _, v in local.projects : v.number]
}
+
+output "projects" {
+ description = "Map of projects attributes keyed by projects id."
+ value = local.projects
+}
diff --git a/modules/projects-data-source/variables.tf b/modules/projects-data-source/variables.tf
index 0895ab2dd6..a7f393d335 100644
--- a/modules/projects-data-source/variables.tf
+++ b/modules/projects-data-source/variables.tf
@@ -14,6 +14,12 @@
* limitations under the License.
*/
+variable "filter" {
+ description = "A string filter as defined in the [REST API](https://cloud.google.com/resource-manager/reference/rest/v1/projects/list#query-parameters)."
+ type = string
+ default = "lifecycleState:ACTIVE"
+}
+
variable "parent" {
description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format."
type = string
@@ -22,9 +28,3 @@ variable "parent" {
error_message = "Parent must be of the form folders/folder_id or organizations/organization_id."
}
}
-
-variable "filter" {
- description = "A string filter as defined in the [REST API](https://cloud.google.com/resource-manager/reference/rest/v1/projects/list#query-parameters)."
- type = string
- default = "lifecycleState:ACTIVE"
-}
\ No newline at end of file
diff --git a/modules/projects-data-source/versions.tf b/modules/projects-data-source/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/projects-data-source/versions.tf
+++ b/modules/projects-data-source/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/pubsub/README.md b/modules/pubsub/README.md
index 411511abbe..81e4336599 100644
--- a/modules/pubsub/README.md
+++ b/modules/pubsub/README.md
@@ -1,7 +1,6 @@
# Google Cloud Pub/Sub Module
-This module allows managing a single Pub/Sub topic, including multiple subscriptions and IAM bindings at the topic and subscriptions levels.
-
+This module allows managing a single Pub/Sub topic, including multiple subscriptions and IAM bindings at the topic and subscriptions levels, as well as schemas.
## Examples
@@ -9,7 +8,7 @@ This module allows managing a single Pub/Sub topic, including multiple subscript
```hcl
module "pubsub" {
- source = "./modules/pubsub"
+ source = "./fabric/modules/pubsub"
project_id = "my-project"
name = "my-topic"
iam = {
@@ -20,13 +19,45 @@ module "pubsub" {
# tftest modules=1 resources=3
```
+### Topic with schema
+
+```hcl
+module "topic_with_schema" {
+ source = "./fabric/modules/pubsub"
+ project_id = "my-project"
+ name = "my-topic"
+ schema = {
+ msg_encoding = "JSON"
+ schema_type = "AVRO"
+ definition = jsonencode({
+ "type" = "record",
+ "name" = "Avro",
+ "fields" = [{
+ "name" = "StringField",
+ "type" = "string"
+ },
+ {
+ "name" = "FloatField",
+ "type" = "float"
+ },
+ {
+ "name" = "BooleanField",
+ "type" = "boolean"
+ },
+ ]
+ })
+ }
+}
+# tftest modules=1 resources=2
+```
+
### Subscriptions
Subscriptions are defined with the `subscriptions` variable, allowing optional configuration of per-subscription defaults. Push subscriptions need extra configuration, shown in the following example.
```hcl
module "pubsub" {
- source = "./modules/pubsub"
+ source = "./fabric/modules/pubsub"
project_id = "my-project"
name = "my-topic"
subscriptions = {
@@ -38,6 +69,7 @@ module "pubsub" {
message_retention_duration = null
retain_acked_messages = true
expiration_policy_ttl = null
+ filter = null
}
}
}
@@ -51,7 +83,7 @@ Push subscriptions need extra configuration in the `push_configs` variable.
```hcl
module "pubsub" {
- source = "./modules/pubsub"
+ source = "./fabric/modules/pubsub"
project_id = "my-project"
name = "my-topic"
subscriptions = {
@@ -68,11 +100,35 @@ module "pubsub" {
# tftest modules=1 resources=2
```
+### BigQuery subscriptions
+
+BigQuery subscriptions need extra configuration in the `bigquery_subscription_configs` variable.
+
+```hcl
+module "pubsub" {
+ source = "./fabric/modules/pubsub"
+ project_id = "my-project"
+ name = "my-topic"
+ subscriptions = {
+ test-bigquery = null
+ }
+ bigquery_subscription_configs = {
+ test-bigquery = {
+ table = "my_project_id:my_dataset.my_table"
+ use_topic_schema = true
+ write_metadata = false
+ drop_unknown_fields = true
+ }
+ }
+}
+# tftest modules=1 resources=2
+```
+
### Subscriptions with IAM
```hcl
module "pubsub" {
- source = "./modules/pubsub"
+ source = "./fabric/modules/pubsub"
project_id = "my-project"
name = "my-topic"
subscriptions = {
@@ -93,25 +149,30 @@ module "pubsub" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [name](variables.tf#L60) | PubSub topic name. | string
| ✓ | |
-| [project_id](variables.tf#L65) | Project used for resources. | string
| ✓ | |
-| [dead_letter_configs](variables.tf#L17) | Per-subscription dead letter policy configuration. | map(object({…}))
| | {}
|
-| [defaults](variables.tf#L26) | Subscription defaults for options. | object({…})
| | {…}
|
-| [iam](variables.tf#L42) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
-| [kms_key](variables.tf#L48) | KMS customer managed encryption key. | string
| | null
|
-| [labels](variables.tf#L54) | Labels. | map(string)
| | {}
|
-| [push_configs](variables.tf#L70) | Push subscription configurations. | map(object({…}))
| | {}
|
-| [regions](variables.tf#L83) | List of regions used to set persistence policy. | list(string)
| | []
|
-| [subscription_iam](variables.tf#L89) | IAM bindings for subscriptions in {SUBSCRIPTION => {ROLE => [MEMBERS]}} format. | map(map(list(string)))
| | {}
|
-| [subscriptions](variables.tf#L95) | Topic subscriptions. Also define push configs for push subscriptions. If options is set to null subscription defaults will be used. Labels default to topic labels if set to null. | map(object({…}))
| | {}
|
+| [name](variables.tf#L79) | PubSub topic name. | string
| ✓ | |
+| [project_id](variables.tf#L84) | Project used for resources. | string
| ✓ | |
+| [bigquery_subscription_configs](variables.tf#L17) | Configuration parameters for BigQuery subscriptions. | map(object({…}))
| | {}
|
+| [dead_letter_configs](variables.tf#L28) | Per-subscription dead letter policy configuration. | map(object({…}))
| | {}
|
+| [defaults](variables.tf#L37) | Subscription defaults for options. | object({…})
| | {…}
|
+| [iam](variables.tf#L55) | IAM bindings for topic in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
+| [kms_key](variables.tf#L61) | KMS customer managed encryption key. | string
| | null
|
+| [labels](variables.tf#L67) | Labels. | map(string)
| | {}
|
+| [message_retention_duration](variables.tf#L73) | Minimum duration to retain a message after it is published to the topic. | string
| | null
|
+| [push_configs](variables.tf#L89) | Push subscription configurations. | map(object({…}))
| | {}
|
+| [regions](variables.tf#L102) | List of regions used to set persistence policy. | list(string)
| | []
|
+| [schema](variables.tf#L108) | Topic schema. If set, all messages in this topic should follow this schema. | object({…})
| | null
|
+| [subscription_iam](variables.tf#L118) | IAM bindings for subscriptions in {SUBSCRIPTION => {ROLE => [MEMBERS]}} format. | map(map(list(string)))
| | {}
|
+| [subscriptions](variables.tf#L124) | Topic subscriptions. Also define push configs for push subscriptions. If options is set to null subscription defaults will be used. Labels default to topic labels if set to null. | map(object({…}))
| | {}
|
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [id](outputs.tf#L17) | Topic id. | |
-| [subscription_id](outputs.tf#L25) | Subscription ids. | |
-| [subscriptions](outputs.tf#L35) | Subscription resources. | |
-| [topic](outputs.tf#L43) | Topic resource. | |
+| [schema](outputs.tf#L25) | Schema resource. | |
+| [schema_id](outputs.tf#L30) | Schema resource id. | |
+| [subscription_id](outputs.tf#L35) | Subscription ids. | |
+| [subscriptions](outputs.tf#L45) | Subscription resources. | |
+| [topic](outputs.tf#L53) | Topic resource. | |
diff --git a/modules/pubsub/main.tf b/modules/pubsub/main.tf
index 545abbbd7f..7e5630a9af 100644
--- a/modules/pubsub/main.tf
+++ b/modules/pubsub/main.tf
@@ -35,11 +35,20 @@ locals {
}
}
+resource "google_pubsub_schema" "default" {
+ count = var.schema == null ? 0 : 1
+ name = "${var.name}-schema"
+ type = var.schema.schema_type
+ definition = var.schema.definition
+ project = var.project_id
+}
+
resource "google_pubsub_topic" "default" {
- project = var.project_id
- name = var.name
- kms_key_name = var.kms_key
- labels = var.labels
+ project = var.project_id
+ name = var.name
+ kms_key_name = var.kms_key
+ labels = var.labels
+ message_retention_duration = var.message_retention_duration
dynamic "message_storage_policy" {
for_each = length(var.regions) > 0 ? [var.regions] : []
@@ -47,6 +56,14 @@ resource "google_pubsub_topic" "default" {
allowed_persistence_regions = var.regions
}
}
+
+ dynamic "schema_settings" {
+ for_each = var.schema == null ? [] : [""]
+ content {
+ schema = google_pubsub_schema.default[0].id
+ encoding = var.schema.msg_encoding
+ }
+ }
}
resource "google_pubsub_topic_iam_binding" "default" {
@@ -66,6 +83,7 @@ resource "google_pubsub_subscription" "default" {
ack_deadline_seconds = each.value.options.ack_deadline_seconds
message_retention_duration = each.value.options.message_retention_duration
retain_acked_messages = each.value.options.retain_acked_messages
+ filter = each.value.options.filter
dynamic "expiration_policy" {
for_each = each.value.options.expiration_policy_ttl == null ? [] : [""]
@@ -98,6 +116,16 @@ resource "google_pubsub_subscription" "default" {
}
}
}
+
+ dynamic "bigquery_config" {
+ for_each = try(var.bigquery_subscription_configs[each.key], null) == null ? [] : [""]
+ content {
+ table = var.bigquery_subscription_configs[each.key].table
+ use_topic_schema = var.bigquery_subscription_configs[each.key].use_topic_schema
+ write_metadata = var.bigquery_subscription_configs[each.key].write_metadata
+ drop_unknown_fields = var.bigquery_subscription_configs[each.key].drop_unknown_fields
+ }
+ }
}
resource "google_pubsub_subscription_iam_binding" "default" {
diff --git a/modules/pubsub/outputs.tf b/modules/pubsub/outputs.tf
index c26eb4d9a5..4aea42c53b 100644
--- a/modules/pubsub/outputs.tf
+++ b/modules/pubsub/outputs.tf
@@ -22,6 +22,16 @@ output "id" {
]
}
+output "schema" {
+ description = "Schema resource."
+ value = try(google_pubsub_schema.default[0], null)
+}
+
+output "schema_id" {
+ description = "Schema resource id."
+ value = try(google_pubsub_schema.default[0].id, null)
+}
+
output "subscription_id" {
description = "Subscription ids."
value = {
diff --git a/modules/pubsub/variables.tf b/modules/pubsub/variables.tf
index 78dd2fa628..afefb4a813 100644
--- a/modules/pubsub/variables.tf
+++ b/modules/pubsub/variables.tf
@@ -14,6 +14,17 @@
* limitations under the License.
*/
+variable "bigquery_subscription_configs" {
+ description = "Configuration parameters for BigQuery subscriptions."
+ type = map(object({
+ table = string
+ use_topic_schema = bool
+ write_metadata = bool
+ drop_unknown_fields = bool
+ }))
+ default = {}
+}
+
variable "dead_letter_configs" {
description = "Per-subscription dead letter policy configuration."
type = map(object({
@@ -30,12 +41,14 @@ variable "defaults" {
message_retention_duration = string
retain_acked_messages = bool
expiration_policy_ttl = string
+ filter = string
})
default = {
ack_deadline_seconds = null
message_retention_duration = null
retain_acked_messages = null
expiration_policy_ttl = null
+ filter = null
}
}
@@ -57,6 +70,12 @@ variable "labels" {
default = {}
}
+variable "message_retention_duration" {
+ description = "Minimum duration to retain a message after it is published to the topic."
+ type = string
+ default = null
+}
+
variable "name" {
description = "PubSub topic name."
type = string
@@ -86,6 +105,16 @@ variable "regions" {
default = []
}
+variable "schema" {
+ description = "Topic schema. If set, all messages in this topic should follow this schema."
+ type = object({
+ definition = string
+ msg_encoding = optional(string, "ENCODING_UNSPECIFIED")
+ schema_type = string
+ })
+ default = null
+}
+
variable "subscription_iam" {
description = "IAM bindings for subscriptions in {SUBSCRIPTION => {ROLE => [MEMBERS]}} format."
type = map(map(list(string)))
@@ -101,6 +130,7 @@ variable "subscriptions" {
message_retention_duration = string
retain_acked_messages = bool
expiration_policy_ttl = string
+ filter = string
})
}))
default = {}
diff --git a/modules/pubsub/versions.tf b/modules/pubsub/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/pubsub/versions.tf
+++ b/modules/pubsub/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/secret-manager/README.md b/modules/secret-manager/README.md
index 60296a960a..6816db4d4b 100644
--- a/modules/secret-manager/README.md
+++ b/modules/secret-manager/README.md
@@ -14,9 +14,9 @@ The secret replication policy is automatically managed if no location is set, or
```hcl
module "secret-manager" {
- source = "./modules/secret-manager"
+ source = "./fabric/modules/secret-manager"
project_id = "my-project"
- secrets = {
+ secrets = {
test-auto = null
test-manual = ["europe-west1", "europe-west4"]
}
@@ -30,14 +30,14 @@ IAM bindings can be set per secret in the same way as for most other modules sup
```hcl
module "secret-manager" {
- source = "./modules/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 = {
@@ -54,9 +54,9 @@ As mentioned above, please be aware that **version data will be stored in state
```hcl
module "secret-manager" {
- source = "./modules/secret-manager"
+ source = "./fabric/modules/secret-manager"
project_id = "my-project"
- secrets = {
+ secrets = {
test-auto = null
test-manual = ["europe-west1", "europe-west4"]
}
@@ -91,7 +91,7 @@ module "secret-manager" {
| [ids](outputs.tf#L17) | Secret ids keyed by secret_ids (names). | |
| [secrets](outputs.tf#L24) | Secret resources. | |
| [version_ids](outputs.tf#L29) | Version ids keyed by secret name : version name. | |
-| [versions](outputs.tf#L36) | Secret versions. | |
+| [versions](outputs.tf#L36) | Secret versions. | ✓ |
## Requirements
diff --git a/modules/secret-manager/outputs.tf b/modules/secret-manager/outputs.tf
index 7267fa1293..7295b84df2 100644
--- a/modules/secret-manager/outputs.tf
+++ b/modules/secret-manager/outputs.tf
@@ -36,4 +36,5 @@ output "version_ids" {
output "versions" {
description = "Secret versions."
value = google_secret_manager_secret_version.default
+ sensitive = true
}
diff --git a/modules/secret-manager/versions.tf b/modules/secret-manager/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/secret-manager/versions.tf
+++ b/modules/secret-manager/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/service-directory/README.md b/modules/service-directory/README.md
index b67753c5a4..d6961b418a 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 = "./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 = "./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 = "./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" = [
@@ -77,7 +77,7 @@ module "service-directory" {
}
module "dns-sd" {
- source = "./modules/dns"
+ source = "./fabric/modules/dns"
project_id = "my-project"
type = "service-directory"
name = "apps"
diff --git a/modules/service-directory/versions.tf b/modules/service-directory/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/service-directory/versions.tf
+++ b/modules/service-directory/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/source-repository/README.md b/modules/source-repository/README.md
index 48f29aa14a..389de9e9d7 100644
--- a/modules/source-repository/README.md
+++ b/modules/source-repository/README.md
@@ -1,15 +1,14 @@
# Google Cloud Source Repository Module
-This module allows managing a single Cloud Source Repository, including IAM bindings.
-
+This module allows managing a single Cloud Source Repository, including IAM bindings and basic Cloud Build triggers.
## Examples
-### Simple repository with IAM
+### Repository with IAM
```hcl
module "repo" {
- source = "./modules/source-repository"
+ source = "./fabric/modules/source-repository"
project_id = "my-project"
name = "my-repo"
iam = {
@@ -18,21 +17,64 @@ module "repo" {
}
# tftest modules=1 resources=2
```
+
+### Repository with Cloud Build trigger
+
+```hcl
+module "repo" {
+ source = "./fabric/modules/source-repository"
+ project_id = "my-project"
+ name = "my-repo"
+ triggers = {
+ foo = {
+ filename = "ci/workflow-foo.yaml"
+ included_files = ["**/*tf"]
+ service_account = null
+ substitutions = {
+ BAR = 1
+ }
+ template = {
+ branch_name = "main"
+ project_id = null
+ tag_name = null
+ }
+ }
+ }
+}
+# tftest modules=1 resources=2
+```
+
+
+## Files
+
+| name | description | resources |
+|---|---|---|
+| [iam.tf](./iam.tf) | IAM resources. | google_sourcerepo_repository_iam_binding
· google_sourcerepo_repository_iam_member
|
+| [main.tf](./main.tf) | Module-level locals and resources. | google_cloudbuild_trigger
· google_sourcerepo_repository
|
+| [outputs.tf](./outputs.tf) | Module outputs. | |
+| [variables.tf](./variables.tf) | Module variables. | |
+| [versions.tf](./versions.tf) | Version pins. | |
+
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [name](variables.tf#L23) | Repository name. | string
| ✓ | |
-| [project_id](variables.tf#L28) | Project used for resources. | string
| ✓ | |
-| [iam](variables.tf#L17) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
+| [name](variables.tf#L44) | Repository name. | string
| ✓ | |
+| [project_id](variables.tf#L49) | Project used for resources. | string
| ✓ | |
+| [group_iam](variables.tf#L17) | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | map(list(string))
| | {}
|
+| [iam](variables.tf#L24) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
+| [iam_additive](variables.tf#L31) | IAM additive bindings in {ROLE => [MEMBERS]} format. | map(list(string))
| | {}
|
+| [iam_additive_members](variables.tf#L38) | IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values. | map(list(string))
| | {}
|
+| [triggers](variables.tf#L54) | Cloud Build triggers. | map(object({…}))
| | {}
|
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [id](outputs.tf#L17) | Repository id. | |
-| [url](outputs.tf#L22) | Repository URL. | |
+| [name](outputs.tf#L22) | Repository name. | |
+| [url](outputs.tf#L27) | Repository URL. | |
diff --git a/modules/source-repository/iam.tf b/modules/source-repository/iam.tf
new file mode 100644
index 0000000000..e5c3ec499e
--- /dev/null
+++ b/modules/source-repository/iam.tf
@@ -0,0 +1,67 @@
+/**
+ * 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:file:description IAM resources.
+
+locals {
+ _group_iam_roles = distinct(flatten(values(var.group_iam)))
+ _group_iam = {
+ for r in local._group_iam_roles : r => [
+ for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
+ ]
+ }
+ _iam_additive_pairs = flatten([
+ for role, members in var.iam_additive : [
+ for member in members : { role = role, member = member }
+ ]
+ ])
+ _iam_additive_member_pairs = flatten([
+ for member, roles in var.iam_additive_members : [
+ for role in roles : { role = role, member = member }
+ ]
+ ])
+ iam = {
+ for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
+ role => concat(
+ try(var.iam[role], []),
+ try(local._group_iam[role], [])
+ )
+ }
+ iam_additive = {
+ for pair in concat(local._iam_additive_pairs, local._iam_additive_member_pairs) :
+ "${pair.role}-${pair.member}" => pair
+ }
+}
+
+resource "google_sourcerepo_repository_iam_binding" "authoritative" {
+ for_each = local.iam
+ project = var.project_id
+ repository = google_sourcerepo_repository.default.name
+ role = each.key
+ members = each.value
+}
+
+resource "google_sourcerepo_repository_iam_member" "additive" {
+ for_each = (
+ length(var.iam_additive) + length(var.iam_additive_members) > 0
+ ? local.iam_additive
+ : {}
+ )
+ project = var.project_id
+ repository = google_sourcerepo_repository.default.name
+ role = each.value.role
+ member = each.value.member
+}
diff --git a/modules/source-repository/main.tf b/modules/source-repository/main.tf
index c4057d76d3..d74b7e6c87 100644
--- a/modules/source-repository/main.tf
+++ b/modules/source-repository/main.tf
@@ -19,14 +19,18 @@ resource "google_sourcerepo_repository" "default" {
name = var.name
}
-resource "google_sourcerepo_repository_iam_binding" "default" {
- for_each = var.iam
- project = var.project_id
- repository = google_sourcerepo_repository.default.name
- role = each.key
- members = each.value
-
- depends_on = [
- google_sourcerepo_repository.default
- ]
+resource "google_cloudbuild_trigger" "default" {
+ for_each = coalesce(var.triggers, {})
+ project = var.project_id
+ name = each.key
+ filename = each.value.filename
+ included_files = each.value.included_files
+ service_account = each.value.service_account
+ substitutions = each.value.substitutions
+ trigger_template {
+ project_id = try(each.value.template.project_id, var.project_id)
+ branch_name = try(each.value.template.branch_name, null)
+ repo_name = google_sourcerepo_repository.default.name
+ tag_name = try(each.value.template.tag_name, null)
+ }
}
diff --git a/modules/source-repository/outputs.tf b/modules/source-repository/outputs.tf
index d1a4b25e9d..be55307a6d 100644
--- a/modules/source-repository/outputs.tf
+++ b/modules/source-repository/outputs.tf
@@ -19,6 +19,11 @@ output "id" {
value = google_sourcerepo_repository.default.id
}
+output "name" {
+ description = "Repository name."
+ value = google_sourcerepo_repository.default.name
+}
+
output "url" {
description = "Repository URL."
value = google_sourcerepo_repository.default.url
diff --git a/modules/source-repository/variables.tf b/modules/source-repository/variables.tf
index e592f35892..587b0f6d2b 100644
--- a/modules/source-repository/variables.tf
+++ b/modules/source-repository/variables.tf
@@ -14,10 +14,31 @@
* limitations under the License.
*/
+variable "group_iam" {
+ description = "Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
variable "iam" {
description = "IAM bindings in {ROLE => [MEMBERS]} format."
type = map(list(string))
default = {}
+ nullable = false
+}
+
+variable "iam_additive" {
+ description = "IAM additive bindings in {ROLE => [MEMBERS]} format."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "iam_additive_members" {
+ description = "IAM additive bindings in {MEMBERS => [ROLE]} format. This might break if members are dynamic values."
+ type = map(list(string))
+ default = {}
}
variable "name" {
@@ -29,3 +50,20 @@ variable "project_id" {
description = "Project used for resources."
type = string
}
+
+variable "triggers" {
+ description = "Cloud Build triggers."
+ type = map(object({
+ filename = string
+ included_files = list(string)
+ service_account = string
+ substitutions = map(string)
+ template = object({
+ branch_name = string
+ project_id = string
+ tag_name = string
+ })
+ }))
+ default = {}
+ nullable = false
+}
diff --git a/modules/source-repository/versions.tf b/modules/source-repository/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/source-repository/versions.tf
+++ b/modules/source-repository/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/modules/vpc-sc/README.md b/modules/vpc-sc/README.md
index 98381bae14..a695fa9e9a 100644
--- a/modules/vpc-sc/README.md
+++ b/modules/vpc-sc/README.md
@@ -2,7 +2,7 @@
This module offers a unified interface to manage VPC Service Controls [Access Policy](https://cloud.google.com/access-context-manager/docs/create-access-policy), [Access Levels](https://cloud.google.com/access-context-manager/docs/manage-access-levels), and [Service Perimeters](https://cloud.google.com/vpc-service-controls/docs/service-perimeters).
-Given the complexity of the underlying resources, the module intentionally mimics their interfaces to make it easier to map their documentation onto its variables, and reduce the internal complexity. The tradeoff is some verbosity, and a very complex type for the `service_perimeters_regular` variable (while [optional type attributes](https://www.terraform.io/language/expressions/type-constraints#experimental-optional-object-type-attributes) are still an experiment).
+Given the complexity of the underlying resources, the module intentionally mimics their interfaces to make it easier to map their documentation onto its variables, and reduce the internal complexity.
If you are using [Application Default Credentials](https://cloud.google.com/sdk/gcloud/reference/auth/application-default) with Terraform and run into permissions issues, make sure to check out the recommended provider configuration in the [VPC SC resources documentation](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/access_context_manager_access_level).
@@ -14,7 +14,7 @@ By default, the module is configured to use an existing policy, passed in by nam
```hcl
module "test" {
- source = "./modules/vpc-sc"
+ source = "./fabric/modules/vpc-sc"
access_policy = "12345678"
}
# tftest modules=0 resources=0
@@ -24,7 +24,7 @@ If you need the module to create the policy for you, use the `access_policy_crea
```hcl
module "test" {
- source = "./modules/vpc-sc"
+ source = "./fabric/modules/vpc-sc"
access_policy = null
access_policy_create = {
parent = "organizations/123456"
@@ -40,25 +40,20 @@ As highlighted above, the `access_levels` type replicates the underlying resourc
```hcl
module "test" {
- source = "./modules/vpc-sc"
+ source = "./fabric/modules/vpc-sc"
access_policy = "12345678"
access_levels = {
a1 = {
- combining_function = null
- conditions = [{
- members = ["user:user1@example.com"], ip_subnetworks = null,
- negate = null, regions = null, required_access_levels = null
- }]
+ conditions = [
+ { members = ["user:user1@example.com"] }
+ ]
}
a2 = {
combining_function = "OR"
- conditions = [{
- regions = ["IT", "FR"], ip_subnetworks = null,
- members = null, negate = null, required_access_levels = null
- },{
- ip_subnetworks = ["101.101.101.0/24"], members = null,
- negate = null, regions = null, required_access_levels = null
- }]
+ conditions = [
+ { regions = ["IT", "FR"] },
+ { ip_subnetworks = ["101.101.101.0/24"] }
+ ]
}
}
}
@@ -81,16 +76,13 @@ Resources for both perimeters have a `lifecycle` block that ignores changes to `
```hcl
module "test" {
- source = "./modules/vpc-sc"
+ source = "./fabric/modules/vpc-sc"
access_policy = "12345678"
service_perimeters_bridge = {
b1 = {
- status_resources = ["projects/111110", "projects/111111"]
- spec_resources = null
- use_explicit_dry_run_spec = false
+ status_resources = ["projects/111110", "projects/111111"]
}
b2 = {
- status_resources = null
spec_resources = ["projects/222220", "projects/222221"]
use_explicit_dry_run_spec = true
}
@@ -103,39 +95,65 @@ module "test" {
```hcl
module "test" {
- source = "./modules/vpc-sc"
+ source = "./fabric/modules/vpc-sc"
access_policy = "12345678"
access_levels = {
a1 = {
- combining_function = null
- conditions = [{
- members = ["user:user1@example.com"], ip_subnetworks = null,
- negate = null, regions = null, required_access_levels = null
- }]
+ conditions = [
+ { members = ["user:user1@example.com"] }
+ ]
}
a2 = {
- combining_function = null
- conditions = [{
- members = ["user:user2@example.com"], ip_subnetworks = null,
- negate = null, regions = null, required_access_levels = null
- }]
+ conditions = [
+ { members = ["user:user2@example.com"] }
+ ]
+ }
+ }
+ egress_policies = {
+ # allow writing to external GCS bucket from a specific SA
+ gcs-sa-foo = {
+ from = {
+ identities = [
+ "serviceAccount:foo@myproject.iam.gserviceaccount.com"
+ ]
+ }
+ to = {
+ operations = [{
+ method_selectors = ["*"]
+ service_name = "storage.googleapis.com"
+ }]
+ resources = ["projects/123456789"]
+ }
+ }
+ }
+ ingress_policies = {
+ # allow management from external automation SA
+ sa-tf-test = {
+ from = {
+ identities = [
+ "serviceAccount:test-tf@myproject.iam.gserviceaccount.com",
+ ],
+ source_access_levels = ["*"]
+ }
+ to = {
+ operations = [{ service_name = "*" }]
+ resources = ["*"]
+ }
}
}
service_perimeters_regular = {
r1 = {
- spec = null
status = {
- access_levels = [module.test.access_level_names["a1"], "a2"]
+ access_levels = ["a1", "a2"]
resources = ["projects/11111", "projects/111111"]
restricted_services = ["storage.googleapis.com"]
- egress_policies = null
- ingress_policies = null
+ egress_policies = ["gcs-sa-foo"]
+ ingress_policies = ["sa-tf-test"]
vpc_accessible_services = {
allowed_services = ["storage.googleapis.com"]
enable_restriction = true
}
}
- use_explicit_dry_run_spec = false
}
}
}
@@ -144,22 +162,38 @@ module "test" {
## Notes
-- To remove an access level, first remove the binding between perimeter and the access level in `status` and/or `spec` without removing the access level itself. Once you have run `terraform apply`, you'll then be able to remove the access level and run `terraform apply` again.
+- To remove an access level, first remove the binding between perimeter and the access level in `status` and/or `spec` without removing the access level itself. Once you have run `terraform apply`, you'll then be able to remove the access level and run `terraform apply` again.
## TODO
- [ ] implement support for the `google_access_context_manager_gcp_user_access_binding` resource
+
+
+## Files
+
+| name | description | resources |
+|---|---|---|
+| [access-levels.tf](./access-levels.tf) | Access level resources. | google_access_context_manager_access_level
|
+| [main.tf](./main.tf) | Module-level locals and resources. | google_access_context_manager_access_policy
|
+| [outputs.tf](./outputs.tf) | Module outputs. | |
+| [service-perimeters-bridge.tf](./service-perimeters-bridge.tf) | Bridge service perimeter resources. | google_access_context_manager_service_perimeter
|
+| [service-perimeters-regular.tf](./service-perimeters-regular.tf) | Regular service perimeter resources. | google_access_context_manager_service_perimeter
|
+| [variables.tf](./variables.tf) | Module variables. | |
+| [versions.tf](./versions.tf) | Version pins. | |
+
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [access_policy](variables.tf#L55) | Access Policy name, leave null to use auto-created one. | string
| ✓ | |
-| [access_levels](variables.tf#L17) | Map of access levels in name => [conditions] format. | map(object({…}))
| | {}
|
-| [access_policy_create](variables.tf#L60) | Access Policy configuration, fill in to create. Parent is in 'organizations/123456' format. | object({…})
| | null
|
-| [service_perimeters_bridge](variables.tf#L69) | Bridge service perimeters. | map(object({…}))
| | {}
|
-| [service_perimeters_regular](variables.tf#L79) | Regular service perimeters. | map(object({…}))
| | {}
|
+| [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({…}))
| | {}
|
## Outputs
diff --git a/modules/vpc-sc/access-levels.tf b/modules/vpc-sc/access-levels.tf
index 9aeb232be8..1eb85343f2 100644
--- a/modules/vpc-sc/access-levels.tf
+++ b/modules/vpc-sc/access-levels.tf
@@ -14,66 +14,66 @@
* limitations under the License.
*/
-# TODO(ludomagno): add a second variable and resource for custom access levels
+# tfdoc:file:description Access level resources.
# this code implements "additive" access levels, if "authoritative"
# access levels are needed, switch to the
# google_access_context_manager_access_levels resource
resource "google_access_context_manager_access_level" "basic" {
- for_each = var.access_levels == null ? {} : var.access_levels
- parent = "accessPolicies/${local.access_policy}"
- name = "accessPolicies/${local.access_policy}/accessLevels/${each.key}"
- title = each.key
+ for_each = var.access_levels
+ parent = "accessPolicies/${local.access_policy}"
+ name = "accessPolicies/${local.access_policy}/accessLevels/${each.key}"
+ title = each.key
+ description = each.value.description
+
basic {
combining_function = each.value.combining_function
+
dynamic "conditions" {
- for_each = toset(
- each.value.conditions == null ? [] : each.value.conditions
- )
- iterator = condition
+ for_each = toset(each.value.conditions)
+ iterator = c
content {
- # uncomment here and in the variable type to enable
- # dynamic "device_policy" {
- # for_each = toset(
- # condition.key.device_policy == null ? [] : [condition.key.device_policy]
- # )
- # iterator = device_policy
- # content {
- # dynamic "os_constraints" {
- # for_each = toset(
- # device_policy.key.os_constraints == null ? [] : device_policy.key.os_constraints
- # )
- # iterator = os_constraint
- # content {
- # minimum_version = os_constraint.key.minimum_version
- # os_type = os_constraint.key.os_type
- # require_verified_chrome_os = os_constraint.key.require_verified_chrome_os
- # }
- # }
- # allowed_encryption_statuses = device_policy.key.allowed_encryption_statuses
- # allowed_device_management_levels = device_policy.key.allowed_device_management_levels
- # require_admin_approval = device_policy.key.require_admin_approval
- # require_corp_owned = device_policy.key.require_corp_owned
- # require_screen_lock = device_policy.key.require_screen_lock
- # }
- # }
- ip_subnetworks = (
- condition.key.ip_subnetworks == null ? [] : condition.key.ip_subnetworks
- )
- members = (
- condition.key.members == null ? [] : condition.key.members
- )
- negate = condition.key.negate
- regions = (
- condition.key.regions == null ? [] : condition.key.regions
- )
- required_access_levels = (
- condition.key.required_access_levels == null
- ? []
- : condition.key.required_access_levels
- )
+ ip_subnetworks = c.value.ip_subnetworks
+ members = c.value.members
+ negate = c.value.negate
+ regions = c.value.regions
+ required_access_levels = coalesce(c.value.required_access_levels, [])
+
+ dynamic "device_policy" {
+ for_each = c.value.device_policy == null ? [] : [c.value.device_policy]
+ iterator = dp
+ content {
+
+ allowed_device_management_levels = (
+ dp.value.allowed_device_management_levels
+ )
+ allowed_encryption_statuses = (
+ dp.value.allowed_encryption_statuses
+ )
+ require_admin_approval = dp.value.key.require_admin_approval
+ require_corp_owned = dp.value.require_corp_owned
+ require_screen_lock = dp.value.require_screen_lock
+
+ dynamic "os_constraints" {
+ for_each = toset(
+ dp.value.os_constraints == null
+ ? []
+ : dp.value.os_constraints
+ )
+ iterator = oc
+ content {
+ minimum_version = oc.value.minimum_version
+ os_type = oc.value.os_type
+ require_verified_chrome_os = oc.value.require_verified_chrome_os
+ }
+ }
+
+ }
+ }
+
}
}
+
}
}
diff --git a/modules/vpc-sc/service-perimeters-bridge.tf b/modules/vpc-sc/service-perimeters-bridge.tf
index f50d6d5762..e3233082c0 100644
--- a/modules/vpc-sc/service-perimeters-bridge.tf
+++ b/modules/vpc-sc/service-perimeters-bridge.tf
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+# tfdoc:file:description Bridge service perimeter resources.
+
# this code implements "additive" service perimeters, if "authoritative"
# service perimeters are needed, switch to the
# google_access_context_manager_service_perimeters resource
diff --git a/modules/vpc-sc/service-perimeters-regular.tf b/modules/vpc-sc/service-perimeters-regular.tf
index a834ce7580..5b87ca3ff8 100644
--- a/modules/vpc-sc/service-perimeters-regular.tf
+++ b/modules/vpc-sc/service-perimeters-regular.tf
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+# tfdoc:file:description Regular service perimeter resources.
+
# this code implements "additive" service perimeters, if "authoritative"
# service perimeters are needed, switch to the
# google_access_context_manager_service_perimeters resource
@@ -26,53 +28,44 @@ resource "google_access_context_manager_service_perimeter" "regular" {
perimeter_type = "PERIMETER_TYPE_REGULAR"
use_explicit_dry_run_spec = each.value.use_explicit_dry_run_spec
dynamic "spec" {
- for_each = each.value.spec == null ? {} : { 1 = 1 }
+ for_each = each.value.spec == null ? [] : [each.value.spec]
+ iterator = spec
content {
access_levels = (
- each.value.spec.access_levels == null ? null : [
- for k in each.value.spec.access_levels :
+ spec.value.access_levels == null ? null : [
+ for k in spec.value.access_levels :
try(google_access_context_manager_access_level.basic[k].id, k)
]
)
- resources = each.value.spec.resources
- restricted_services = each.value.spec.restricted_services
- # begin egress_policies
+ resources = spec.value.resources
+ restricted_services = spec.value.restricted_services
+
dynamic "egress_policies" {
- for_each = toset(
- each.value.spec.egress_policies == null
- ? []
- : each.value.spec.egress_policies
- )
+ for_each = spec.value.egress_policies == null ? {} : {
+ for k in spec.value.egress_policies :
+ k => lookup(var.egress_policies, k, null)
+ if contains(keys(var.egress_policies), k)
+ }
iterator = policy
content {
- # begin egress_from
dynamic "egress_from" {
- for_each = policy.key.egress_from == null ? {} : { 1 = 1 }
+ for_each = policy.value.from == null ? [] : [""]
content {
- identity_type = policy.key.egress_from.identity_type
- identities = policy.key.egress_from.identities
+ identity_type = policy.value.from.identity_type
+ identities = policy.value.from.identities
}
}
- # end egress_from
- # begin egress_to
dynamic "egress_to" {
- for_each = policy.key.egress_to == null ? {} : { 1 = 1 }
+ for_each = policy.value.to == null ? [] : [""]
content {
- resources = policy.key.egress_to.resources
+ resources = policy.value.to.resources
dynamic "operations" {
- for_each = toset(
- policy.key.egress_to.operations == null
- ? []
- : policy.key.egress_to.operations
- )
+ for_each = toset(policy.value.to.operations)
+ iterator = o
content {
- service_name = operations.key.service_name
+ service_name = o.value.service_name
dynamic "method_selectors" {
- for_each = toset(
- operations.key.method_selectors == null
- ? []
- : operations.key.method_selectors
- )
+ for_each = toset(coalesce(o.value.method_selectors, []))
content {
method = method_selectors.key
}
@@ -81,140 +74,110 @@ resource "google_access_context_manager_service_perimeter" "regular" {
}
}
}
- # end egress_to
}
}
- # end egress_policies
- # begin ingress_policies
+
dynamic "ingress_policies" {
- for_each = toset(
- each.value.spec.ingress_policies == null
- ? []
- : each.value.spec.ingress_policies
- )
+ for_each = spec.value.ingress_policies == null ? {} : {
+ for k in spec.value.ingress_policies :
+ k => lookup(var.ingress_policies, k, null)
+ if contains(keys(var.ingress_policies), k)
+ }
iterator = policy
content {
- # begin ingress_from
dynamic "ingress_from" {
- for_each = policy.key.ingress_from == null ? {} : { 1 = 1 }
+ for_each = policy.value.from == null ? [] : [""]
content {
- identity_type = policy.key.ingress_from.identity_type
- identities = policy.key.ingress_from.identities
- # begin sources
+ identity_type = policy.value.from.identity_type
+ identities = policy.value.from.identities
dynamic "sources" {
- for_each = toset(
- policy.key.ingress_from.source_access_levels == null
- ? []
- : policy.key.ingress_from.source_access_levels
- )
+ for_each = toset(policy.value.from.access_levels)
+ iterator = s
content {
- access_level = sources.key
+ access_level = try(
+ google_access_context_manager_access_level.basic[s.value].id, s.value
+ )
}
}
dynamic "sources" {
- for_each = toset(
- policy.key.ingress_from.source_resources == null
- ? []
- : policy.key.ingress_from.source_resources
- )
+ for_each = toset(policy.value.from.resources)
content {
resource = sources.key
}
}
- # end sources
}
}
- # end ingress_from
- # begin ingress_to
dynamic "ingress_to" {
- for_each = policy.key.ingress_to == null ? {} : { 1 = 1 }
+ for_each = policy.value.to == null ? [] : [""]
content {
- resources = policy.key.ingress_to.resources
+ resources = policy.value.to.resources
dynamic "operations" {
- for_each = toset(
- policy.key.ingress_to.operations == null
- ? []
- : policy.key.ingress_to.operations
- )
+ for_each = toset(policy.value.to.operations)
+ iterator = o
content {
- service_name = operations.key.service_name
+ service_name = o.value.service_name
dynamic "method_selectors" {
- for_each = toset(
- operations.key.method_selectors == null
- ? []
- : operations.key.method_selectors
- )
+ for_each = toset(coalesce(o.value.method_selectors, []))
content {
- method = method_selectors.key
+ method = method_selectors.value
}
}
}
}
}
}
- # end ingress_to
}
}
- # end ingress_policies
- # begin vpc_accessible_services
+
dynamic "vpc_accessible_services" {
- for_each = each.value.spec.vpc_accessible_services == null ? {} : { 1 = 1 }
+ for_each = spec.value.vpc_accessible_services == null ? {} : { 1 = 1 }
content {
- allowed_services = each.value.spec.vpc_accessible_services.allowed_services
- enable_restriction = each.value.spec.vpc_accessible_services.enable_restriction
+ allowed_services = spec.value.vpc_accessible_services.allowed_services
+ enable_restriction = spec.value.vpc_accessible_services.enable_restriction
}
}
- # end vpc_accessible_services
+
}
}
dynamic "status" {
- for_each = each.value.status == null ? {} : { 1 = 1 }
+ for_each = each.value.status == null ? [] : [each.value.status]
+ iterator = status
content {
access_levels = (
- each.value.status.access_levels == null ? null : [
- for k in each.value.status.access_levels :
+ status.value.access_levels == null ? null : [
+ for k in status.value.access_levels :
try(google_access_context_manager_access_level.basic[k].id, k)
]
)
- resources = each.value.status.resources
- restricted_services = each.value.status.restricted_services
- # begin egress_policies
+ resources = status.value.resources
+ restricted_services = status.value.restricted_services
+
dynamic "egress_policies" {
- for_each = toset(
- each.value.status.egress_policies == null
- ? []
- : each.value.status.egress_policies
- )
+ for_each = status.value.egress_policies == null ? {} : {
+ for k in status.value.egress_policies :
+ k => lookup(var.egress_policies, k, null)
+ if contains(keys(var.egress_policies), k)
+ }
iterator = policy
content {
- # begin egress_from
dynamic "egress_from" {
- for_each = policy.key.egress_from == null ? {} : { 1 = 1 }
+ for_each = policy.value.from == null ? [] : [""]
content {
- identity_type = policy.key.egress_from.identity_type
- identities = policy.key.egress_from.identities
+ identity_type = policy.value.from.identity_type
+ identities = policy.value.from.identities
}
}
- # end egress_from
- # begin egress_to
dynamic "egress_to" {
- for_each = policy.key.egress_to == null ? {} : { 1 = 1 }
+ for_each = policy.value.to == null ? [] : [""]
content {
- resources = policy.key.egress_to.resources
+ resources = policy.value.to.resources
dynamic "operations" {
- for_each = toset(
- policy.key.egress_to.operations == null
- ? []
- : policy.key.egress_to.operations
- )
+ for_each = toset(policy.value.to.operations)
+ iterator = o
content {
- service_name = operations.key.service_name
+ service_name = o.value.service_name
dynamic "method_selectors" {
- for_each = toset(
- operations.key.method_selectors == null
- ? []
- : operations.key.method_selectors
- )
+ for_each = toset(coalesce(o.value.method_selectors, []))
content {
method = method_selectors.key
}
@@ -223,90 +186,70 @@ resource "google_access_context_manager_service_perimeter" "regular" {
}
}
}
- # end egress_to
}
}
- # end egress_policies
- # begin ingress_policies
+
dynamic "ingress_policies" {
- for_each = toset(
- each.value.status.ingress_policies == null
- ? []
- : each.value.status.ingress_policies
- )
+ for_each = status.value.ingress_policies == null ? {} : {
+ for k in status.value.ingress_policies :
+ k => lookup(var.ingress_policies, k, null)
+ if contains(keys(var.ingress_policies), k)
+ }
iterator = policy
content {
- # begin ingress_from
dynamic "ingress_from" {
- for_each = policy.key.ingress_from == null ? {} : { 1 = 1 }
+ for_each = policy.value.from == null ? [] : [""]
content {
- identity_type = policy.key.ingress_from.identity_type
- identities = policy.key.ingress_from.identities
- # begin sources
+ identity_type = policy.value.from.identity_type
+ identities = policy.value.from.identities
dynamic "sources" {
- for_each = toset(
- policy.key.ingress_from.source_access_levels == null
- ? []
- : policy.key.ingress_from.source_access_levels
- )
+ for_each = toset(policy.value.from.access_levels)
+ iterator = s
content {
- access_level = sources.key
+ access_level = try(
+ google_access_context_manager_access_level.basic[s.value].id,
+ s.value
+ )
}
}
dynamic "sources" {
- for_each = toset(
- policy.key.ingress_from.source_resources == null
- ? []
- : policy.key.ingress_from.source_resources
- )
+ for_each = toset(policy.value.from.resources)
content {
resource = sources.key
}
}
- # end sources
}
}
- # end ingress_from
- # begin ingress_to
dynamic "ingress_to" {
- for_each = policy.key.ingress_to == null ? {} : { 1 = 1 }
+ for_each = policy.value.to == null ? [] : [""]
content {
- resources = policy.key.ingress_to.resources
+ resources = policy.value.to.resources
dynamic "operations" {
- for_each = toset(
- policy.key.ingress_to.operations == null
- ? []
- : policy.key.ingress_to.operations
- )
+ for_each = toset(policy.value.to.operations)
+ iterator = o
content {
- service_name = operations.key.service_name
+ service_name = o.value.service_name
dynamic "method_selectors" {
- for_each = toset(
- operations.key.method_selectors == null
- ? []
- : operations.key.method_selectors
- )
+ for_each = toset(coalesce(o.value.method_selectors, []))
content {
- method = method_selectors.key
+ method = method_selectors.value
}
}
}
}
}
}
- # end ingress_to
}
}
- # end ingress_policies
- # begin vpc_accessible_services
+
dynamic "vpc_accessible_services" {
- for_each = each.value.status.vpc_accessible_services == null ? {} : { 1 = 1 }
+ for_each = status.value.vpc_accessible_services == null ? {} : { 1 = 1 }
content {
- allowed_services = each.value.status.vpc_accessible_services.allowed_services
- enable_restriction = each.value.status.vpc_accessible_services.enable_restriction
+ allowed_services = status.value.vpc_accessible_services.allowed_services
+ enable_restriction = status.value.vpc_accessible_services.enable_restriction
}
}
- # end vpc_accessible_services
+
}
}
# lifecycle {
diff --git a/modules/vpc-sc/variables.tf b/modules/vpc-sc/variables.tf
index e7318de710..a196cc52b1 100644
--- a/modules/vpc-sc/variables.tf
+++ b/modules/vpc-sc/variables.tf
@@ -15,31 +15,32 @@
*/
variable "access_levels" {
- description = "Map of access levels in name => [conditions] format."
+ description = "Access level definitions."
type = map(object({
- combining_function = string
- conditions = list(object({
- # disabled to reduce var surface, uncomment here and in resource to enable
- # device_policy = object({
- # require_screen_lock = bool
- # allowed_encryption_statuses = list(string)
- # allowed_device_management_levels = list(string)
- # os_constraints = list(object({
- # minimum_version = string
- # os_type = string
- # require_verified_chrome_os = bool
- # }))
- # require_admin_approval = bool
- # require_corp_owned = bool
- # })
- ip_subnetworks = list(string)
- members = list(string)
- negate = bool
- regions = list(string)
- required_access_levels = list(string)
- }))
+ 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 = {}
+ default = {}
+ nullable = false
validation {
condition = alltrue([
for k, v in var.access_levels : (
@@ -53,7 +54,7 @@ variable "access_levels" {
}
variable "access_policy" {
- description = "Access Policy name, leave null to use auto-created one."
+ description = "Access Policy name, set to null if creating one."
type = string
}
@@ -66,12 +67,72 @@ variable "access_policy_create" {
default = null
}
+variable "egress_policies" {
+ description = "Egress policy definitions that can be referenced in perimeters."
+ 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
+ validation {
+ condition = alltrue([
+ for k, v in var.egress_policies : contains([
+ "IDENTITY_TYPE_UNSPECIFIED", "ANY_IDENTITY",
+ "ANY_USER", "ANY_SERVICE_ACCOUNT"
+ ], v.from.identity_type)
+ ])
+ error_message = "Invalid `from.identity_type` value in egress policy."
+ }
+}
+
+variable "ingress_policies" {
+ description = "Ingress policy definitions that can be referenced in perimeters."
+ 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
+ validation {
+ condition = alltrue([
+ for k, v in var.ingress_policies :
+ v.from.identity_type == null || contains([
+ "IDENTITY_TYPE_UNSPECIFIED", "ANY_IDENTITY",
+ "ANY_USER", "ANY_SERVICE_ACCOUNT"
+ ], coalesce(v.from.identity_type, "-"))
+ ])
+ error_message = "Invalid `from.identity_type` value in eress policy."
+ }
+}
+
variable "service_perimeters_bridge" {
description = "Bridge service perimeters."
type = map(object({
- spec_resources = list(string)
- status_resources = list(string)
- use_explicit_dry_run_spec = bool
+ spec_resources = optional(list(string))
+ status_resources = optional(list(string))
+ use_explicit_dry_run_spec = optional(bool, false)
}))
default = {}
}
@@ -79,81 +140,30 @@ variable "service_perimeters_bridge" {
variable "service_perimeters_regular" {
description = "Regular service perimeters."
type = map(object({
- spec = object({
- access_levels = list(string)
- resources = list(string)
- restricted_services = list(string)
- egress_policies = list(object({
- egress_from = object({
- identity_type = string
- identities = list(string)
- })
- egress_to = object({
- operations = list(object({
- method_selectors = list(string)
- service_name = string
- }))
- resources = list(string)
- })
- }))
- ingress_policies = list(object({
- ingress_from = object({
- identity_type = string
- identities = list(string)
- source_access_levels = list(string)
- source_resources = list(string)
- })
- ingress_to = object({
- operations = list(object({
- method_selectors = list(string)
- service_name = string
- }))
- resources = list(string)
- })
- }))
- vpc_accessible_services = object({
+ spec = optional(object({
+ access_levels = optional(list(string))
+ resources = optional(list(string))
+ restricted_services = optional(list(string))
+ egress_policies = optional(list(string))
+ ingress_policies = optional(list(string))
+ vpc_accessible_services = optional(object({
allowed_services = list(string)
enable_restriction = bool
- })
- })
- status = object({
- access_levels = list(string)
- resources = list(string)
- restricted_services = list(string)
- egress_policies = list(object({
- egress_from = object({
- identity_type = string
- identities = list(string)
- })
- egress_to = object({
- operations = list(object({
- method_selectors = list(string)
- service_name = string
- }))
- resources = list(string)
- })
}))
- ingress_policies = list(object({
- ingress_from = object({
- identity_type = string
- identities = list(string)
- source_access_levels = list(string)
- source_resources = list(string)
- })
- ingress_to = object({
- operations = list(object({
- method_selectors = list(string)
- service_name = string
- }))
- resources = list(string)
- })
- }))
- vpc_accessible_services = object({
+ }))
+ status = optional(object({
+ access_levels = optional(list(string))
+ resources = optional(list(string))
+ restricted_services = optional(list(string))
+ egress_policies = optional(list(string))
+ ingress_policies = optional(list(string))
+ vpc_accessible_services = optional(object({
allowed_services = list(string)
enable_restriction = bool
- })
- })
- use_explicit_dry_run_spec = bool
+ }))
+ }))
+ use_explicit_dry_run_spec = optional(bool, false)
}))
- default = {}
+ default = {}
+ nullable = false
}
diff --git a/modules/vpc-sc/versions.tf b/modules/vpc-sc/versions.tf
index 290412687a..90b632f6d4 100644
--- a/modules/vpc-sc/versions.tf
+++ b/modules/vpc-sc/versions.tf
@@ -13,15 +13,15 @@
# limitations under the License.
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.1"
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 4.0.0"
+ version = ">= 4.47.0" # tftest
}
}
}
diff --git a/stages.png b/stages.png
deleted file mode 100644
index 83f3c7e8e3..0000000000
Binary files a/stages.png and /dev/null differ
diff --git a/tests/examples/cloud_operations/asset_inventory_feed_remediation/__init__.py b/tests/blueprints/__init__.py
similarity index 100%
rename from tests/examples/cloud_operations/asset_inventory_feed_remediation/__init__.py
rename to tests/blueprints/__init__.py
diff --git a/tests/examples/cloud_operations/dns_fine_grained_iam/__init__.py b/tests/blueprints/apigee/bigquery-analytics/__init__.py
similarity index 100%
rename from tests/examples/cloud_operations/dns_fine_grained_iam/__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 0000000000..8a650b56e2
--- /dev/null
+++ b/tests/blueprints/apigee/bigquery-analytics/basic.tfvars
@@ -0,0 +1,23 @@
+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"]
+ psa_ip_cidr_range = "10.0.4.0/22"
+ }
+}
+psc_config = {
+ europe-west1 = "10.0.0.0/28"
+}
diff --git a/tests/blueprints/apigee/bigquery-analytics/basic.yaml b/tests/blueprints/apigee/bigquery-analytics/basic.yaml
new file mode 100644
index 0000000000..2b044dcb6a
--- /dev/null
+++ b/tests/blueprints/apigee/bigquery-analytics/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: 9
+ resources: 60
diff --git a/tests/blueprints/apigee/bigquery-analytics/tftest.yaml b/tests/blueprints/apigee/bigquery-analytics/tftest.yaml
new file mode 100644
index 0000000000..a3441f5596
--- /dev/null
+++ b/tests/blueprints/apigee/bigquery-analytics/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/bigquery-analytics
+
+tests:
+ basic:
diff --git a/tests/examples/cloud_operations/dns_shared_vpc/__init__.py b/tests/blueprints/apigee/hybrid-gke/__init__.py
similarity index 100%
rename from tests/examples/cloud_operations/dns_shared_vpc/__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 0000000000..5b2cb4ccf6
--- /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/blueprints/apigee/hybrid-gke/basic.yaml b/tests/blueprints/apigee/hybrid-gke/basic.yaml
new file mode 100644
index 0000000000..2db435daa2
--- /dev/null
+++ b/tests/blueprints/apigee/hybrid-gke/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: 9
+ resources: 37
diff --git a/tests/blueprints/apigee/hybrid-gke/tftest.yaml b/tests/blueprints/apigee/hybrid-gke/tftest.yaml
new file mode 100644
index 0000000000..ebe16e577e
--- /dev/null
+++ b/tests/blueprints/apigee/hybrid-gke/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/hybrid-gke
+
+tests:
+ basic:
diff --git a/tests/examples/cloud_operations/iam_delegated_role_grants/__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/examples/cloud_operations/iam_delegated_role_grants/__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 0000000000..ae07c514fd
--- /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 0000000000..ef1fa1e009
--- /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: 72
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 0000000000..5c92fb82ae
--- /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/examples/cloud_operations/onprem_sa_key_management/__init__.py b/tests/blueprints/cloud_operations/__init__.py
similarity index 100%
rename from tests/examples/cloud_operations/onprem_sa_key_management/__init__.py
rename to tests/blueprints/cloud_operations/__init__.py
diff --git a/tests/examples/cloud_operations/packer_image_builder/__init__.py b/tests/blueprints/cloud_operations/adfs/__init__.py
similarity index 100%
rename from tests/examples/cloud_operations/packer_image_builder/__init__.py
rename to tests/blueprints/cloud_operations/adfs/__init__.py
diff --git a/tests/blueprints/cloud_operations/adfs/fixture/main.tf b/tests/blueprints/cloud_operations/adfs/fixture/main.tf
new file mode 100644
index 0000000000..ac5a4133db
--- /dev/null
+++ b/tests/blueprints/cloud_operations/adfs/fixture/main.tf
@@ -0,0 +1,24 @@
+/**
+ * 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/adfs"
+ prefix = var.prefix
+ project_create = var.project_create
+ project_id = var.project_id
+ ad_dns_domain_name = var.ad_dns_domain_name
+ adfs_dns_domain_name = var.adfs_dns_domain_name
+}
diff --git a/tests/blueprints/cloud_operations/adfs/fixture/variables.tf b/tests/blueprints/cloud_operations/adfs/fixture/variables.tf
new file mode 100644
index 0000000000..2fb54b546d
--- /dev/null
+++ b/tests/blueprints/cloud_operations/adfs/fixture/variables.tf
@@ -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
+#
+# 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.
+
+# 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.
+
+variable "project_create" {
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = {
+ billing_account_id = "12345-12345-12345"
+ parent = "folders/123456789"
+ }
+}
+
+variable "project_id" {
+ type = string
+ default = "my-project"
+}
+
+variable "prefix" {
+ type = string
+ default = "test"
+}
+
+variable "network_config" {
+ type = object({
+ network = string
+ subnet = string
+ })
+ default = null
+}
+
+variable "ad_dns_domain_name" {
+ type = string
+ default = "example.com"
+}
+
+variable "adfs_dns_domain_name" {
+ type = string
+ default = "adfs.example.com"
+}
+
+variable "disk_size" {
+ type = number
+ default = 50
+}
+
+variable "disk_type" {
+ type = string
+ default = "pd-ssd"
+}
+
+variable "image" {
+ type = string
+ default = "projects/windows-cloud/global/images/family/windows-2022"
+}
+
+variable "instance_type" {
+ type = string
+ default = "n1-standard-2"
+}
+
+variable "region" {
+ type = string
+ default = "europe-west1"
+}
+
+variable "zone" {
+ type = string
+ default = "europe-west1-c"
+}
+
+variable "ad_ip_cidr_block" {
+ type = string
+ default = "10.0.0.0/24"
+}
+
+variable "subnet_ip_cidr_block" {
+ type = string
+ default = "10.0.1.0/28"
+}
diff --git a/tests/blueprints/cloud_operations/adfs/test_plan.py b/tests/blueprints/cloud_operations/adfs/test_plan.py
new file mode 100644
index 0000000000..c9682f2d28
--- /dev/null
+++ b/tests/blueprints/cloud_operations/adfs/test_plan.py
@@ -0,0 +1,19 @@
+# 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(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) == 15
diff --git a/tests/examples/cloud_operations/quota_monitoring/__init__.py b/tests/blueprints/cloud_operations/asset_inventory_feed_remediation/__init__.py
similarity index 100%
rename from tests/examples/cloud_operations/quota_monitoring/__init__.py
rename to tests/blueprints/cloud_operations/asset_inventory_feed_remediation/__init__.py
diff --git a/tests/examples/cloud_operations/asset_inventory_feed_remediation/fixture/cf/README b/tests/blueprints/cloud_operations/asset_inventory_feed_remediation/fixture/cf/README
similarity index 100%
rename from tests/examples/cloud_operations/asset_inventory_feed_remediation/fixture/cf/README
rename to tests/blueprints/cloud_operations/asset_inventory_feed_remediation/fixture/cf/README
diff --git a/tests/blueprints/cloud_operations/asset_inventory_feed_remediation/fixture/main.tf b/tests/blueprints/cloud_operations/asset_inventory_feed_remediation/fixture/main.tf
new file mode 100644
index 0000000000..83f86c9635
--- /dev/null
+++ b/tests/blueprints/cloud_operations/asset_inventory_feed_remediation/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/cloud-operations/asset-inventory-feed-remediation"
+ project_create = var.project_create
+ project_id = var.project_id
+}
diff --git a/tests/examples/cloud_operations/asset_inventory_feed_remediation/fixture/variables.tf b/tests/blueprints/cloud_operations/asset_inventory_feed_remediation/fixture/variables.tf
similarity index 100%
rename from tests/examples/cloud_operations/asset_inventory_feed_remediation/fixture/variables.tf
rename to tests/blueprints/cloud_operations/asset_inventory_feed_remediation/fixture/variables.tf
diff --git a/tests/examples/cloud_operations/asset_inventory_feed_remediation/test_plan.py b/tests/blueprints/cloud_operations/asset_inventory_feed_remediation/test_plan.py
similarity index 100%
rename from tests/examples/cloud_operations/asset_inventory_feed_remediation/test_plan.py
rename to tests/blueprints/cloud_operations/asset_inventory_feed_remediation/test_plan.py
diff --git a/tests/examples/cloud_operations/scheduled_asset_inventory_export_bq/__init__.py b/tests/blueprints/cloud_operations/dns_fine_grained_iam/__init__.py
similarity index 100%
rename from tests/examples/cloud_operations/scheduled_asset_inventory_export_bq/__init__.py
rename to tests/blueprints/cloud_operations/dns_fine_grained_iam/__init__.py
diff --git a/tests/blueprints/cloud_operations/dns_fine_grained_iam/fixture/main.tf b/tests/blueprints/cloud_operations/dns_fine_grained_iam/fixture/main.tf
new file mode 100644
index 0000000000..ed8a914e7f
--- /dev/null
+++ b/tests/blueprints/cloud_operations/dns_fine_grained_iam/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/cloud-operations/dns-fine-grained-iam"
+ name = var.name
+ project_create = var.project_create
+ project_id = var.project_id
+}
diff --git a/tests/examples/cloud_operations/dns_fine_grained_iam/fixture/variables.tf b/tests/blueprints/cloud_operations/dns_fine_grained_iam/fixture/variables.tf
similarity index 100%
rename from tests/examples/cloud_operations/dns_fine_grained_iam/fixture/variables.tf
rename to tests/blueprints/cloud_operations/dns_fine_grained_iam/fixture/variables.tf
diff --git a/tests/examples/cloud_operations/dns_fine_grained_iam/test_plan.py b/tests/blueprints/cloud_operations/dns_fine_grained_iam/test_plan.py
similarity index 100%
rename from tests/examples/cloud_operations/dns_fine_grained_iam/test_plan.py
rename to tests/blueprints/cloud_operations/dns_fine_grained_iam/test_plan.py
diff --git a/tests/examples/cloud_operations/unmanaged_instances_healthcheck/__init__.py b/tests/blueprints/cloud_operations/dns_shared_vpc/__init__.py
similarity index 100%
rename from tests/examples/cloud_operations/unmanaged_instances_healthcheck/__init__.py
rename to tests/blueprints/cloud_operations/dns_shared_vpc/__init__.py
diff --git a/tests/blueprints/cloud_operations/dns_shared_vpc/fixture/main.tf b/tests/blueprints/cloud_operations/dns_shared_vpc/fixture/main.tf
new file mode 100644
index 0000000000..78ae428114
--- /dev/null
+++ b/tests/blueprints/cloud_operations/dns_shared_vpc/fixture/main.tf
@@ -0,0 +1,24 @@
+/**
+ * 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/dns-shared-vpc"
+ billing_account_id = "111111-222222-333333"
+ folder_id = "folders/1234567890"
+ prefix = var.prefix
+ shared_vpc_link = "https://www.googleapis.com/compute/v1/projects/test-dns/global/networks/default"
+ teams = var.teams
+}
diff --git a/tests/blueprints/cloud_operations/dns_shared_vpc/fixture/variables.tf b/tests/blueprints/cloud_operations/dns_shared_vpc/fixture/variables.tf
new file mode 100644
index 0000000000..dd34e4d5c3
--- /dev/null
+++ b/tests/blueprints/cloud_operations/dns_shared_vpc/fixture/variables.tf
@@ -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
+#
+# 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.
+
+variable "prefix" {
+ type = string
+ default = "test"
+}
+
+variable "teams" {
+ type = list(string)
+ default = ["team1", "team2"]
+}
diff --git a/tests/examples/cloud_operations/dns_shared_vpc/test_plan.py b/tests/blueprints/cloud_operations/dns_shared_vpc/test_plan.py
similarity index 100%
rename from tests/examples/cloud_operations/dns_shared_vpc/test_plan.py
rename to tests/blueprints/cloud_operations/dns_shared_vpc/test_plan.py
diff --git a/tests/examples/cloud_operations/vm_migration/host_target_projects/__init__.py b/tests/blueprints/cloud_operations/iam_delegated_role_grants/__init__.py
similarity index 100%
rename from tests/examples/cloud_operations/vm_migration/host_target_projects/__init__.py
rename to tests/blueprints/cloud_operations/iam_delegated_role_grants/__init__.py
diff --git a/tests/blueprints/cloud_operations/iam_delegated_role_grants/fixture/main.tf b/tests/blueprints/cloud_operations/iam_delegated_role_grants/fixture/main.tf
new file mode 100644
index 0000000000..655439cfa9
--- /dev/null
+++ b/tests/blueprints/cloud_operations/iam_delegated_role_grants/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/cloud-operations/iam-delegated-role-grants"
+ project_create = true
+ project_id = var.project_id
+ project_administrators = ["user:user@example.com"]
+}
diff --git a/tests/examples/cloud_operations/iam_delegated_role_grants/fixture/variables.tf b/tests/blueprints/cloud_operations/iam_delegated_role_grants/fixture/variables.tf
similarity index 100%
rename from tests/examples/cloud_operations/iam_delegated_role_grants/fixture/variables.tf
rename to tests/blueprints/cloud_operations/iam_delegated_role_grants/fixture/variables.tf
diff --git a/tests/examples/cloud_operations/iam_delegated_role_grants/test_plan.py b/tests/blueprints/cloud_operations/iam_delegated_role_grants/test_plan.py
similarity index 100%
rename from tests/examples/cloud_operations/iam_delegated_role_grants/test_plan.py
rename to tests/blueprints/cloud_operations/iam_delegated_role_grants/test_plan.py
diff --git a/tests/examples/cloud_operations/vm_migration/host_target_sharedvpc/__init__.py b/tests/blueprints/cloud_operations/onprem_sa_key_management/__init__.py
similarity index 100%
rename from tests/examples/cloud_operations/vm_migration/host_target_sharedvpc/__init__.py
rename to tests/blueprints/cloud_operations/onprem_sa_key_management/__init__.py
diff --git a/tests/blueprints/cloud_operations/onprem_sa_key_management/fixture/main.tf b/tests/blueprints/cloud_operations/onprem_sa_key_management/fixture/main.tf
new file mode 100644
index 0000000000..29e473f024
--- /dev/null
+++ b/tests/blueprints/cloud_operations/onprem_sa_key_management/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/cloud-operations/onprem-sa-key-management"
+ project_create = var.project_create
+ project_id = var.project_id
+}
diff --git a/tests/examples/cloud_operations/onprem_sa_key_management/fixture/variables.tf b/tests/blueprints/cloud_operations/onprem_sa_key_management/fixture/variables.tf
similarity index 100%
rename from tests/examples/cloud_operations/onprem_sa_key_management/fixture/variables.tf
rename to tests/blueprints/cloud_operations/onprem_sa_key_management/fixture/variables.tf
diff --git a/tests/examples/cloud_operations/onprem_sa_key_management/test_plan.py b/tests/blueprints/cloud_operations/onprem_sa_key_management/test_plan.py
similarity index 100%
rename from tests/examples/cloud_operations/onprem_sa_key_management/test_plan.py
rename to tests/blueprints/cloud_operations/onprem_sa_key_management/test_plan.py
diff --git a/tests/examples/cloud_operations/vm_migration/single_project/__init__.py b/tests/blueprints/cloud_operations/packer_image_builder/__init__.py
similarity index 100%
rename from tests/examples/cloud_operations/vm_migration/single_project/__init__.py
rename to tests/blueprints/cloud_operations/packer_image_builder/__init__.py
diff --git a/tests/blueprints/cloud_operations/packer_image_builder/fixture/main.tf b/tests/blueprints/cloud_operations/packer_image_builder/fixture/main.tf
new file mode 100644
index 0000000000..6dec6b25bf
--- /dev/null
+++ b/tests/blueprints/cloud_operations/packer_image_builder/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/cloud-operations/packer-image-builder"
+ project_id = "test-project"
+ packer_account_users = ["user:john@testdomain.com"]
+ create_packer_vars = var.create_packer_vars
+}
diff --git a/tests/examples/cloud_operations/packer_image_builder/fixture/packer/build.pkrvars.tpl b/tests/blueprints/cloud_operations/packer_image_builder/fixture/packer/build.pkrvars.tpl
similarity index 100%
rename from tests/examples/cloud_operations/packer_image_builder/fixture/packer/build.pkrvars.tpl
rename to tests/blueprints/cloud_operations/packer_image_builder/fixture/packer/build.pkrvars.tpl
diff --git a/tests/examples/cloud_operations/packer_image_builder/fixture/variables.tf b/tests/blueprints/cloud_operations/packer_image_builder/fixture/variables.tf
similarity index 100%
rename from tests/examples/cloud_operations/packer_image_builder/fixture/variables.tf
rename to tests/blueprints/cloud_operations/packer_image_builder/fixture/variables.tf
diff --git a/tests/examples/cloud_operations/packer_image_builder/test_plan.py b/tests/blueprints/cloud_operations/packer_image_builder/test_plan.py
similarity index 100%
rename from tests/examples/cloud_operations/packer_image_builder/test_plan.py
rename to tests/blueprints/cloud_operations/packer_image_builder/test_plan.py
diff --git a/tests/examples/data_solutions/cmek_via_centralized_kms/__init__.py b/tests/blueprints/cloud_operations/quota_monitoring/__init__.py
similarity index 100%
rename from tests/examples/data_solutions/cmek_via_centralized_kms/__init__.py
rename to tests/blueprints/cloud_operations/quota_monitoring/__init__.py
diff --git a/tests/examples/cloud_operations/quota_monitoring/fixture/cf/README b/tests/blueprints/cloud_operations/quota_monitoring/fixture/cf/README
similarity index 100%
rename from tests/examples/cloud_operations/quota_monitoring/fixture/cf/README
rename to tests/blueprints/cloud_operations/quota_monitoring/fixture/cf/README
diff --git a/tests/blueprints/cloud_operations/quota_monitoring/fixture/main.tf b/tests/blueprints/cloud_operations/quota_monitoring/fixture/main.tf
new file mode 100644
index 0000000000..ef26ea0b73
--- /dev/null
+++ b/tests/blueprints/cloud_operations/quota_monitoring/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/cloud-operations/quota-monitoring"
+ name = var.name
+ project_create = var.project_create
+ project_id = var.project_id
+}
diff --git a/tests/examples/cloud_operations/quota_monitoring/fixture/variables.tf b/tests/blueprints/cloud_operations/quota_monitoring/fixture/variables.tf
similarity index 100%
rename from tests/examples/cloud_operations/quota_monitoring/fixture/variables.tf
rename to tests/blueprints/cloud_operations/quota_monitoring/fixture/variables.tf
diff --git a/tests/examples/cloud_operations/quota_monitoring/test_plan.py b/tests/blueprints/cloud_operations/quota_monitoring/test_plan.py
similarity index 100%
rename from tests/examples/cloud_operations/quota_monitoring/test_plan.py
rename to tests/blueprints/cloud_operations/quota_monitoring/test_plan.py
diff --git a/tests/examples/data_solutions/data_platform_foundations/__init__.py b/tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/__init__.py
similarity index 100%
rename from tests/examples/data_solutions/data_platform_foundations/__init__.py
rename to tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/__init__.py
diff --git a/tests/examples/cloud_operations/scheduled_asset_inventory_export_bq/fixture/bundle_cffile.zip b/tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/fixture/bundle_cffile.zip
similarity index 100%
rename from tests/examples/cloud_operations/scheduled_asset_inventory_export_bq/fixture/bundle_cffile.zip
rename to tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/fixture/bundle_cffile.zip
diff --git a/tests/examples/cloud_operations/scheduled_asset_inventory_export_bq/fixture/cf/README b/tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/fixture/cf/README
similarity index 100%
rename from tests/examples/cloud_operations/scheduled_asset_inventory_export_bq/fixture/cf/README
rename to tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/fixture/cf/README
diff --git a/tests/examples/cloud_operations/scheduled_asset_inventory_export_bq/fixture/cffile/README b/tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/fixture/cffile/README
similarity index 100%
rename from tests/examples/cloud_operations/scheduled_asset_inventory_export_bq/fixture/cffile/README
rename to tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/fixture/cffile/README
diff --git a/tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/fixture/main.tf b/tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/fixture/main.tf
new file mode 100644
index 0000000000..05d6325356
--- /dev/null
+++ b/tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/fixture/main.tf
@@ -0,0 +1,25 @@
+/**
+ * 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/scheduled-asset-inventory-export-bq"
+ billing_account = var.billing_account
+ cai_config = var.cai_config
+ cai_gcs_export = var.cai_gcs_export
+ file_config = var.file_config
+ project_create = var.project_create
+ project_id = var.project_id
+}
diff --git a/tests/examples/cloud_operations/scheduled_asset_inventory_export_bq/fixture/variables.tf b/tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/fixture/variables.tf
similarity index 100%
rename from tests/examples/cloud_operations/scheduled_asset_inventory_export_bq/fixture/variables.tf
rename to tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/fixture/variables.tf
diff --git a/tests/examples/cloud_operations/scheduled_asset_inventory_export_bq/test_plan.py b/tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/test_plan.py
similarity index 100%
rename from tests/examples/cloud_operations/scheduled_asset_inventory_export_bq/test_plan.py
rename to tests/blueprints/cloud_operations/scheduled_asset_inventory_export_bq/test_plan.py
diff --git a/tests/examples/data_solutions/gcs_to_bq_with_least_privileges/__init__.py b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/__init__.py
similarity index 100%
rename from tests/examples/data_solutions/gcs_to_bq_with_least_privileges/__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
new file mode 100644
index 0000000000..3552740c2a
--- /dev/null
+++ b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../../blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider"
+ billing_account = var.billing_account
+ project_create = var.project_create
+ project_id = var.project_id
+ parent = var.parent
+ tfe_organization_id = var.tfe_organization_id
+ tfe_workspace_id = var.tfe_workspace_id
+ workload_identity_pool_id = var.workload_identity_pool_id
+ workload_identity_pool_provider_id = var.workload_identity_pool_provider_id
+ issuer_uri = var.issuer_uri
+}
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
new file mode 100644
index 0000000000..d99981c0cf
--- /dev/null
+++ b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/variables.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
+#
+# 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.
+
+variable "billing_account" {
+ type = string
+ default = "1234-ABCD-1234"
+}
+
+variable "project_create" {
+ type = bool
+ default = true
+}
+
+variable "project_id" {
+ type = string
+ default = "project-1"
+}
+
+variable "parent" {
+ description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format."
+ type = string
+ default = null
+ validation {
+ condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent))
+ error_message = "Parent must be of the form folders/folder_id or organizations/organization_id."
+ }
+}
+
+variable "tfe_organization_id" {
+ description = "TFE organization id."
+ type = string
+ default = "org-123"
+}
+
+variable "tfe_workspace_id" {
+ description = "TFE workspace id."
+ type = string
+ default = "ws-123"
+}
+
+variable "workload_identity_pool_id" {
+ description = "Workload identity pool id."
+ type = string
+ default = "tfe-pool"
+}
+
+variable "workload_identity_pool_provider_id" {
+ description = "Workload identity pool provider id."
+ type = string
+ default = "tfe-provider"
+}
+
+variable "issuer_uri" {
+ description = "Terraform Enterprise uri. Replace the uri if a self hosted instance is used."
+ type = string
+ default = "https://app.terraform.io/"
+}
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
new file mode 100644
index 0000000000..228e51dfde
--- /dev/null
+++ b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/test_plan.py
@@ -0,0 +1,19 @@
+# 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(e2e_plan_runner):
+ "Test that plan works and the numbers of resources is as expected."
+ modules, resources = e2e_plan_runner()
+ assert len(modules) == 2
+ assert len(resources) == 10
diff --git a/tests/examples/factories/__init__.py b/tests/blueprints/cloud_operations/unmanaged_instances_healthcheck/__init__.py
similarity index 100%
rename from tests/examples/factories/__init__.py
rename to tests/blueprints/cloud_operations/unmanaged_instances_healthcheck/__init__.py
diff --git a/tests/blueprints/cloud_operations/unmanaged_instances_healthcheck/fixture/main.tf b/tests/blueprints/cloud_operations/unmanaged_instances_healthcheck/fixture/main.tf
new file mode 100644
index 0000000000..22b5da3c9c
--- /dev/null
+++ b/tests/blueprints/cloud_operations/unmanaged_instances_healthcheck/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/cloud-operations/unmanaged-instances-healthcheck"
+ billing_account = var.billing_account
+ project_create = var.project_create
+ project_id = var.project_id
+}
diff --git a/tests/examples/cloud_operations/unmanaged_instances_healthcheck/fixture/variables.tf b/tests/blueprints/cloud_operations/unmanaged_instances_healthcheck/fixture/variables.tf
similarity index 100%
rename from tests/examples/cloud_operations/unmanaged_instances_healthcheck/fixture/variables.tf
rename to tests/blueprints/cloud_operations/unmanaged_instances_healthcheck/fixture/variables.tf
diff --git a/tests/blueprints/cloud_operations/unmanaged_instances_healthcheck/test_plan.py b/tests/blueprints/cloud_operations/unmanaged_instances_healthcheck/test_plan.py
new file mode 100644
index 0000000000..c79be049e8
--- /dev/null
+++ b/tests/blueprints/cloud_operations/unmanaged_instances_healthcheck/test_plan.py
@@ -0,0 +1,19 @@
+# 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(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) == 31
diff --git a/tests/examples/factories/net_vpc_firewall_yaml/__init__.py b/tests/blueprints/cloud_operations/vm_migration/host_target_projects/__init__.py
similarity index 100%
rename from tests/examples/factories/net_vpc_firewall_yaml/__init__.py
rename to tests/blueprints/cloud_operations/vm_migration/host_target_projects/__init__.py
diff --git a/tests/blueprints/cloud_operations/vm_migration/host_target_projects/fixture/main.tf b/tests/blueprints/cloud_operations/vm_migration/host_target_projects/fixture/main.tf
new file mode 100644
index 0000000000..488dd7ccf6
--- /dev/null
+++ b/tests/blueprints/cloud_operations/vm_migration/host_target_projects/fixture/main.tf
@@ -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
+#
+# 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.
+
+module "host-target-projects-test" {
+ source = "../../../../../../blueprints/cloud-operations/vm-migration/host-target-projects"
+ project_create = var.project_create
+ migration_admin_users = ["user:admin@example.com"]
+ migration_viewer_users = ["user:viewer@example.com"]
+ migration_target_projects = ["${module.test-target-project.name}"]
+ depends_on = [
+ module.test-target-project
+ ]
+}
+
+variable "project_create" {
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = {
+ billing_account_id = "1234-ABCD-1234"
+ parent = "folders/1234563"
+ }
+}
+
+#This is a dummy project created to run this test. The example, here tested, is expected to run on top of existing foundations.
+module "test-target-project" {
+ source = "../../../../../../modules/project"
+ billing_account = "1234-ABCD-1234"
+ name = "test-target-project"
+ project_create = true
+}
diff --git a/tests/examples/cloud_operations/vm_migration/host_target_projects/test_plan.py b/tests/blueprints/cloud_operations/vm_migration/host_target_projects/test_plan.py
similarity index 100%
rename from tests/examples/cloud_operations/vm_migration/host_target_projects/test_plan.py
rename to tests/blueprints/cloud_operations/vm_migration/host_target_projects/test_plan.py
diff --git a/tests/examples/factories/project_factory/__init__.py b/tests/blueprints/cloud_operations/vm_migration/host_target_sharedvpc/__init__.py
similarity index 100%
rename from tests/examples/factories/project_factory/__init__.py
rename to tests/blueprints/cloud_operations/vm_migration/host_target_sharedvpc/__init__.py
diff --git a/tests/blueprints/cloud_operations/vm_migration/host_target_sharedvpc/fixture/main.tf b/tests/blueprints/cloud_operations/vm_migration/host_target_sharedvpc/fixture/main.tf
new file mode 100644
index 0000000000..f8bea07c08
--- /dev/null
+++ b/tests/blueprints/cloud_operations/vm_migration/host_target_sharedvpc/fixture/main.tf
@@ -0,0 +1,51 @@
+# 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.
+
+module "host-target-sharedvpc-test" {
+ source = "../../../../../../blueprints/cloud-operations/vm-migration/host-target-sharedvpc"
+ project_create = var.project_create
+ migration_admin_users = ["user:admin@example.com"]
+ migration_viewer_users = ["user:viewer@example.com"]
+ migration_target_projects = [module.test-target-project.name]
+ sharedvpc_host_projects = [module.test-sharedvpc-host-project.name]
+ depends_on = [
+ module.test-target-project,
+ module.test-sharedvpc-host-project,
+ ]
+}
+
+variable "project_create" {
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = {
+ billing_account_id = "1234-ABCD-1234"
+ parent = "folders/1234563"
+ }
+}
+
+#These are a dummy projects created to run this test. The example, here tested, is expected to run on top of existing foundations.
+module "test-target-project" {
+ source = "../../../../../../modules/project"
+ billing_account = "1234-ABCD-1234"
+ name = "test-target-project"
+ project_create = true
+}
+module "test-sharedvpc-host-project" {
+ source = "../../../../../../modules/project"
+ billing_account = "1234-ABCD-1234"
+ name = "test-sharedvpc-host-project"
+ project_create = true
+}
diff --git a/tests/examples/cloud_operations/vm_migration/host_target_sharedvpc/test_plan.py b/tests/blueprints/cloud_operations/vm_migration/host_target_sharedvpc/test_plan.py
similarity index 100%
rename from tests/examples/cloud_operations/vm_migration/host_target_sharedvpc/test_plan.py
rename to tests/blueprints/cloud_operations/vm_migration/host_target_sharedvpc/test_plan.py
diff --git a/tests/examples/foundations/__init__.py b/tests/blueprints/cloud_operations/vm_migration/single_project/__init__.py
similarity index 100%
rename from tests/examples/foundations/__init__.py
rename to tests/blueprints/cloud_operations/vm_migration/single_project/__init__.py
diff --git a/tests/blueprints/cloud_operations/vm_migration/single_project/fixture/main.tf b/tests/blueprints/cloud_operations/vm_migration/single_project/fixture/main.tf
new file mode 100644
index 0000000000..ccb46d5416
--- /dev/null
+++ b/tests/blueprints/cloud_operations/vm_migration/single_project/fixture/main.tf
@@ -0,0 +1,31 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# 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.
+
+module "single-project-test" {
+ source = "../../../../../../blueprints/cloud-operations/vm-migration/single-project"
+ project_create = var.project_create
+ migration_admin_users = ["user:admin@example.com"]
+ migration_viewer_users = ["user:viewer@example.com"]
+}
+
+variable "project_create" {
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = {
+ billing_account_id = "1234-ABCD-1234"
+ parent = "folders/1234563"
+ }
+}
diff --git a/tests/blueprints/cloud_operations/vm_migration/single_project/test_plan.py b/tests/blueprints/cloud_operations/vm_migration/single_project/test_plan.py
new file mode 100644
index 0000000000..6e2c141da5
--- /dev/null
+++ b/tests/blueprints/cloud_operations/vm_migration/single_project/test_plan.py
@@ -0,0 +1,24 @@
+# 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
+
+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) == 4
+ assert len(resources) == 20
diff --git a/tests/examples/foundations/business_units/__init__.py b/tests/blueprints/data_solutions/__init__.py
similarity index 100%
rename from tests/examples/foundations/business_units/__init__.py
rename to tests/blueprints/data_solutions/__init__.py
diff --git a/tests/examples/foundations/environments/__init__.py b/tests/blueprints/data_solutions/cloudsql_multiregion/__init__.py
similarity index 100%
rename from tests/examples/foundations/environments/__init__.py
rename to tests/blueprints/data_solutions/cloudsql_multiregion/__init__.py
diff --git a/tests/blueprints/data_solutions/cloudsql_multiregion/fixture/main.tf b/tests/blueprints/data_solutions/cloudsql_multiregion/fixture/main.tf
new file mode 100644
index 0000000000..3d71614976
--- /dev/null
+++ b/tests/blueprints/data_solutions/cloudsql_multiregion/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/data-solutions/cloudsql-multiregion/"
+ data_eng_principals = ["dataeng@example.com"]
+ postgres_user_password = "my-root-password"
+ project_id = "project"
+ project_create = {
+ billing_account_id = "123456-123456-123456"
+ parent = "folders/12345678"
+ }
+ prefix = "prefix"
+}
diff --git a/tests/blueprints/data_solutions/cloudsql_multiregion/test_plan.py b/tests/blueprints/data_solutions/cloudsql_multiregion/test_plan.py
new file mode 100644
index 0000000000..90371cf7c7
--- /dev/null
+++ b/tests/blueprints/data_solutions/cloudsql_multiregion/test_plan.py
@@ -0,0 +1,19 @@
+# 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(e2e_plan_runner):
+ "Test that plan works and the numbers of resources is as expected."
+ modules, resources = e2e_plan_runner()
+ assert len(modules) == 9
+ assert len(resources) == 48
diff --git a/tests/examples/networking/__init__.py b/tests/blueprints/data_solutions/cmek_via_centralized_kms/__init__.py
similarity index 100%
rename from tests/examples/networking/__init__.py
rename to tests/blueprints/data_solutions/cmek_via_centralized_kms/__init__.py
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
new file mode 100644
index 0000000000..65cc20aeb2
--- /dev/null
+++ b/tests/blueprints/data_solutions/cmek_via_centralized_kms/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/data-solutions/cmek-via-centralized-kms/"
+ billing_account = var.billing_account
+ root_node = var.root_node
+}
diff --git a/tests/examples/data_solutions/cmek_via_centralized_kms/fixture/variables.tf b/tests/blueprints/data_solutions/cmek_via_centralized_kms/fixture/variables.tf
similarity index 100%
rename from tests/examples/data_solutions/cmek_via_centralized_kms/fixture/variables.tf
rename to tests/blueprints/data_solutions/cmek_via_centralized_kms/fixture/variables.tf
diff --git a/tests/examples/data_solutions/cmek_via_centralized_kms/test_plan.py b/tests/blueprints/data_solutions/cmek_via_centralized_kms/test_plan.py
similarity index 100%
rename from tests/examples/data_solutions/cmek_via_centralized_kms/test_plan.py
rename to tests/blueprints/data_solutions/cmek_via_centralized_kms/test_plan.py
diff --git a/tests/examples/networking/decentralized_firewall/__init__.py b/tests/blueprints/data_solutions/composer_2/__init__.py
similarity index 100%
rename from tests/examples/networking/decentralized_firewall/__init__.py
rename to tests/blueprints/data_solutions/composer_2/__init__.py
diff --git a/tests/blueprints/data_solutions/composer_2/fixture/main.tf b/tests/blueprints/data_solutions/composer_2/fixture/main.tf
new file mode 100644
index 0000000000..4b35e6f80e
--- /dev/null
+++ b/tests/blueprints/data_solutions/composer_2/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/data-solutions/composer-2/"
+ project_id = "project"
+
+ project_create = {
+ billing_account_id = "123456-123456-123456"
+ parent = "folders/12345678"
+ }
+ prefix = "prefix"
+}
diff --git a/tests/blueprints/data_solutions/composer_2/test_plan.py b/tests/blueprints/data_solutions/composer_2/test_plan.py
new file mode 100644
index 0000000000..04f4a39f71
--- /dev/null
+++ b/tests/blueprints/data_solutions/composer_2/test_plan.py
@@ -0,0 +1,19 @@
+# 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(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) == 25
diff --git a/tests/examples/networking/filtering_proxy/__init__.py b/tests/blueprints/data_solutions/data_platform_foundations/__init__.py
similarity index 100%
rename from tests/examples/networking/filtering_proxy/__init__.py
rename to tests/blueprints/data_solutions/data_platform_foundations/__init__.py
diff --git a/tests/blueprints/data_solutions/data_platform_foundations/fixture/main.tf b/tests/blueprints/data_solutions/data_platform_foundations/fixture/main.tf
new file mode 100644
index 0000000000..52317d6f5f
--- /dev/null
+++ b/tests/blueprints/data_solutions/data_platform_foundations/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/data-solutions/data-platform-foundations/"
+ organization_domain = "example.com"
+ billing_account_id = "123456-123456-123456"
+ folder_id = "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
new file mode 100644
index 0000000000..1b51472cdc
--- /dev/null
+++ b/tests/blueprints/data_solutions/data_platform_foundations/test_plan.py
@@ -0,0 +1,25 @@
+# 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)
+ assert len(modules) == 41
+ assert len(resources) == 315
diff --git a/tests/examples/networking/hub_and_spoke_peering/__init__.py b/tests/blueprints/data_solutions/data_playground/__init__.py
similarity index 100%
rename from tests/examples/networking/hub_and_spoke_peering/__init__.py
rename to tests/blueprints/data_solutions/data_playground/__init__.py
diff --git a/tests/blueprints/data_solutions/data_playground/fixture/main.tf b/tests/blueprints/data_solutions/data_playground/fixture/main.tf
new file mode 100644
index 0000000000..e9e1d29798
--- /dev/null
+++ b/tests/blueprints/data_solutions/data_playground/fixture/main.tf
@@ -0,0 +1,25 @@
+/**
+ * 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/data-solutions/data-playground/"
+ project_id = "sampleproject"
+ prefix = "tst"
+ project_create = {
+ billing_account_id = "123456-123456-123456",
+ parent = "folders/467898377"
+ }
+}
diff --git a/tests/blueprints/data_solutions/data_playground/test_plan.py b/tests/blueprints/data_solutions/data_playground/test_plan.py
new file mode 100644
index 0000000000..a0c3b5e6fe
--- /dev/null
+++ b/tests/blueprints/data_solutions/data_playground/test_plan.py
@@ -0,0 +1,25 @@
+# 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)
+ assert len(modules) == 7
+ assert len(resources) == 37
diff --git a/tests/examples/networking/hub_and_spoke_vpn/__init__.py b/tests/blueprints/data_solutions/gcs_to_bq_with_least_privileges/__init__.py
similarity index 100%
rename from tests/examples/networking/hub_and_spoke_vpn/__init__.py
rename to tests/blueprints/data_solutions/gcs_to_bq_with_least_privileges/__init__.py
diff --git a/tests/blueprints/data_solutions/gcs_to_bq_with_least_privileges/fixture/main.tf b/tests/blueprints/data_solutions/gcs_to_bq_with_least_privileges/fixture/main.tf
new file mode 100644
index 0000000000..4fc83c755b
--- /dev/null
+++ b/tests/blueprints/data_solutions/gcs_to_bq_with_least_privileges/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/data-solutions/gcs-to-bq-with-least-privileges/"
+ project_create = var.project_create
+ project_id = var.project_id
+ prefix = var.prefix
+}
diff --git a/tests/examples/data_solutions/gcs_to_bq_with_least_privileges/fixture/variables.tf b/tests/blueprints/data_solutions/gcs_to_bq_with_least_privileges/fixture/variables.tf
similarity index 100%
rename from tests/examples/data_solutions/gcs_to_bq_with_least_privileges/fixture/variables.tf
rename to tests/blueprints/data_solutions/gcs_to_bq_with_least_privileges/fixture/variables.tf
diff --git a/tests/blueprints/data_solutions/gcs_to_bq_with_least_privileges/test_plan.py b/tests/blueprints/data_solutions/gcs_to_bq_with_least_privileges/test_plan.py
new file mode 100644
index 0000000000..773e80ca17
--- /dev/null
+++ b/tests/blueprints/data_solutions/gcs_to_bq_with_least_privileges/test_plan.py
@@ -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.
+
+
+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)
+ assert len(modules) == 11
+ assert len(resources) == 47
diff --git a/tests/examples/networking/ilb_next_hop/__init__.py b/tests/blueprints/data_solutions/sqlserver_alwayson/__init__.py
similarity index 100%
rename from tests/examples/networking/ilb_next_hop/__init__.py
rename to tests/blueprints/data_solutions/sqlserver_alwayson/__init__.py
diff --git a/tests/blueprints/data_solutions/sqlserver_alwayson/fixture/main.tf b/tests/blueprints/data_solutions/sqlserver_alwayson/fixture/main.tf
new file mode 100644
index 0000000000..72f7a7d337
--- /dev/null
+++ b/tests/blueprints/data_solutions/sqlserver_alwayson/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/data-solutions/sqlserver-alwayson/"
+ project_create = var.project_create
+ project_id = var.project_id
+ prefix = var.prefix
+ network = "example-network"
+ subnetwork = "example-subnetwork"
+ sql_admin_password = "password"
+ ad_domain_fqdn = "ad.example.com"
+ ad_domain_netbios = "ad"
+}
diff --git a/tests/blueprints/data_solutions/sqlserver_alwayson/fixture/variables.tf b/tests/blueprints/data_solutions/sqlserver_alwayson/fixture/variables.tf
new file mode 100644
index 0000000000..e2c50ac27c
--- /dev/null
+++ b/tests/blueprints/data_solutions/sqlserver_alwayson/fixture/variables.tf
@@ -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.
+ */
+
+variable "prefix" {
+ description = "Unique prefix used for resource names."
+ type = string
+ default = "test"
+}
+
+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 = {
+ billing_account_id = "123456-123456-123456"
+ parent = "folders/12345678"
+ }
+}
+
+variable "project_id" {
+ description = "Project id, references existing project if `project_create` is null."
+ type = string
+ default = "sqlserver"
+}
diff --git a/tests/blueprints/data_solutions/sqlserver_alwayson/test_plan.py b/tests/blueprints/data_solutions/sqlserver_alwayson/test_plan.py
new file mode 100644
index 0000000000..863148007d
--- /dev/null
+++ b/tests/blueprints/data_solutions/sqlserver_alwayson/test_plan.py
@@ -0,0 +1,25 @@
+# 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)
+ assert len(modules) == 11
+ assert len(resources) == 38
diff --git a/tests/examples/networking/onprem_google_access_dns/__init__.py b/tests/blueprints/factories/__init__.py
similarity index 100%
rename from tests/examples/networking/onprem_google_access_dns/__init__.py
rename to tests/blueprints/factories/__init__.py
diff --git a/tests/examples/networking/private_cloud_function_from_onprem/__init__.py b/tests/blueprints/factories/bigquery_factory/__init__.py
similarity index 100%
rename from tests/examples/networking/private_cloud_function_from_onprem/__init__.py
rename to tests/blueprints/factories/bigquery_factory/__init__.py
diff --git a/tests/blueprints/factories/bigquery_factory/fixture/main.tf b/tests/blueprints/factories/bigquery_factory/fixture/main.tf
new file mode 100644
index 0000000000..75f4fc1c5e
--- /dev/null
+++ b/tests/blueprints/factories/bigquery_factory/fixture/main.tf
@@ -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.
+ */
+
+module "bq" {
+ source = "../../../../../blueprints/factories/bigquery-factory/"
+
+ project_id = "test-project"
+ views_dir = "./views"
+ tables_dir = "./tables"
+}
diff --git a/tests/blueprints/factories/bigquery_factory/fixture/tables/table_a.yaml b/tests/blueprints/factories/bigquery_factory/fixture/tables/table_a.yaml
new file mode 100644
index 0000000000..05adbcb023
--- /dev/null
+++ b/tests/blueprints/factories/bigquery_factory/fixture/tables/table_a.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.
+
+dataset: dataset_a
+table: table_a
+schema: [{name: "test", type: "STRING"},{name: "test2", type: "INT64"}]
diff --git a/tests/blueprints/factories/bigquery_factory/fixture/variables.tf b/tests/blueprints/factories/bigquery_factory/fixture/variables.tf
new file mode 100644
index 0000000000..8269dbbe15
--- /dev/null
+++ b/tests/blueprints/factories/bigquery_factory/fixture/variables.tf
@@ -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.
+ */
+
+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/factories/bigquery_factory/fixture/views/view_a.yaml b/tests/blueprints/factories/bigquery_factory/fixture/views/view_a.yaml
new file mode 100644
index 0000000000..23c41b98f1
--- /dev/null
+++ b/tests/blueprints/factories/bigquery_factory/fixture/views/view_a.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.
+
+dataset: dataset_b
+view: view_a
+query: "SELECT CURRENT_DATE() LIMIT 1"
diff --git a/tests/blueprints/factories/bigquery_factory/test_plan.py b/tests/blueprints/factories/bigquery_factory/test_plan.py
new file mode 100644
index 0000000000..74705e423e
--- /dev/null
+++ b/tests/blueprints/factories/bigquery_factory/test_plan.py
@@ -0,0 +1,19 @@
+# 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(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
diff --git a/tests/examples/networking/shared_vpc_gke/__init__.py b/tests/blueprints/factories/cloud_identity_group_factory/__init__.py
similarity index 100%
rename from tests/examples/networking/shared_vpc_gke/__init__.py
rename to tests/blueprints/factories/cloud_identity_group_factory/__init__.py
diff --git a/tests/blueprints/factories/cloud_identity_group_factory/fixture/data/group1@example.com.yaml b/tests/blueprints/factories/cloud_identity_group_factory/fixture/data/group1@example.com.yaml
new file mode 100644
index 0000000000..98bdcb8e1e
--- /dev/null
+++ b/tests/blueprints/factories/cloud_identity_group_factory/fixture/data/group1@example.com.yaml
@@ -0,0 +1,8 @@
+# skip boilerplate check
+
+display_name: Group 1
+description: Group 1
+members:
+ - user1@example.com
+managers:
+ - user2@example.com
\ No newline at end of file
diff --git a/tests/blueprints/factories/cloud_identity_group_factory/fixture/main.tf b/tests/blueprints/factories/cloud_identity_group_factory/fixture/main.tf
new file mode 100644
index 0000000000..4f56c63c22
--- /dev/null
+++ b/tests/blueprints/factories/cloud_identity_group_factory/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/factories/cloud-identity-group-factory/"
+ customer_id = "customers/C01234567"
+ data_dir = "data"
+}
diff --git a/tests/blueprints/factories/cloud_identity_group_factory/test_plan.py b/tests/blueprints/factories/cloud_identity_group_factory/test_plan.py
new file mode 100644
index 0000000000..7de10b1a5f
--- /dev/null
+++ b/tests/blueprints/factories/cloud_identity_group_factory/test_plan.py
@@ -0,0 +1,19 @@
+# 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(e2e_plan_runner):
+ "Test that plan works and the numbers of resources is as expected."
+ modules, resources = e2e_plan_runner()
+ assert len(modules) == 1
+ assert len(resources) == 3
diff --git a/tests/examples/serverless/api_gateway/__init__.py b/tests/blueprints/factories/net_vpc_firewall_yaml/__init__.py
similarity index 100%
rename from tests/examples/serverless/api_gateway/__init__.py
rename to tests/blueprints/factories/net_vpc_firewall_yaml/__init__.py
diff --git a/tests/blueprints/factories/net_vpc_firewall_yaml/fixture/main.tf b/tests/blueprints/factories/net_vpc_firewall_yaml/fixture/main.tf
new file mode 100644
index 0000000000..22956f4018
--- /dev/null
+++ b/tests/blueprints/factories/net_vpc_firewall_yaml/fixture/main.tf
@@ -0,0 +1,25 @@
+/**
+ * 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 = "../../../../../blueprints/factories/net-vpc-firewall-yaml"
+ project_id = "my-project"
+ network = "my-network"
+ config_directories = [
+ "./rules"
+ ]
+ log_config = var.log_config
+}
diff --git a/tests/examples/factories/net_vpc_firewall_yaml/fixture/rules/common.yaml b/tests/blueprints/factories/net_vpc_firewall_yaml/fixture/rules/common.yaml
similarity index 100%
rename from tests/examples/factories/net_vpc_firewall_yaml/fixture/rules/common.yaml
rename to tests/blueprints/factories/net_vpc_firewall_yaml/fixture/rules/common.yaml
diff --git a/tests/examples/factories/net_vpc_firewall_yaml/fixture/variables.tf b/tests/blueprints/factories/net_vpc_firewall_yaml/fixture/variables.tf
similarity index 100%
rename from tests/examples/factories/net_vpc_firewall_yaml/fixture/variables.tf
rename to tests/blueprints/factories/net_vpc_firewall_yaml/fixture/variables.tf
diff --git a/tests/examples/factories/net_vpc_firewall_yaml/test_plan.py b/tests/blueprints/factories/net_vpc_firewall_yaml/test_plan.py
similarity index 100%
rename from tests/examples/factories/net_vpc_firewall_yaml/test_plan.py
rename to tests/blueprints/factories/net_vpc_firewall_yaml/test_plan.py
diff --git a/tests/fast/stages/s03_data_platform/fixture/__init__.py b/tests/blueprints/factories/project_factory/__init__.py
similarity index 100%
rename from tests/fast/stages/s03_data_platform/fixture/__init__.py
rename to tests/blueprints/factories/project_factory/__init__.py
diff --git a/tests/blueprints/factories/project_factory/fixture/defaults.yaml b/tests/blueprints/factories/project_factory/fixture/defaults.yaml
new file mode 100644
index 0000000000..61837818f1
--- /dev/null
+++ b/tests/blueprints/factories/project_factory/fixture/defaults.yaml
@@ -0,0 +1,25 @@
+# skip boilerplate check
+
+billing_account_id: 012345-67890A-BCDEF0
+
+# [opt] Setup for billing alerts
+billing_alert:
+ amount: 1000
+ thresholds:
+ current: [0.5, 0.8]
+ forecasted: [0.5, 0.8]
+ credit_treatment: INCLUDE_ALL_CREDITS
+
+# [opt] Contacts for billing alerts and important notifications
+essential_contacts: ["team-contacts@example.com"]
+
+# [opt] Labels set for all projects
+labels:
+ environment: prod
+ department: accounting
+ application: example-app
+ foo: bar
+
+# [opt] Additional notification channels for billing
+notification_channels: []
+prefix: test
diff --git a/tests/blueprints/factories/project_factory/fixture/main.tf b/tests/blueprints/factories/project_factory/fixture/main.tf
new file mode 100644
index 0000000000..ae686b9350
--- /dev/null
+++ b/tests/blueprints/factories/project_factory/fixture/main.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.
+ */
+
+locals {
+ _defaults = yamldecode(file(var.defaults_file))
+ _defaults_net = {
+ billing_account_id = var.billing_account_id
+ environment_dns_zone = var.environment_dns_zone
+ shared_vpc_self_link = var.shared_vpc_self_link
+ vpc_host_project = var.vpc_host_project
+ }
+ defaults = merge(local._defaults, local._defaults_net)
+ projects = {
+ for f in fileset("${var.data_dir}", "**/*.yaml") :
+ trimsuffix(f, ".yaml") => yamldecode(file("${var.data_dir}/${f}"))
+ }
+}
+
+module "projects" {
+ source = "../../../../../blueprints/factories/project-factory"
+ for_each = local.projects
+ defaults = local.defaults
+ project_id = each.key
+ 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, [])
+ essential_contacts = try(each.value.essential_contacts, [])
+ folder_id = each.value.folder_id
+ group_iam = try(each.value.group_iam, {})
+ iam = try(each.value.iam, {})
+ kms_service_agents = try(each.value.kms, {})
+ labels = try(each.value.labels, {})
+ org_policies = try(each.value.org_policies, null)
+ prefix = each.value.prefix
+ service_accounts = try(each.value.service_accounts, {})
+ services = try(each.value.services, [])
+ service_identities_iam = try(each.value.service_identities_iam, {})
+ vpc = try(each.value.vpc, null)
+}
diff --git a/tests/blueprints/factories/project_factory/fixture/projects/project.yaml b/tests/blueprints/factories/project_factory/fixture/projects/project.yaml
new file mode 100644
index 0000000000..a158198484
--- /dev/null
+++ b/tests/blueprints/factories/project_factory/fixture/projects/project.yaml
@@ -0,0 +1,104 @@
+# skip boilerplate check
+
+# [opt] Billing account id - overrides default if set
+billing_account_id: 012345-67890A-BCDEF0
+
+# [opt] Billing alerts config - overrides default if set
+billing_alert:
+ amount: 10
+ thresholds:
+ current:
+ - 0.5
+ - 0.8
+ forecasted: []
+ credit_treatment: INCLUDE_ALL_CREDITS
+
+# [opt] DNS zones to be created as children of the environment_dns_zone defined in defaults
+dns_zones:
+ - lorem
+ - ipsum
+
+# [opt] Contacts for billing alerts and important notifications
+essential_contacts:
+ - team-a-contacts@example.com
+
+# Folder the project will be created as children of
+folder_id: folders/012345678901
+
+# [opt] Authoritative IAM bindings in group => [roles] format
+group_iam:
+ test-team-foobar@fast-lab-0.gcp-pso-italy.net:
+ - roles/compute.admin
+
+# [opt] Authoritative IAM bindings in role => [principals] format
+# Generally used to grant roles to service accounts external to the project
+iam:
+ roles/compute.admin:
+ - serviceAccount:service-account
+
+# [opt] Service robots and keys they will be assigned as cryptoKeyEncrypterDecrypter
+# in service => [keys] format
+kms_service_agents:
+ compute: [key1, key2]
+ storage: [key1, key2]
+
+# [opt] Labels for the project - merged with the ones defined in defaults
+labels:
+ environment: prod
+
+# [opt] Org policy overrides defined at project level
+org_policies:
+ policy_boolean:
+ constraints/compute.disableGuestAttributesAccess: true
+ policy_list:
+ constraints/compute.trustedImageProjects:
+ inherit_from_parent: null
+ status: true
+ suggested_value: null
+ values:
+ - projects/fast-prod-iac-core-0
+
+# [opt] Prefix - overrides default if set
+prefix: test1
+
+# [opt] Service account to create for the project and their roles on the project
+# in name => [roles] format
+service_accounts:
+ another-service-account:
+ - roles/compute.admin
+ my-service-account:
+ - roles/compute.admin
+
+# [opt] APIs to enable on the project.
+services:
+ - storage.googleapis.com
+ - stackdriver.googleapis.com
+ - compute.googleapis.com
+
+# [opt] Roles to assign to the service identities in service => [roles] format
+service_identities_iam:
+ compute:
+ - roles/storage.objectViewer
+
+ # [opt] VPC setup.
+ # If set enables the `compute.googleapis.com` service and configures
+ # service project attachment
+vpc:
+ # [opt] If set, enables the container API
+ gke_setup:
+ # Grants "roles/container.hostServiceAgentUser" to the container robot if set
+ enable_host_service_agent: false
+
+ # Grants "roles/compute.securityAdmin" to the container robot if set
+ enable_security_admin: true
+
+ # Host project the project will be service project of
+ host_project: fast-prod-net-spoke-0
+
+ # [opt] Subnets in the host project where principals will be granted networkUser
+ # in region/subnet-name => [principals]
+ subnets_iam:
+ europe-west1/prod-default-ew1:
+ - user:foobar@example.com
+ - serviceAccount:service-account1@example.com
+ - my-service-account
diff --git a/tests/blueprints/factories/project_factory/fixture/variables.tf b/tests/blueprints/factories/project_factory/fixture/variables.tf
new file mode 100644
index 0000000000..d0d6759bad
--- /dev/null
+++ b/tests/blueprints/factories/project_factory/fixture/variables.tf
@@ -0,0 +1,64 @@
+/**
+ * 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_id" {
+ description = "Billing account id."
+ type = string
+ default = "012345-67890A-BCDEF0"
+}
+
+variable "data_dir" {
+ description = "Relative path for the folder storing configuration data."
+ type = string
+ default = "./projects/"
+}
+
+variable "environment_dns_zone" {
+ description = "DNS zone suffix for environment."
+ type = string
+ default = "prod.gcp.example.com"
+}
+
+variable "defaults_file" {
+ description = "Relative path for the file storing the project factory configuration."
+ type = string
+ default = "./defaults.yaml"
+}
+
+variable "service_accounts" {
+ description = "Service accounts to be created, and roles assigned them on the project."
+ type = map(list(string))
+ default = {}
+}
+
+variable "service_accounts_iam" {
+ description = "IAM bindings on service account resources. Format is KEY => {ROLE => [MEMBERS]}"
+ type = map(map(list(string)))
+ default = {}
+ nullable = false
+}
+
+variable "shared_vpc_self_link" {
+ description = "Self link for the shared VPC."
+ type = string
+ default = "self-link"
+}
+
+variable "vpc_host_project" {
+ description = "Host project for the shared VPC."
+ type = string
+ default = "host-project"
+}
diff --git a/tests/blueprints/factories/project_factory/test_plan.py b/tests/blueprints/factories/project_factory/test_plan.py
new file mode 100644
index 0000000000..4c8e86412c
--- /dev/null
+++ b/tests/blueprints/factories/project_factory/test_plan.py
@@ -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.
+
+
+def test_plan(e2e_plan_runner):
+ "Check for a clean plan"
+ modules, resources = e2e_plan_runner()
+ assert len(modules) > 0 and len(resources) > 0
+
+
+def test_plan_service_accounts(e2e_plan_runner):
+ "Check for a clean plan"
+ service_accounts = '''{
+ sa-001 = []
+ sa-002 = ["roles/owner"]
+ }'''
+ service_accounts_iam = '''{
+ sa-002 = {
+ "roles/iam.serviceAccountTokenCreator" = ["group:team-1@example.com"]
+ }
+ }'''
+ modules, resources = e2e_plan_runner(
+ service_accounts=service_accounts,
+ service_accounts_iam=service_accounts_iam)
+ assert len(modules) > 0 and len(resources) > 0
diff --git a/tests/modules/apigee_organization/__init__.py b/tests/blueprints/gke/__init__.py
similarity index 100%
rename from tests/modules/apigee_organization/__init__.py
rename to tests/blueprints/gke/__init__.py
diff --git a/tests/modules/apigee_x_instance/__init__.py b/tests/blueprints/gke/binauthz/__init__.py
similarity index 100%
rename from tests/modules/apigee_x_instance/__init__.py
rename to tests/blueprints/gke/binauthz/__init__.py
diff --git a/tests/blueprints/gke/binauthz/fixture/main.tf b/tests/blueprints/gke/binauthz/fixture/main.tf
new file mode 100644
index 0000000000..23e1504b84
--- /dev/null
+++ b/tests/blueprints/gke/binauthz/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/gke/binauthz"
+ prefix = var.prefix
+ project_create = var.project_create
+ project_id = var.project_id
+}
diff --git a/tests/blueprints/gke/binauthz/fixture/variables.tf b/tests/blueprints/gke/binauthz/fixture/variables.tf
new file mode 100644
index 0000000000..8a09c75eba
--- /dev/null
+++ b/tests/blueprints/gke/binauthz/fixture/variables.tf
@@ -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
+#
+# 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.
+
+variable "project_create" {
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = {
+ billing_account_id = "12345-12345-12345"
+ parent = "folders/123456789"
+ }
+}
+
+variable "project_id" {
+ type = string
+ default = "my-project"
+}
+
+variable "prefix" {
+ type = string
+ default = "test"
+}
diff --git a/tests/blueprints/gke/binauthz/test_plan.py b/tests/blueprints/gke/binauthz/test_plan.py
new file mode 100644
index 0000000000..cf012c0615
--- /dev/null
+++ b/tests/blueprints/gke/binauthz/test_plan.py
@@ -0,0 +1,19 @@
+# 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(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
diff --git a/tests/modules/naming_convention/__init__.py b/tests/blueprints/gke/multi_cluster_mesh_gke_fleet_api/__init__.py
similarity index 100%
rename from tests/modules/naming_convention/__init__.py
rename to tests/blueprints/gke/multi_cluster_mesh_gke_fleet_api/__init__.py
diff --git a/tests/blueprints/gke/multi_cluster_mesh_gke_fleet_api/fixture/main.tf b/tests/blueprints/gke/multi_cluster_mesh_gke_fleet_api/fixture/main.tf
new file mode 100644
index 0000000000..47524fa526
--- /dev/null
+++ b/tests/blueprints/gke/multi_cluster_mesh_gke_fleet_api/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/gke/multi-cluster-mesh-gke-fleet-api"
+ billing_account_id = var.billing_account_id
+ parent = var.parent
+ host_project_id = var.host_project_id
+ fleet_project_id = var.fleet_project_id
+ mgmt_project_id = var.mgmt_project_id
+ region = var.region
+ clusters_config = var.clusters_config
+ mgmt_subnet_cidr_block = var.mgmt_subnet_cidr_block
+ istio_version = var.istio_version
+}
diff --git a/tests/blueprints/gke/multi_cluster_mesh_gke_fleet_api/fixture/variables.tf b/tests/blueprints/gke/multi_cluster_mesh_gke_fleet_api/fixture/variables.tf
new file mode 100644
index 0000000000..6c6b6c8fba
--- /dev/null
+++ b/tests/blueprints/gke/multi_cluster_mesh_gke_fleet_api/fixture/variables.tf
@@ -0,0 +1,107 @@
+/**
+ * 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_id" {
+ description = "Billing account id."
+ type = string
+ default = "123-456-789"
+}
+
+variable "parent" {
+ description = "Parent."
+ type = string
+ default = "folders/123456789"
+}
+
+variable "host_project_id" {
+ description = "Host project ID."
+ type = string
+ default = "my-host-project"
+}
+
+variable "fleet_project_id" {
+ description = "Fleet project ID."
+ type = string
+ default = "my-fleet-project"
+}
+
+variable "mgmt_project_id" {
+ description = "Management Project ID."
+ type = string
+ default = "my-mgmt-project"
+}
+
+variable "mgmt_subnet_cidr_block" {
+ description = "Management subnet CIDR block."
+ type = string
+ default = "10.0.0.0/24"
+}
+
+variable "region" {
+ description = "Region."
+ type = string
+ default = "europe-west1"
+}
+
+variable "clusters_config" {
+ description = "Clusters configuration."
+ type = map(object({
+ subnet_cidr_block = string
+ master_cidr_block = string
+ services_cidr_block = string
+ pods_cidr_block = string
+ }))
+ default = {
+ cluster-a = {
+ subnet_cidr_block = "10.0.1.0/24"
+ master_cidr_block = "10.16.0.0/28"
+ services_cidr_block = "192.168.1.0/24"
+ pods_cidr_block = "172.16.0.0/20"
+ }
+ cluster-b = {
+ subnet_cidr_block = "10.0.2.0/24"
+ master_cidr_block = "10.16.0.16/28"
+ services_cidr_block = "192.168.2.0/24"
+ pods_cidr_block = "172.16.16.0/20"
+ }
+ }
+}
+
+variable "mgmt_server_config" {
+ description = "Mgmt server configuration"
+ type = object({
+ disk_size = number
+ disk_type = string
+ image = string
+ instance_type = string
+ region = string
+ zone = string
+ })
+ default = {
+ disk_size = 50
+ disk_type = "pd-ssd"
+ image = "projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts"
+ instance_type = "n1-standard-2"
+ region = "europe-west1"
+ zone = "europe-west1-c"
+ }
+}
+
+variable "istio_version" {
+ description = "ASM version"
+ type = string
+ default = "1.14.1-asm.3"
+}
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
new file mode 100644
index 0000000000..270a142d1d
--- /dev/null
+++ b/tests/blueprints/gke/multi_cluster_mesh_gke_fleet_api/test_plan.py
@@ -0,0 +1,19 @@
+# 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(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
diff --git a/tests/modules/cloud_run/fixture/variables.tf b/tests/blueprints/gke/multitenant_fleet/__init__.py
similarity index 100%
rename from tests/modules/cloud_run/fixture/variables.tf
rename to tests/blueprints/gke/multitenant_fleet/__init__.py
diff --git a/tests/blueprints/gke/multitenant_fleet/fixture/main.tf b/tests/blueprints/gke/multitenant_fleet/fixture/main.tf
new file mode 100644
index 0000000000..394d555de3
--- /dev/null
+++ b/tests/blueprints/gke/multitenant_fleet/fixture/main.tf
@@ -0,0 +1,55 @@
+/**
+ * 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/gke/multitenant-fleet"
+ project_id = "test-prj"
+ billing_account_id = "ABCDEF-0123456-ABCDEF"
+ folder_id = "folders/1234567890"
+ prefix = "test"
+ clusters = {
+ cluster-0 = {
+ location = "europe-west1"
+ private_cluster_config = {
+ enable_private_endpoint = true
+ master_global_access = true
+ }
+ vpc_config = {
+ subnetwork = "projects/my-host-project-id/regions/europe-west1/subnetworks/mycluster-subnet"
+ master_ipv4_cidr_block = "172.16.10.0/28"
+ secondary_range_names = {
+ pods = "pods"
+ services = "services"
+ }
+ }
+ }
+ }
+ nodepools = {
+ cluster-0 = {
+ nodepool-0 = {
+ node_config = {
+ disk_type = "pd-balanced"
+ machine_type = "n2-standard-4"
+ spot = true
+ }
+ }
+ }
+ }
+ vpc_config = {
+ host_project_id = "my-host-project-id"
+ vpc_self_link = "projects/my-host-project-id/global/networks/my-network"
+ }
+}
diff --git a/tests/blueprints/gke/multitenant_fleet/test_plan.py b/tests/blueprints/gke/multitenant_fleet/test_plan.py
new file mode 100644
index 0000000000..2b94b766f7
--- /dev/null
+++ b/tests/blueprints/gke/multitenant_fleet/test_plan.py
@@ -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.
+
+
+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
diff --git a/tests/blueprints/gke/shared_vpc_gke/__init__.py b/tests/blueprints/gke/shared_vpc_gke/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/blueprints/gke/shared_vpc_gke/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/blueprints/gke/shared_vpc_gke/fixture/main.tf b/tests/blueprints/gke/shared_vpc_gke/fixture/main.tf
new file mode 100644
index 0000000000..ac4e647a2e
--- /dev/null
+++ b/tests/blueprints/gke/shared_vpc_gke/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/gke/shared-vpc-gke"
+ billing_account_id = var.billing_account_id
+ prefix = var.prefix
+ root_node = var.root_node
+}
diff --git a/tests/examples/networking/decentralized_firewall/fixture/variables.tf b/tests/blueprints/gke/shared_vpc_gke/fixture/variables.tf
similarity index 100%
rename from tests/examples/networking/decentralized_firewall/fixture/variables.tf
rename to tests/blueprints/gke/shared_vpc_gke/fixture/variables.tf
diff --git a/tests/examples/networking/shared_vpc_gke/test_plan.py b/tests/blueprints/gke/shared_vpc_gke/test_plan.py
similarity index 100%
rename from tests/examples/networking/shared_vpc_gke/test_plan.py
rename to tests/blueprints/gke/shared_vpc_gke/test_plan.py
diff --git a/tests/blueprints/networking/__init__.py b/tests/blueprints/networking/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/blueprints/networking/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/blueprints/networking/decentralized_firewall/__init__.py b/tests/blueprints/networking/decentralized_firewall/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/blueprints/networking/decentralized_firewall/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/blueprints/networking/decentralized_firewall/fixture/main.tf b/tests/blueprints/networking/decentralized_firewall/fixture/main.tf
new file mode 100644
index 0000000000..92d45041ec
--- /dev/null
+++ b/tests/blueprints/networking/decentralized_firewall/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/networking/decentralized-firewall"
+ billing_account_id = var.billing_account_id
+ prefix = var.prefix
+ root_node = var.root_node
+}
diff --git a/tests/examples/networking/shared_vpc_gke/fixture/variables.tf b/tests/blueprints/networking/decentralized_firewall/fixture/variables.tf
similarity index 100%
rename from tests/examples/networking/shared_vpc_gke/fixture/variables.tf
rename to tests/blueprints/networking/decentralized_firewall/fixture/variables.tf
diff --git a/tests/examples/networking/decentralized_firewall/test_plan.py b/tests/blueprints/networking/decentralized_firewall/test_plan.py
similarity index 100%
rename from tests/examples/networking/decentralized_firewall/test_plan.py
rename to tests/blueprints/networking/decentralized_firewall/test_plan.py
diff --git a/tests/blueprints/networking/filtering_proxy/__init__.py b/tests/blueprints/networking/filtering_proxy/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/blueprints/networking/filtering_proxy/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/blueprints/networking/filtering_proxy/fixture/main.tf b/tests/blueprints/networking/filtering_proxy/fixture/main.tf
new file mode 100644
index 0000000000..2d250c0abd
--- /dev/null
+++ b/tests/blueprints/networking/filtering_proxy/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/networking/filtering-proxy"
+ billing_account = "123456-123456-123456"
+ mig = var.mig
+ prefix = "fabric"
+ root_node = "folders/123456789"
+}
diff --git a/tests/examples/networking/filtering_proxy/fixture/variables.tf b/tests/blueprints/networking/filtering_proxy/fixture/variables.tf
similarity index 100%
rename from tests/examples/networking/filtering_proxy/fixture/variables.tf
rename to tests/blueprints/networking/filtering_proxy/fixture/variables.tf
diff --git a/tests/examples/networking/filtering_proxy/test_plan.py b/tests/blueprints/networking/filtering_proxy/test_plan.py
similarity index 100%
rename from tests/examples/networking/filtering_proxy/test_plan.py
rename to tests/blueprints/networking/filtering_proxy/test_plan.py
diff --git a/tests/blueprints/networking/filtering_proxy_psc/__init__.py b/tests/blueprints/networking/filtering_proxy_psc/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/blueprints/networking/filtering_proxy_psc/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/blueprints/networking/filtering_proxy_psc/fixture/main.tf b/tests/blueprints/networking/filtering_proxy_psc/fixture/main.tf
new file mode 100644
index 0000000000..eb01058d10
--- /dev/null
+++ b/tests/blueprints/networking/filtering_proxy_psc/fixture/main.tf
@@ -0,0 +1,25 @@
+/**
+ * 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/networking/filtering-proxy-psc"
+ prefix = "fabric"
+ project_create = {
+ billing_account = "123456-ABCDEF-123456"
+ parent = "folders/1234567890"
+ }
+ project_id = "test-project"
+}
diff --git a/tests/blueprints/networking/filtering_proxy_psc/test_plan.py b/tests/blueprints/networking/filtering_proxy_psc/test_plan.py
new file mode 100644
index 0000000000..498bb2bfb5
--- /dev/null
+++ b/tests/blueprints/networking/filtering_proxy_psc/test_plan.py
@@ -0,0 +1,19 @@
+# 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(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) == 34
diff --git a/tests/blueprints/networking/glb_and_armor/__init__.py b/tests/blueprints/networking/glb_and_armor/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/blueprints/networking/glb_and_armor/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/blueprints/networking/glb_and_armor/fixture/main.tf b/tests/blueprints/networking/glb_and_armor/fixture/main.tf
new file mode 100644
index 0000000000..2a5a70773f
--- /dev/null
+++ b/tests/blueprints/networking/glb_and_armor/fixture/main.tf
@@ -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
+#
+# 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.
+
+module "test" {
+ source = "../../../../../blueprints/networking/glb-and-armor"
+ prefix = var.prefix
+ project_create = var.project_create
+ project_id = var.project_id
+ enforce_security_policy = var.enforce_security_policy
+}
diff --git a/tests/blueprints/networking/glb_and_armor/fixture/variables.tf b/tests/blueprints/networking/glb_and_armor/fixture/variables.tf
new file mode 100644
index 0000000000..41090c1c32
--- /dev/null
+++ b/tests/blueprints/networking/glb_and_armor/fixture/variables.tf
@@ -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
+#
+# 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.
+
+variable "prefix" {
+ type = string
+ default = "test"
+}
+
+variable "project_create" {
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = {
+ billing_account_id = "123456789"
+ parent = "organizations/123456789"
+ }
+}
+
+variable "project_id" {
+ type = string
+ default = "project-1"
+}
+
+variable "enforce_security_policy" {
+ type = bool
+ default = true
+}
diff --git a/tests/blueprints/networking/glb_and_armor/test_plan.py b/tests/blueprints/networking/glb_and_armor/test_plan.py
new file mode 100644
index 0000000000..dc4a4956ff
--- /dev/null
+++ b/tests/blueprints/networking/glb_and_armor/test_plan.py
@@ -0,0 +1,19 @@
+# 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(e2e_plan_runner):
+ "Test that plan works and the numbers of resources is as expected."
+ modules, resources = e2e_plan_runner()
+ assert len(modules) == 11
+ assert len(resources) == 25
diff --git a/tests/blueprints/networking/hub_and_spoke_peering/__init__.py b/tests/blueprints/networking/hub_and_spoke_peering/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/blueprints/networking/hub_and_spoke_peering/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/blueprints/networking/hub_and_spoke_peering/fixture/main.tf b/tests/blueprints/networking/hub_and_spoke_peering/fixture/main.tf
new file mode 100644
index 0000000000..c5b105e68a
--- /dev/null
+++ b/tests/blueprints/networking/hub_and_spoke_peering/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/networking/hub-and-spoke-peering"
+ prefix = var.prefix
+ project_create = {
+ billing_account = "123456-123456-123456"
+ oslogin = true
+ parent = "folders/123456789"
+ }
+ project_id = var.project_id
+}
diff --git a/tests/blueprints/networking/hub_and_spoke_peering/fixture/variables.tf b/tests/blueprints/networking/hub_and_spoke_peering/fixture/variables.tf
new file mode 100644
index 0000000000..b67795f96e
--- /dev/null
+++ b/tests/blueprints/networking/hub_and_spoke_peering/fixture/variables.tf
@@ -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
+#
+# 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.
+
+variable "prefix" {
+ type = string
+ default = "test"
+}
+
+variable "project_id" {
+ type = string
+ default = "project-1"
+}
diff --git a/tests/blueprints/networking/hub_and_spoke_peering/test_plan.py b/tests/blueprints/networking/hub_and_spoke_peering/test_plan.py
new file mode 100644
index 0000000000..127a0ba4ba
--- /dev/null
+++ b/tests/blueprints/networking/hub_and_spoke_peering/test_plan.py
@@ -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.
+
+
+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) == 21
+ assert len(resources) == 61
diff --git a/tests/blueprints/networking/hub_and_spoke_vpn/__init__.py b/tests/blueprints/networking/hub_and_spoke_vpn/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/blueprints/networking/hub_and_spoke_vpn/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/blueprints/networking/hub_and_spoke_vpn/fixture/main.tf b/tests/blueprints/networking/hub_and_spoke_vpn/fixture/main.tf
new file mode 100644
index 0000000000..37558c7145
--- /dev/null
+++ b/tests/blueprints/networking/hub_and_spoke_vpn/fixture/main.tf
@@ -0,0 +1,25 @@
+/**
+ * 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/networking/hub-and-spoke-vpn"
+ prefix = var.prefix
+ project_create_config = {
+ billing_account_id = "ABCDE-123456-ABCDE"
+ parent_id = null
+ }
+ project_id = var.project_id
+}
diff --git a/tests/blueprints/networking/hub_and_spoke_vpn/fixture/variables.tf b/tests/blueprints/networking/hub_and_spoke_vpn/fixture/variables.tf
new file mode 100644
index 0000000000..b67795f96e
--- /dev/null
+++ b/tests/blueprints/networking/hub_and_spoke_vpn/fixture/variables.tf
@@ -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
+#
+# 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.
+
+variable "prefix" {
+ type = string
+ default = "test"
+}
+
+variable "project_id" {
+ type = string
+ default = "project-1"
+}
diff --git a/tests/blueprints/networking/hub_and_spoke_vpn/test_plan.py b/tests/blueprints/networking/hub_and_spoke_vpn/test_plan.py
new file mode 100644
index 0000000000..a24aaa596b
--- /dev/null
+++ b/tests/blueprints/networking/hub_and_spoke_vpn/test_plan.py
@@ -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.
+
+
+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) == 19
+ assert len(resources) == 73
diff --git a/tests/blueprints/networking/ilb_next_hop/__init__.py b/tests/blueprints/networking/ilb_next_hop/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/blueprints/networking/ilb_next_hop/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/blueprints/networking/ilb_next_hop/fixture/main.tf b/tests/blueprints/networking/ilb_next_hop/fixture/main.tf
new file mode 100644
index 0000000000..acaad22ad4
--- /dev/null
+++ b/tests/blueprints/networking/ilb_next_hop/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/networking/ilb-next-hop"
+ prefix = var.prefix
+ project_create = var.project_create
+ project_id = var.project_id
+}
diff --git a/tests/blueprints/networking/ilb_next_hop/fixture/variables.tf b/tests/blueprints/networking/ilb_next_hop/fixture/variables.tf
new file mode 100644
index 0000000000..4eede1798a
--- /dev/null
+++ b/tests/blueprints/networking/ilb_next_hop/fixture/variables.tf
@@ -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
+#
+# 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.
+
+variable "prefix" {
+ type = string
+ default = "test"
+}
+
+variable "project_create" {
+ type = bool
+ default = true
+}
+
+variable "project_id" {
+ type = string
+ default = "project-1"
+}
diff --git a/tests/examples/networking/ilb_next_hop/test_plan.py b/tests/blueprints/networking/ilb_next_hop/test_plan.py
similarity index 100%
rename from tests/examples/networking/ilb_next_hop/test_plan.py
rename to tests/blueprints/networking/ilb_next_hop/test_plan.py
diff --git a/tests/blueprints/networking/onprem_google_access_dns/__init__.py b/tests/blueprints/networking/onprem_google_access_dns/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/blueprints/networking/onprem_google_access_dns/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/blueprints/networking/onprem_google_access_dns/fixture/main.tf b/tests/blueprints/networking/onprem_google_access_dns/fixture/main.tf
new file mode 100644
index 0000000000..d88267895a
--- /dev/null
+++ b/tests/blueprints/networking/onprem_google_access_dns/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/networking/onprem-google-access-dns"
+ project_id = var.project_id
+}
diff --git a/tests/examples/networking/hub_and_spoke_peering/fixture/variables.tf b/tests/blueprints/networking/onprem_google_access_dns/fixture/variables.tf
similarity index 100%
rename from tests/examples/networking/hub_and_spoke_peering/fixture/variables.tf
rename to tests/blueprints/networking/onprem_google_access_dns/fixture/variables.tf
diff --git a/tests/examples/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/examples/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/__init__.py b/tests/blueprints/networking/private_cloud_function_from_onprem/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/blueprints/networking/private_cloud_function_from_onprem/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/blueprints/networking/private_cloud_function_from_onprem/fixture/main.tf b/tests/blueprints/networking/private_cloud_function_from_onprem/fixture/main.tf
new file mode 100644
index 0000000000..d166a0fc28
--- /dev/null
+++ b/tests/blueprints/networking/private_cloud_function_from_onprem/fixture/main.tf
@@ -0,0 +1,24 @@
+/**
+ * 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/networking/private-cloud-function-from-onprem"
+ project_create = {
+ billing_account_id = "123456-ABCDEF-123456"
+ parent = "folders/1234567890"
+ }
+ project_id = "test-project"
+}
diff --git a/tests/examples/networking/private_cloud_function_from_onprem/test_plan.py b/tests/blueprints/networking/private_cloud_function_from_onprem/test_plan.py
similarity index 100%
rename from tests/examples/networking/private_cloud_function_from_onprem/test_plan.py
rename to tests/blueprints/networking/private_cloud_function_from_onprem/test_plan.py
diff --git a/tests/blueprints/serverless/__init__.py b/tests/blueprints/serverless/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/blueprints/serverless/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/blueprints/serverless/api_gateway/__init__.py b/tests/blueprints/serverless/api_gateway/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/blueprints/serverless/api_gateway/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/blueprints/serverless/api_gateway/fixture/main.tf b/tests/blueprints/serverless/api_gateway/fixture/main.tf
new file mode 100644
index 0000000000..094d469773
--- /dev/null
+++ b/tests/blueprints/serverless/api_gateway/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../../blueprints/serverless/api-gateway"
+ project_create = var.project_create
+ project_id = var.project_id
+ regions = var.regions
+}
diff --git a/tests/examples/serverless/api_gateway/fixture/variables.tf b/tests/blueprints/serverless/api_gateway/fixture/variables.tf
similarity index 100%
rename from tests/examples/serverless/api_gateway/fixture/variables.tf
rename to tests/blueprints/serverless/api_gateway/fixture/variables.tf
diff --git a/tests/examples/serverless/api_gateway/test_plan.py b/tests/blueprints/serverless/api_gateway/test_plan.py
similarity index 100%
rename from tests/examples/serverless/api_gateway/test_plan.py
rename to tests/blueprints/serverless/api_gateway/test_plan.py
diff --git a/tests/collectors.py b/tests/collectors.py
new file mode 100644
index 0000000000..e69782f09d
--- /dev/null
+++ b/tests/collectors.py
@@ -0,0 +1,92 @@
+# 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.
+"""Pytest plugin to discover tests specified in YAML files.
+
+This plugin uses the pytest_collect_file hook to collect all files
+matching tftest*.yaml and runs plan_validate for each test found.
+See FabricTestFile for details on the file structure.
+
+"""
+
+import pytest
+import yaml
+
+from .fixtures import plan_summary, plan_validator
+
+
+class FabricTestFile(pytest.File):
+
+ def collect(self):
+ """Read yaml test spec and yield test items for each test definition.
+
+ The test spec should contain a `module` key with the path of the
+ terraform module to test, relative to the root of the repository
+
+ Tests are defined within the top-level `tests` key, and should
+ have the following structure:
+
+ test-name:
+ tfvars:
+ - tfvars1.tfvars
+ - tfvars2.tfvars
+ inventory:
+ - inventory1.yaml
+ - inventory2.yaml
+
+ All paths specifications are relative to the location of the test
+ spec. The inventory key is optional, if omitted, the inventory
+ will be taken from the file test-name.yaml
+
+ """
+
+ try:
+ raw = yaml.safe_load(self.path.open())
+ module = raw.pop('module')
+ except (IOError, OSError, yaml.YAMLError) as e:
+ raise Exception(f'cannot read test spec {self.path}: {e}')
+ except KeyError as e:
+ raise Exception(f'`module` key not found in {self.path}: {e}')
+ common = raw.pop('common_tfvars', [])
+ for test_name, spec in raw.get('tests', {}).items():
+ spec = {} if spec is None else spec
+ inventories = spec.get('inventory', [f'{test_name}.yaml'])
+ tfvars = common + [f'{test_name}.tfvars'] + spec.get('tfvars', [])
+ for i in inventories:
+ name = test_name
+ if isinstance(inventories, list) and len(inventories) > 1:
+ name = f'{test_name}[{i}]'
+ yield FabricTestItem.from_parent(self, name=name, module=module,
+ inventory=[i], tfvars=tfvars)
+
+
+class FabricTestItem(pytest.Item):
+
+ def __init__(self, name, parent, module, inventory, tfvars):
+ super().__init__(name, parent)
+ self.module = module
+ self.inventory = inventory
+ self.tfvars = tfvars
+
+ def runtest(self):
+ s = plan_validator(self.module, self.inventory, self.parent.path.parent,
+ self.tfvars)
+
+ def reportinfo(self):
+ return self.path, None, self.name
+
+
+def pytest_collect_file(parent, file_path):
+ 'Collect tftest*.yaml files and run plan_validator from them.'
+ if file_path.suffix == '.yaml' and file_path.name.startswith('tftest'):
+ return FabricTestFile.from_parent(parent, path=file_path)
diff --git a/tests/conftest.py b/tests/conftest.py
index 9dc566b4b7..167c74f73c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -11,125 +11,12 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-
-"Shared fixtures"
-
-import inspect
-import os
-import shutil
-import tempfile
+'Pytest configuration.'
import pytest
-import tftest
-
-BASEDIR = os.path.dirname(os.path.dirname(__file__))
-
-
-@pytest.fixture(scope='session')
-def _plan_runner():
- "Returns a function to run Terraform plan on a fixture."
-
- def run_plan(fixture_path=None, targets=None, refresh=True, **tf_vars):
- "Runs Terraform plan and returns parsed output."
- if fixture_path is None:
- # find out the fixture directory from the caller's directory
- caller = inspect.stack()[2]
- fixture_path = os.path.join(os.path.dirname(caller.filename), "fixture")
-
- fixture_parent = os.path.dirname(fixture_path)
- fixture_prefix = os.path.basename(fixture_path) + "_"
- with tempfile.TemporaryDirectory(prefix=fixture_prefix,
- dir=fixture_parent) as tmp_path:
- # copy fixture to a temporary directory so we can execute
- # multiple tests in parallel
- shutil.copytree(fixture_path, tmp_path, dirs_exist_ok=True)
- tf = tftest.TerraformTest(tmp_path, BASEDIR,
- os.environ.get('TERRAFORM', 'terraform'))
- tf.setup(upgrade=True)
- return tf.plan(output=True, refresh=refresh, tf_vars=tf_vars, targets=targets)
-
- return run_plan
-
-
-@ pytest.fixture(scope='session')
-def plan_runner(_plan_runner):
- "Returns a function to run Terraform plan on a module fixture."
-
- def run_plan(fixture_path=None, targets=None, **tf_vars):
- "Runs Terraform plan and returns plan and module resources."
- plan = _plan_runner(fixture_path, targets=targets, **tf_vars)
- # skip the fixture
- root_module = plan.root_module['child_modules'][0]
- return plan, root_module['resources']
-
- return run_plan
-
-
-@ pytest.fixture(scope='session')
-def e2e_plan_runner(_plan_runner):
- "Returns a function to run Terraform plan on an end-to-end fixture."
-
- def run_plan(fixture_path=None, targets=None, refresh=True,
- include_bare_resources=False, **tf_vars):
- "Runs Terraform plan on an end-to-end module using defaults, returns data."
- plan = _plan_runner(fixture_path, targets=targets,
- refresh=refresh, **tf_vars)
- # skip the fixture
- root_module = plan.root_module['child_modules'][0]
- modules = dict((mod['address'], mod['resources'])
- for mod in root_module['child_modules'])
- resources = [r for m in modules.values() for r in m]
- if include_bare_resources:
- bare_resources = root_module['resources']
- resources.extend(bare_resources)
- return modules, resources
-
- return run_plan
-
-
-@ pytest.fixture(scope='session')
-def doc_example_plan_runner(_plan_runner):
- "Returns a function to run Terraform plan on documentation examples."
-
- def run_plan(fixture_path=None):
- "Runs Terraform plan and returns count of modules and resources."
- tf = tftest.TerraformTest(fixture_path, BASEDIR,
- os.environ.get('TERRAFORM', 'terraform'))
- tf.setup(upgrade=True)
- plan = tf.plan(output=True, refresh=True)
- # the fixture is the example we are testing
- modules = plan.modules or {}
- return (
- len(modules),
- sum(len(m.resources) for m in modules.values()))
-
- return run_plan
-
-
-@ pytest.fixture(scope='session')
-def apply_runner():
- "Returns a function to run Terraform apply on a fixture."
-
- def run_apply(fixture_path=None, **tf_vars):
- "Runs Terraform plan and returns parsed output."
- if fixture_path is None:
- # find out the fixture directory from the caller's directory
- caller = inspect.stack()[1]
- fixture_path = os.path.join(os.path.dirname(caller.filename), "fixture")
-
- fixture_parent = os.path.dirname(fixture_path)
- fixture_prefix = os.path.basename(fixture_path) + "_"
-
- with tempfile.TemporaryDirectory(prefix=fixture_prefix,
- dir=fixture_parent) as tmp_path:
- # copy fixture to a temporary directory so we can execute
- # multiple tests in parallel
- shutil.copytree(fixture_path, tmp_path, dirs_exist_ok=True)
- tf = tftest.TerraformTest(tmp_path, BASEDIR,
- os.environ.get('TERRAFORM', 'terraform'))
- tf.setup(upgrade=True)
- apply = tf.apply(tf_vars=tf_vars)
- output = tf.output(json_format=True)
- return apply, output
- return run_apply
+pytest_plugins = (
+ 'tests.fixtures',
+ 'tests.legacy_fixtures',
+ 'tests.collectors',
+)
diff --git a/tests/doc_examples/conftest.py b/tests/doc_examples/conftest.py
deleted file mode 100644
index 6289a58026..0000000000
--- a/tests/doc_examples/conftest.py
+++ /dev/null
@@ -1,53 +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 pathlib import Path
-
-import marko
-
-MODULES_PATH = Path(__file__).parents[2] / 'modules/'
-
-
-def pytest_generate_tests(metafunc):
- if 'example' in metafunc.fixturenames:
- modules = [
- x for x in MODULES_PATH.iterdir()
- if x.is_dir()
- ]
- modules.sort()
- examples = []
- ids = []
- for module in modules:
- readme = module / 'README.md'
- if not readme.exists():
- continue
- doc = marko.parse(readme.read_text())
- index = 0
- last_header = None
- for child in doc.children:
- if isinstance(child, marko.block.FencedCode) and child.lang == 'hcl':
- index += 1
- code = child.children[0].children
- if 'tftest skip' in code:
- continue
- examples.append(code)
- name = f'{module.stem}:{last_header}'
- if index > 1:
- name += f' {index}'
- ids.append(name)
- elif isinstance(child, marko.block.Heading):
- last_header = child.children[0].children
- index = 0
-
- metafunc.parametrize('example', examples, ids=ids)
diff --git a/tests/doc_examples/test_plan.py b/tests/doc_examples/test_plan.py
deleted file mode 100644
index 0287a9d6e9..0000000000
--- a/tests/doc_examples/test_plan.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# Copyright 2022 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import re
-from pathlib import Path
-
-
-BASE_PATH = Path(__file__).parent
-EXPECTED_RESOURCES_RE = re.compile(r'# tftest modules=(\d+) resources=(\d+)')
-
-
-def test_example(doc_example_plan_runner, tmp_path, example):
- (tmp_path / 'modules').symlink_to(
- Path(BASE_PATH, '../../modules/').resolve())
- (tmp_path / 'variables.tf').symlink_to(
- Path(BASE_PATH, 'variables.tf').resolve())
- (tmp_path / 'main.tf').write_text(example)
-
- match = EXPECTED_RESOURCES_RE.search(example)
- 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
-
- num_modules, num_resources = doc_example_plan_runner(str(tmp_path))
- assert expected_modules == num_modules
- assert expected_resources == num_resources
diff --git a/tests/doc_examples/variables.tf b/tests/doc_examples/variables.tf
deleted file mode 100644
index 38fb3db38c..0000000000
--- a/tests/doc_examples/variables.tf
+++ /dev/null
@@ -1,76 +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.
-
-# common variables used for examples
-
-variable "bucket" {
- default = "bucket"
-}
-
-variable "billing_account_id" {
- default = "billing_account_id"
-}
-
-variable "kms_key" {
- default = {
- self_link = "kms_key_self_link"
- }
-}
-
-variable "organization_id" {
- default = "organizations/1122334455"
-}
-
-variable "project_id" {
- default = "projects/project-id"
-}
-
-variable "region" {
- default = "region"
-}
-
-variable "service_account" {
- default = {
- id = "service_account_id"
- email = "service_account_email"
- iam_email = "service_account_iam_email"
- }
-}
-
-variable "subnet" {
- default = {
- name = "subnet_name"
- region = "subnet_region"
- cidr = "subnet_cidr"
- self_link = "subnet_self_link"
- }
-}
-
-variable "vpc" {
- default = {
- name = "vpc_name"
- self_link = "projects/xxx/global/networks/yyy"
- }
-}
-
-variable "vpc2" {
- default = {
- name = "vpc2_name"
- self_link = "vpc2_self_link"
- }
-}
-
-variable "zone" {
- default = "zone"
-}
diff --git a/tests/examples/__init__.py b/tests/examples/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/examples/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/examples/cloud_operations/asset_inventory_feed_remediation/fixture/main.tf b/tests/examples/cloud_operations/asset_inventory_feed_remediation/fixture/main.tf
deleted file mode 100644
index 63ca043c9d..0000000000
--- a/tests/examples/cloud_operations/asset_inventory_feed_remediation/fixture/main.tf
+++ /dev/null
@@ -1,21 +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 = "../../../../../examples/cloud-operations/asset-inventory-feed-remediation"
- project_create = var.project_create
- project_id = var.project_id
-}
diff --git a/tests/examples/cloud_operations/dns_fine_grained_iam/fixture/main.tf b/tests/examples/cloud_operations/dns_fine_grained_iam/fixture/main.tf
deleted file mode 100644
index 95846e5d4a..0000000000
--- a/tests/examples/cloud_operations/dns_fine_grained_iam/fixture/main.tf
+++ /dev/null
@@ -1,22 +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 = "../../../../../examples/cloud-operations/dns-fine-grained-iam"
- name = var.name
- project_create = var.project_create
- project_id = var.project_id
-}
diff --git a/tests/examples/cloud_operations/dns_shared_vpc/fixture/main.tf b/tests/examples/cloud_operations/dns_shared_vpc/fixture/main.tf
deleted file mode 100644
index 094e766c98..0000000000
--- a/tests/examples/cloud_operations/dns_shared_vpc/fixture/main.tf
+++ /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.
- */
-
-module "test" {
- source = "../../../../../examples/cloud-operations/dns-shared-vpc"
- billing_account_id = "111111-222222-333333"
- folder_id = "folders/1234567890"
- shared_vpc_link = "https://www.googleapis.com/compute/v1/projects/test-dns/global/networks/default"
- teams = var.teams
-}
diff --git a/tests/examples/cloud_operations/dns_shared_vpc/fixture/variables.tf b/tests/examples/cloud_operations/dns_shared_vpc/fixture/variables.tf
deleted file mode 100644
index c6eeb83efe..0000000000
--- a/tests/examples/cloud_operations/dns_shared_vpc/fixture/variables.tf
+++ /dev/null
@@ -1,18 +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
-#
-# 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.
-
-variable "teams" {
- type = list(string)
- default = ["team1", "team2"]
-}
diff --git a/tests/examples/cloud_operations/iam_delegated_role_grants/fixture/main.tf b/tests/examples/cloud_operations/iam_delegated_role_grants/fixture/main.tf
deleted file mode 100644
index 005cd6148b..0000000000
--- a/tests/examples/cloud_operations/iam_delegated_role_grants/fixture/main.tf
+++ /dev/null
@@ -1,22 +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 = "../../../../../examples/cloud-operations/iam-delegated-role-grants"
- project_create = true
- project_id = var.project_id
- project_administrators = ["user:user@example.com"]
-}
diff --git a/tests/examples/cloud_operations/onprem_sa_key_management/fixture/main.tf b/tests/examples/cloud_operations/onprem_sa_key_management/fixture/main.tf
deleted file mode 100644
index 48f4bdccf5..0000000000
--- a/tests/examples/cloud_operations/onprem_sa_key_management/fixture/main.tf
+++ /dev/null
@@ -1,21 +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 = "../../../../../examples/cloud-operations/onprem-sa-key-management"
- project_create = var.project_create
- project_id = var.project_id
-}
diff --git a/tests/examples/cloud_operations/packer_image_builder/fixture/main.tf b/tests/examples/cloud_operations/packer_image_builder/fixture/main.tf
deleted file mode 100644
index 31eefda86c..0000000000
--- a/tests/examples/cloud_operations/packer_image_builder/fixture/main.tf
+++ /dev/null
@@ -1,22 +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 = "../../../../../examples/cloud-operations/packer-image-builder"
- project_id = "test-project"
- packer_account_users = ["user:john@testdomain.com"]
- create_packer_vars = var.create_packer_vars
-}
diff --git a/tests/examples/cloud_operations/quota_monitoring/fixture/main.tf b/tests/examples/cloud_operations/quota_monitoring/fixture/main.tf
deleted file mode 100644
index f4334d8dfa..0000000000
--- a/tests/examples/cloud_operations/quota_monitoring/fixture/main.tf
+++ /dev/null
@@ -1,22 +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 = "../../../../../examples/cloud-operations/quota-monitoring"
- name = var.name
- project_create = var.project_create
- project_id = var.project_id
-}
diff --git a/tests/examples/cloud_operations/scheduled_asset_inventory_export_bq/fixture/main.tf b/tests/examples/cloud_operations/scheduled_asset_inventory_export_bq/fixture/main.tf
deleted file mode 100644
index 1c87e915e4..0000000000
--- a/tests/examples/cloud_operations/scheduled_asset_inventory_export_bq/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 "test" {
- source = "../../../../../examples/cloud-operations/scheduled-asset-inventory-export-bq"
- billing_account = var.billing_account
- cai_config = var.cai_config
- cai_gcs_export = var.cai_gcs_export
- file_config = var.file_config
- project_create = var.project_create
- project_id = var.project_id
-}
diff --git a/tests/examples/cloud_operations/unmanaged_instances_healthcheck/fixture/main.tf b/tests/examples/cloud_operations/unmanaged_instances_healthcheck/fixture/main.tf
deleted file mode 100644
index 213ba24685..0000000000
--- a/tests/examples/cloud_operations/unmanaged_instances_healthcheck/fixture/main.tf
+++ /dev/null
@@ -1,22 +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 = "../../../../../examples/cloud-operations/unmanaged-instances-healthcheck"
- billing_account = var.billing_account
- project_create = var.project_create
- project_id = var.project_id
-}
diff --git a/tests/examples/cloud_operations/unmanaged_instances_healthcheck/test_plan.py b/tests/examples/cloud_operations/unmanaged_instances_healthcheck/test_plan.py
deleted file mode 100644
index 3f6f34994a..0000000000
--- a/tests/examples/cloud_operations/unmanaged_instances_healthcheck/test_plan.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright 2022 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-def test_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
diff --git a/tests/examples/cloud_operations/vm_migration/host_target_projects/fixture/main.tf b/tests/examples/cloud_operations/vm_migration/host_target_projects/fixture/main.tf
deleted file mode 100644
index 94fb07f79d..0000000000
--- a/tests/examples/cloud_operations/vm_migration/host_target_projects/fixture/main.tf
+++ /dev/null
@@ -1,43 +0,0 @@
-# Copyright 2022 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# 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.
-
-module "host-target-projects-test" {
- source = "../../../../../../examples/cloud-operations/vm-migration/host-target-projects"
- project_create = var.project_create
- migration_admin_users = ["user:admin@example.com"]
- migration_viewer_users = ["user:viewer@example.com"]
- migration_target_projects = ["${module.test-target-project.name}"]
- depends_on = [
- module.test-target-project
- ]
-}
-
-variable "project_create" {
- type = object({
- billing_account_id = string
- parent = string
- })
- default = {
- billing_account_id = "1234-ABCD-1234"
- parent = "folders/1234563"
- }
-}
-
-#This is a dummy project created to run this test. The example, here tested, is expected to run on top of existing foundations.
-module "test-target-project" {
- source = "../../../../../../modules/project"
- billing_account = "1234-ABCD-1234"
- name = "test-target-project"
- project_create = true
-}
diff --git a/tests/examples/cloud_operations/vm_migration/host_target_sharedvpc/fixture/main.tf b/tests/examples/cloud_operations/vm_migration/host_target_sharedvpc/fixture/main.tf
deleted file mode 100644
index dac8f5af0b..0000000000
--- a/tests/examples/cloud_operations/vm_migration/host_target_sharedvpc/fixture/main.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
-#
-# 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.
-
-module "host-target-sharedvpc-test" {
- source = "../../../../../../examples/cloud-operations/vm-migration/host-target-sharedvpc"
- project_create = var.project_create
- migration_admin_users = ["user:admin@example.com"]
- migration_viewer_users = ["user:viewer@example.com"]
- migration_target_projects = [module.test-target-project.name]
- sharedvpc_host_projects = [module.test-sharedvpc-host-project.name]
- depends_on = [
- module.test-target-project,
- module.test-sharedvpc-host-project,
- ]
-}
-
-variable "project_create" {
- type = object({
- billing_account_id = string
- parent = string
- })
- default = {
- billing_account_id = "1234-ABCD-1234"
- parent = "folders/1234563"
- }
-}
-
-#These are a dummy projects created to run this test. The example, here tested, is expected to run on top of existing foundations.
-module "test-target-project" {
- source = "../../../../../../modules/project"
- billing_account = "1234-ABCD-1234"
- name = "test-target-project"
- project_create = true
-}
-module "test-sharedvpc-host-project" {
- source = "../../../../../../modules/project"
- billing_account = "1234-ABCD-1234"
- name = "test-sharedvpc-host-project"
- project_create = true
-}
diff --git a/tests/examples/cloud_operations/vm_migration/single_project/fixture/main.tf b/tests/examples/cloud_operations/vm_migration/single_project/fixture/main.tf
deleted file mode 100644
index c2750298cc..0000000000
--- a/tests/examples/cloud_operations/vm_migration/single_project/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
-#
-# 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.
-
-module "single-project-test" {
- source = "../../../../../../examples/cloud-operations/vm-migration/single-project"
- project_create = var.project_create
- migration_admin_users = ["user:admin@example.com"]
- migration_viewer_users = ["user:viewer@example.com"]
-}
-
-variable "project_create" {
- type = object({
- billing_account_id = string
- parent = string
- })
- default = {
- billing_account_id = "1234-ABCD-1234"
- parent = "folders/1234563"
- }
-}
diff --git a/tests/examples/cloud_operations/vm_migration/single_project/test_plan.py b/tests/examples/cloud_operations/vm_migration/single_project/test_plan.py
deleted file mode 100644
index 7d8a47b158..0000000000
--- a/tests/examples/cloud_operations/vm_migration/single_project/test_plan.py
+++ /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.
-
-import os
-
-
-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) == 4
- assert len(resources) == 18
diff --git a/tests/examples/conftest.py b/tests/examples/conftest.py
new file mode 100644
index 0000000000..16863e26de
--- /dev/null
+++ b/tests/examples/conftest.py
@@ -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.
+"""Pytest configuration for testing code examples."""
+
+import collections
+import re
+from pathlib import Path
+
+import marko
+
+FABRIC_ROOT = Path(__file__).parents[2]
+
+FILE_TEST_RE = re.compile(r'# tftest-file +id=(\w+) +path=([\S]+)')
+
+Example = collections.namedtuple('Example', 'name code module files')
+File = collections.namedtuple('File', 'path content')
+
+
+def pytest_generate_tests(metafunc):
+ """Find all README.md files and collect code examples tagged for testing."""
+ if 'example' in metafunc.fixturenames:
+ readmes = FABRIC_ROOT.glob('**/README.md')
+ examples = []
+ ids = []
+
+ for readme in readmes:
+ module = readme.parent
+ doc = marko.parse(readme.read_text())
+ index = 0
+ files = collections.defaultdict(dict)
+
+ # 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[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
+ code = child.children[0].children
+ if 'tftest skip' in code:
+ continue
+ if child.lang == 'hcl':
+ 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
+
+ metafunc.parametrize('example', examples, ids=ids)
diff --git a/tests/examples/data_solutions/cmek_via_centralized_kms/fixture/main.tf b/tests/examples/data_solutions/cmek_via_centralized_kms/fixture/main.tf
deleted file mode 100644
index 7d4c183585..0000000000
--- a/tests/examples/data_solutions/cmek_via_centralized_kms/fixture/main.tf
+++ /dev/null
@@ -1,21 +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 = "../../../../../examples/data-solutions/cmek-via-centralized-kms/"
- billing_account = var.billing_account
- root_node = var.root_node
-}
diff --git a/tests/examples/data_solutions/data_platform_foundations/fixture/main.tf b/tests/examples/data_solutions/data_platform_foundations/fixture/main.tf
deleted file mode 100644
index 0b87b4c091..0000000000
--- a/tests/examples/data_solutions/data_platform_foundations/fixture/main.tf
+++ /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.
- */
-
-module "test" {
- source = "../../../../../examples/data-solutions/data-platform-foundations/"
- organization_domain = "example.com"
- billing_account_id = "123456-123456-123456"
- folder_id = "folders/12345678"
- prefix = "prefix"
-}
diff --git a/tests/examples/data_solutions/data_platform_foundations/test_plan.py b/tests/examples/data_solutions/data_platform_foundations/test_plan.py
deleted file mode 100644
index 4857bf9f14..0000000000
--- a/tests/examples/data_solutions/data_platform_foundations/test_plan.py
+++ /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.
-
-
-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)
- assert len(modules) == 40
- assert len(resources) == 296
diff --git a/tests/examples/data_solutions/gcs_to_bq_with_least_privileges/fixture/main.tf b/tests/examples/data_solutions/gcs_to_bq_with_least_privileges/fixture/main.tf
deleted file mode 100644
index 08a7035d35..0000000000
--- a/tests/examples/data_solutions/gcs_to_bq_with_least_privileges/fixture/main.tf
+++ /dev/null
@@ -1,22 +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 = "../../../../../examples/data-solutions/gcs-to-bq-with-least-privileges/"
- project_create = var.project_create
- project_id = var.project_id
- prefix = var.prefix
-}
diff --git a/tests/examples/data_solutions/gcs_to_bq_with_least_privileges/test_plan.py b/tests/examples/data_solutions/gcs_to_bq_with_least_privileges/test_plan.py
deleted file mode 100644
index 9a5bbe168f..0000000000
--- a/tests/examples/data_solutions/gcs_to_bq_with_least_privileges/test_plan.py
+++ /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.
-
-
-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)
- assert len(modules) == 11
- assert len(resources) == 46
diff --git a/tests/examples/factories/net_vpc_firewall_yaml/fixture/main.tf b/tests/examples/factories/net_vpc_firewall_yaml/fixture/main.tf
deleted file mode 100644
index 724ff9d276..0000000000
--- a/tests/examples/factories/net_vpc_firewall_yaml/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 = "../../../../../examples/factories/net-vpc-firewall-yaml"
- project_id = "my-project"
- network = "my-network"
- config_directories = [
- "./rules"
- ]
- log_config = var.log_config
-}
diff --git a/tests/examples/factories/project_factory/fixture/defaults.yaml b/tests/examples/factories/project_factory/fixture/defaults.yaml
deleted file mode 100644
index dc5b161661..0000000000
--- a/tests/examples/factories/project_factory/fixture/defaults.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-# skip boilerplate check
-
-billing_account_id: 012345-67890A-BCDEF0
-
-# [opt] Setup for billing alerts
-billing_alert:
- amount: 1000
- thresholds:
- current: [0.5, 0.8]
- forecasted: [0.5, 0.8]
- credit_treatment: INCLUDE_ALL_CREDITS
-
-# [opt] Contacts for billing alerts and important notifications
-essential_contacts: ["team-contacts@example.com"]
-
-# [opt] Labels set for all projects
-labels:
- environment: prod
- department: accounting
- application: example-app
- foo: bar
-
-# [opt] Additional notification channels for billing
-notification_channels: []
diff --git a/tests/examples/factories/project_factory/fixture/main.tf b/tests/examples/factories/project_factory/fixture/main.tf
deleted file mode 100644
index f81ab9f025..0000000000
--- a/tests/examples/factories/project_factory/fixture/main.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.
- */
-
-locals {
- _defaults = yamldecode(file(var.defaults_file))
- _defaults_net = {
- billing_account_id = var.billing_account_id
- environment_dns_zone = var.environment_dns_zone
- shared_vpc_self_link = var.shared_vpc_self_link
- vpc_host_project = var.vpc_host_project
- }
- defaults = merge(local._defaults, local._defaults_net)
- projects = {
- for f in fileset("${var.data_dir}", "**/*.yaml") :
- trimsuffix(f, ".yaml") => yamldecode(file("${var.data_dir}/${f}"))
- }
-}
-
-module "projects" {
- source = "../../../../../examples/factories/project-factory"
- for_each = local.projects
- defaults = local.defaults
- project_id = each.key
- 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, [])
- essential_contacts = try(each.value.essential_contacts, [])
- folder_id = each.value.folder_id
- group_iam = try(each.value.group_iam, {})
- iam = try(each.value.iam, {})
- kms_service_agents = try(each.value.kms, {})
- labels = try(each.value.labels, {})
- org_policies = try(each.value.org_policies, null)
- service_accounts = try(each.value.service_accounts, {})
- services = try(each.value.services, [])
- service_identities_iam = try(each.value.service_identities_iam, {})
- vpc = try(each.value.vpc, null)
-}
diff --git a/tests/examples/factories/project_factory/fixture/projects/project.yaml b/tests/examples/factories/project_factory/fixture/projects/project.yaml
deleted file mode 100644
index 7ad1601605..0000000000
--- a/tests/examples/factories/project_factory/fixture/projects/project.yaml
+++ /dev/null
@@ -1,100 +0,0 @@
-# skip boilerplate check
-
-# [opt] Billing account id - overrides default if set
-billing_account_id: 012345-67890A-BCDEF0
-
-# [opt] Billing alerts config - overrides default if set
-billing_alert:
- amount: 10
- thresholds:
- current:
- - 0.5
- - 0.8
- forecasted: []
- credit_treatment: INCLUDE_ALL_CREDITS
-
-# [opt] DNS zones to be created as children of the environment_dns_zone defined in defaults
-dns_zones:
- - lorem
- - ipsum
-
-# [opt] Contacts for billing alerts and important notifications
-essential_contacts:
- - team-a-contacts@example.com
-
-# Folder the project will be created as children of
-folder_id: folders/012345678901
-
-# [opt] Authoritative IAM bindings in group => [roles] format
-group_iam:
- test-team-foobar@fast-lab-0.gcp-pso-italy.net:
- - roles/compute.admin
-
-# [opt] Authoritative IAM bindings in role => [principals] format
-# Generally used to grant roles to service accounts external to the project
-iam:
- roles/compute.admin:
- - serviceAccount:service-account
-
-# [opt] Service robots and keys they will be assigned as cryptoKeyEncrypterDecrypter
-# in service => [keys] format
-kms_service_agents:
- compute: [key1, key2]
- storage: [key1, key2]
-
-# [opt] Labels for the project - merged with the ones defined in defaults
-labels:
- environment: prod
-
-# [opt] Org policy overrides defined at project level
-org_policies:
- policy_boolean:
- constraints/compute.disableGuestAttributesAccess: true
- policy_list:
- constraints/compute.trustedImageProjects:
- inherit_from_parent: null
- status: true
- suggested_value: null
- values:
- - projects/fast-prod-iac-core-0
-
-# [opt] Service account to create for the project and their roles on the project
-# in name => [roles] format
-service_accounts:
- another-service-account:
- - roles/compute.admin
- my-service-account:
- - roles/compute.admin
-
-# [opt] APIs to enable on the project.
-services:
- - storage.googleapis.com
- - stackdriver.googleapis.com
- - compute.googleapis.com
-
-# [opt] Roles to assign to the service identities in service => [roles] format
-service_identities_iam:
- compute:
- - roles/storage.objectViewer
-
- # [opt] VPC setup.
- # If set enables the `compute.googleapis.com` service and configures
- # service project attachment
-vpc:
- # [opt] If set, enables the container API
- gke_setup:
- # Grants "roles/container.hostServiceAgentUser" to the container robot if set
- enable_host_service_agent: false
-
- # Grants "roles/compute.securityAdmin" to the container robot if set
- enable_security_admin: true
-
- # Host project the project will be service project of
- host_project: fast-prod-net-spoke-0
-
- # [opt] Subnets in the host project where principals will be granted networkUser
- # in region/subnet-name => [principals]
- subnets_iam:
- europe-west1/prod-default-ew1:
- - user:foobar@example.com
- - serviceAccount:service-account1
diff --git a/tests/examples/factories/project_factory/fixture/variables.tf b/tests/examples/factories/project_factory/fixture/variables.tf
deleted file mode 100644
index 0662bf78dd..0000000000
--- a/tests/examples/factories/project_factory/fixture/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 "billing_account_id" {
- description = "Billing account id."
- type = string
- default = "012345-67890A-BCDEF0"
-}
-
-variable "data_dir" {
- description = "Relative path for the folder storing configuration data."
- type = string
- default = "./projects/"
-}
-
-variable "environment_dns_zone" {
- description = "DNS zone suffix for environment."
- type = string
- default = "prod.gcp.example.com"
-}
-
-variable "defaults_file" {
- description = "Relative path for the file storing the project factory configuration."
- type = string
- default = "./defaults.yaml"
-}
-
-variable "shared_vpc_self_link" {
- description = "Self link for the shared VPC."
- type = string
- default = "self-link"
-}
-
-variable "vpc_host_project" {
- # tfdoc:variable:source 02-networking
- description = "Host project for the shared VPC."
- type = string
- default = "host-project"
-}
diff --git a/tests/examples/factories/project_factory/test_plan.py b/tests/examples/factories/project_factory/test_plan.py
deleted file mode 100644
index f609b214b3..0000000000
--- a/tests/examples/factories/project_factory/test_plan.py
+++ /dev/null
@@ -1,18 +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(e2e_plan_runner):
- "Check for a clean plan"
- modules, resources = e2e_plan_runner()
- assert len(modules) > 0 and len(resources) > 0
diff --git a/tests/examples/foundations/business_units/fixture/main.tf b/tests/examples/foundations/business_units/fixture/main.tf
deleted file mode 100644
index 8ccf770115..0000000000
--- a/tests/examples/foundations/business_units/fixture/main.tf
+++ /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.
- */
-
-module "test" {
- source = "../../../../../examples/foundations/business-units"
- billing_account_id = var.billing_account_id
- organization_id = var.organization_id
- prefix = var.prefix
- root_node = var.root_node
-}
diff --git a/tests/examples/foundations/business_units/fixture/variables.tf b/tests/examples/foundations/business_units/fixture/variables.tf
deleted file mode 100644
index a89975953d..0000000000
--- a/tests/examples/foundations/business_units/fixture/variables.tf
+++ /dev/null
@@ -1,35 +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
-#
-# 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.
-
-variable "billing_account_id" {
- type = string
- default = "1234-5678-9012"
-}
-
-variable "organization_id" {
- type = string
- default = "organizations/1234567890"
-}
-
-variable "prefix" {
- description = "Prefix used for resources that need unique names."
- type = string
- default = "test"
-}
-
-variable "root_node" {
- description = "Root node for the new hierarchy, either 'organizations/org_id' or 'folders/folder_id'."
- type = string
- default = "folders/1234567890"
-}
diff --git a/tests/examples/foundations/business_units/test_plan.py b/tests/examples/foundations/business_units/test_plan.py
deleted file mode 100644
index 816d514b6b..0000000000
--- a/tests/examples/foundations/business_units/test_plan.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright 2022 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-def test_resources(e2e_plan_runner):
- "Test that plan works and the numbers of resources is as expected."
- modules, resources = e2e_plan_runner()
- assert len(modules) == 8
- assert len(resources) == 83
diff --git a/tests/examples/foundations/environments/fixture/main.tf b/tests/examples/foundations/environments/fixture/main.tf
deleted file mode 100644
index c6d413dc4b..0000000000
--- a/tests/examples/foundations/environments/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 = "../../../../../examples/foundations/environments"
- billing_account_id = var.billing_account_id
- environments = var.environments
- iam_audit_viewers = var.iam_audit_viewers
- iam_shared_owners = var.iam_shared_owners
- iam_terraform_owners = var.iam_terraform_owners
- iam_xpn_config = var.iam_xpn_config
- organization_id = var.organization_id
- prefix = var.prefix
- root_node = var.root_node
-}
diff --git a/tests/examples/foundations/environments/fixture/variables.tf b/tests/examples/foundations/environments/fixture/variables.tf
deleted file mode 100644
index 48ce5fde73..0000000000
--- a/tests/examples/foundations/environments/fixture/variables.tf
+++ /dev/null
@@ -1,66 +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
-#
-# 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.
-
-variable "billing_account_id" {
- type = string
- default = "1234-5678-9012"
-}
-
-variable "environments" {
- type = list(string)
- default = ["test", "prod"]
-}
-
-variable "iam_audit_viewers" {
- type = list(string)
- default = ["user:audit-1@example.org", "user:audit2@example.org"]
-}
-
-variable "iam_shared_owners" {
- type = list(string)
- default = ["user:shared-1@example.org", "user:shared-2@example.org"]
-}
-
-variable "iam_terraform_owners" {
- type = list(string)
- default = ["user:tf-1@example.org", "user:tf-2@example.org"]
-}
-
-variable "iam_xpn_config" {
- type = object({
- grant = bool
- target_org = bool
- })
- default = {
- grant = true
- target_org = false
- }
-}
-
-variable "organization_id" {
- type = string
- default = ""
-}
-
-variable "prefix" {
- description = "Prefix used for resources that need unique names."
- type = string
- default = "test"
-}
-
-variable "root_node" {
- description = "Root node for the new hierarchy, either 'organizations/org_id' or 'folders/folder_id'."
- type = string
- default = "folders/1234567890"
-}
diff --git a/tests/examples/foundations/environments/test_plan.py b/tests/examples/foundations/environments/test_plan.py
deleted file mode 100644
index 7e8762df48..0000000000
--- a/tests/examples/foundations/environments/test_plan.py
+++ /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.
-
-
-def test_folder_roles(e2e_plan_runner):
- "Test folder roles."
- modules, _ = e2e_plan_runner(refresh=False)
- for env in ['test', 'prod']:
- resources = modules[f'module.test.module.environment-folders["{env}"]']
- folders = [r for r in resources if r['type'] == 'google_folder']
- assert len(folders) == 1
- folder = folders[0]
- assert folder['values']['display_name'] == env
-
- bindings = [r['index']
- for r in resources if r['type'] == 'google_folder_iam_binding']
- assert len(bindings) == 5
-
-
-def test_org_roles(e2e_plan_runner):
- "Test folder roles."
- tf_vars = {
- 'organization_id': 'organizations/123',
- 'iam_xpn_config': '{grant = true, target_org = true}'
- }
- modules, _ = e2e_plan_runner(refresh=False, **tf_vars)
- for env in ['test', 'prod']:
- resources = modules[f'module.test.module.environment-folders["{env}"]']
- folder_bindings = [r['index']
- for r in resources if r['type'] == 'google_folder_iam_binding']
- assert len(folder_bindings) == 4
-
- resources = modules[f'module.test.module.tf-service-accounts["{env}"]']
- org_bindings = [r for r in resources
- if r['type'] == 'google_organization_iam_member']
- assert len(org_bindings) == 2
- assert {b['values']['role'] for b in org_bindings} == {
- 'roles/resourcemanager.organizationViewer',
- 'roles/compute.xpnAdmin'
- }
diff --git a/tests/examples/networking/decentralized_firewall/fixture/main.tf b/tests/examples/networking/decentralized_firewall/fixture/main.tf
deleted file mode 100644
index a5df532177..0000000000
--- a/tests/examples/networking/decentralized_firewall/fixture/main.tf
+++ /dev/null
@@ -1,22 +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 = "../../../../../examples/networking/decentralized-firewall"
- billing_account_id = var.billing_account_id
- prefix = var.prefix
- root_node = var.root_node
-}
diff --git a/tests/examples/networking/filtering_proxy/fixture/main.tf b/tests/examples/networking/filtering_proxy/fixture/main.tf
deleted file mode 100644
index 2f508edc72..0000000000
--- a/tests/examples/networking/filtering_proxy/fixture/main.tf
+++ /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.
- */
-
-module "test" {
- source = "../../../../../examples/networking/filtering-proxy"
- billing_account = "123456-123456-123456"
- mig = var.mig
- prefix = "fabric"
- root_node = "folders/123456789"
-}
diff --git a/tests/examples/networking/hub_and_spoke_peering/fixture/main.tf b/tests/examples/networking/hub_and_spoke_peering/fixture/main.tf
deleted file mode 100644
index 7a08f84511..0000000000
--- a/tests/examples/networking/hub_and_spoke_peering/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 "test" {
- source = "../../../../../examples/networking/hub-and-spoke-peering"
- project_create = {
- billing_account = "123456-123456-123456"
- oslogin = true
- parent = "folders/123456789"
- }
- project_id = var.project_id
-}
diff --git a/tests/examples/networking/hub_and_spoke_peering/test_plan.py b/tests/examples/networking/hub_and_spoke_peering/test_plan.py
deleted file mode 100644
index d63fc05728..0000000000
--- a/tests/examples/networking/hub_and_spoke_peering/test_plan.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright 2022 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-def test_resources(e2e_plan_runner):
- "Test that plan works and the numbers of resources is as expected."
- modules, resources = e2e_plan_runner()
- assert len(modules) == 21
- assert len(resources) == 61
diff --git a/tests/examples/networking/hub_and_spoke_vpn/fixture/main.tf b/tests/examples/networking/hub_and_spoke_vpn/fixture/main.tf
deleted file mode 100644
index 5dc7d2aaf4..0000000000
--- a/tests/examples/networking/hub_and_spoke_vpn/fixture/main.tf
+++ /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.
- */
-
-module "test" {
- source = "../../../../../examples/networking/hub-and-spoke-vpn"
- project_id = var.project_id
-}
diff --git a/tests/examples/networking/hub_and_spoke_vpn/fixture/variables.tf b/tests/examples/networking/hub_and_spoke_vpn/fixture/variables.tf
deleted file mode 100644
index 626af01198..0000000000
--- a/tests/examples/networking/hub_and_spoke_vpn/fixture/variables.tf
+++ /dev/null
@@ -1,18 +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
-#
-# 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.
-
-variable "project_id" {
- type = string
- default = "project-1"
-}
diff --git a/tests/examples/networking/hub_and_spoke_vpn/test_plan.py b/tests/examples/networking/hub_and_spoke_vpn/test_plan.py
deleted file mode 100644
index 7d7716824b..0000000000
--- a/tests/examples/networking/hub_and_spoke_vpn/test_plan.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright 2022 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-def test_resources(e2e_plan_runner):
- "Test that plan works and the numbers of resources is as expected."
- modules, resources = e2e_plan_runner()
- assert len(modules) == 17
- assert len(resources) == 71
diff --git a/tests/examples/networking/ilb_next_hop/fixture/main.tf b/tests/examples/networking/ilb_next_hop/fixture/main.tf
deleted file mode 100644
index 151487dc83..0000000000
--- a/tests/examples/networking/ilb_next_hop/fixture/main.tf
+++ /dev/null
@@ -1,21 +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 = "../../../../../examples/networking/ilb-next-hop"
- project_create = var.project_create
- project_id = var.project_id
-}
diff --git a/tests/examples/networking/ilb_next_hop/fixture/variables.tf b/tests/examples/networking/ilb_next_hop/fixture/variables.tf
deleted file mode 100644
index 3d884c2522..0000000000
--- a/tests/examples/networking/ilb_next_hop/fixture/variables.tf
+++ /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
-#
-# 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.
-
-variable "project_create" {
- type = bool
- default = true
-}
-
-variable "project_id" {
- type = string
- default = "project-1"
-}
diff --git a/tests/examples/networking/onprem_google_access_dns/fixture/main.tf b/tests/examples/networking/onprem_google_access_dns/fixture/main.tf
deleted file mode 100644
index 6f802d424d..0000000000
--- a/tests/examples/networking/onprem_google_access_dns/fixture/main.tf
+++ /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.
- */
-
-module "test" {
- source = "../../../../../examples/networking/onprem-google-access-dns"
- project_id = var.project_id
-}
diff --git a/tests/examples/networking/onprem_google_access_dns/fixture/variables.tf b/tests/examples/networking/onprem_google_access_dns/fixture/variables.tf
deleted file mode 100644
index 626af01198..0000000000
--- a/tests/examples/networking/onprem_google_access_dns/fixture/variables.tf
+++ /dev/null
@@ -1,18 +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
-#
-# 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.
-
-variable "project_id" {
- type = string
- default = "project-1"
-}
diff --git a/tests/examples/networking/private_cloud_function_from_onprem/fixture/main.tf b/tests/examples/networking/private_cloud_function_from_onprem/fixture/main.tf
deleted file mode 100644
index 13f142e236..0000000000
--- a/tests/examples/networking/private_cloud_function_from_onprem/fixture/main.tf
+++ /dev/null
@@ -1,24 +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 = "../../../../../examples/networking/private-cloud-function-from-onprem"
- project_create = {
- billing_account_id = "123456-ABCDEF-123456"
- parent = "folders/1234567890"
- }
- project_id = "test-project"
-}
diff --git a/tests/examples/networking/shared_vpc_gke/fixture/main.tf b/tests/examples/networking/shared_vpc_gke/fixture/main.tf
deleted file mode 100644
index 562812b00d..0000000000
--- a/tests/examples/networking/shared_vpc_gke/fixture/main.tf
+++ /dev/null
@@ -1,22 +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 = "../../../../../examples/networking/shared-vpc-gke"
- billing_account_id = var.billing_account_id
- prefix = var.prefix
- root_node = var.root_node
-}
diff --git a/tests/examples/serverless/api_gateway/fixture/main.tf b/tests/examples/serverless/api_gateway/fixture/main.tf
deleted file mode 100644
index b36e2fd4b2..0000000000
--- a/tests/examples/serverless/api_gateway/fixture/main.tf
+++ /dev/null
@@ -1,22 +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 = "../../../../../examples/serverless/api-gateway"
- project_create = var.project_create
- project_id = var.project_id
- regions = var.regions
-}
diff --git a/tests/examples/test_plan.py b/tests/examples/test_plan.py
new file mode 100644
index 0000000000..5f902cbe7e
--- /dev/null
+++ b/tests/examples/test_plan.py
@@ -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.
+
+import re
+import subprocess
+from pathlib import Path
+
+BASE_PATH = Path(__file__).parent
+COUNT_TEST_RE = re.compile(r'# tftest +modules=(\d+) +resources=(\d+)' +
+ r'(?: +files=([\w,-.]+))?' +
+ r'(?: +inventory=([\w\-.]+))?')
+
+
+def test_example(plan_validator, tmp_path, example):
+ if match := COUNT_TEST_RE.search(example.code):
+ (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:
+ destination = tmp_path / example.files[f].path
+ destination.parent.mkdir(parents=True, exist_ok=True)
+ destination.write_text(example.files[f].content)
+
+ 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)
+
+ # 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
new file mode 100644
index 0000000000..3a5a3f758b
--- /dev/null
+++ b/tests/examples/variables.tf
@@ -0,0 +1,91 @@
+# 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.
+
+# common variables used for examples
+
+variable "bucket" {
+ default = "bucket"
+}
+
+variable "billing_account_id" {
+ default = "123456-123456-123456"
+}
+
+variable "kms_key" {
+ default = {
+ self_link = "kms_key_self_link"
+ }
+}
+
+variable "organization_id" {
+ default = "organizations/1122334455"
+}
+
+variable "folder_id" {
+ default = "folders/1122334455"
+}
+
+variable "prefix" {
+ default = "test"
+}
+
+variable "project_id" {
+ default = "project-id"
+}
+
+variable "region" {
+ default = "region"
+}
+
+variable "service_account" {
+ default = {
+ id = "service_account_id"
+ email = "service_account_email"
+ iam_email = "service_account_iam_email"
+ }
+}
+
+variable "subnet" {
+ default = {
+ name = "subnet_name"
+ region = "subnet_region"
+ cidr = "subnet_cidr"
+ self_link = "subnet_self_link"
+ }
+}
+
+variable "vpc" {
+ default = {
+ name = "vpc_name"
+ self_link = "projects/xxx/global/networks/aaa"
+ }
+}
+
+variable "vpc1" {
+ default = {
+ name = "vpc_name"
+ self_link = "projects/xxx/global/networks/bbb"
+ }
+}
+
+variable "vpc2" {
+ default = {
+ name = "vpc2_name"
+ self_link = "projects/xxx/global/networks/ccc"
+ }
+}
+
+variable "zone" {
+ default = "zone"
+}
diff --git a/tests/fast/conftest.py b/tests/fast/conftest.py
deleted file mode 100644
index d96af5dcfe..0000000000
--- a/tests/fast/conftest.py
+++ /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.
-
-"Shared fixtures"
-
-import inspect
-import os
-import types
-
-import pytest
-import tftest
-
-
-BASEDIR = os.path.dirname(os.path.dirname(__file__))
-
-
-@pytest.fixture(scope='session')
-def fast_e2e_plan_runner(_plan_runner):
- "Plan runner for end-to-end root module, returns modules and resources."
- def run_plan(fixture_path=None, targets=None, refresh=True,
- include_bare_resources=False, compute_sums=True, **tf_vars):
- "Runs Terraform plan on a root module using defaults, returns data."
- plan = _plan_runner(fixture_path, targets=targets, refresh=refresh,
- **tf_vars)
- root_module = plan.root_module['child_modules'][0]
- modules = {
- m['address'].removeprefix(root_module['address'])[1:]: m['resources']
- for m in root_module['child_modules']
- }
- resources = [r for m in modules.values() for r in m]
- if include_bare_resources:
- bare_resources = root_module['resources']
- resources.extend(bare_resources)
- if compute_sums:
- return len(modules), len(resources), {k: len(v) for k, v in modules.items()}
- return modules, resources
- return run_plan
diff --git a/tests/fast/stages/s00_bootstrap/fixture/main.tf b/tests/fast/stages/s00_bootstrap/fixture/main.tf
deleted file mode 100644
index 1f07048ad5..0000000000
--- a/tests/fast/stages/s00_bootstrap/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 "stage" {
- source = "../../../../../fast/stages/00-bootstrap"
- prefix = "fast"
- organization = {
- domain = "fast.example.com"
- id = 123456789012
- customer_id = "C00000000"
- }
- billing_account = {
- id = "000000-111111-222222"
- organization_id = 123456789012
- }
-}
diff --git a/tests/fast/stages/s00_bootstrap/simple.tfvars b/tests/fast/stages/s00_bootstrap/simple.tfvars
new file mode 100644
index 0000000000..f8ef5735bd
--- /dev/null
+++ b/tests/fast/stages/s00_bootstrap/simple.tfvars
@@ -0,0 +1,11 @@
+organization = {
+ domain = "fast.example.com"
+ id = 123456789012
+ customer_id = "C00000000"
+}
+billing_account = {
+ id = "000000-111111-222222"
+ organization_id = 123456789012
+}
+prefix = "fast"
+outputs_location = "/fast-config"
diff --git a/tests/fast/stages/s00_bootstrap/simple.yaml b/tests/fast/stages/s00_bootstrap/simple.yaml
new file mode 100644
index 0000000000..703b84b456
--- /dev/null
+++ b/tests/fast/stages/s00_bootstrap/simple.yaml
@@ -0,0 +1,49 @@
+# 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_dataset: 2
+ google_bigquery_dataset_iam_member: 2
+ 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_member: 16
+ google_project: 3
+ google_project_iam_binding: 9
+ google_project_iam_member: 1
+ 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_storage_bucket_object: 5
+ google_storage_project_service_account: 3
+ local_file: 5
+
+outputs:
+ custom_roles:
+ organization_iam_admin: organizations/123456789012/roles/organizationIamAdmin
+ service_project_network_admin: organizations/123456789012/roles/serviceProjectNetworkAdmin
+ outputs_bucket: fast-prod-iac-core-outputs-0
+ project_ids:
+ automation: fast-prod-iac-core-0
+ billing-export: fast-prod-billing-exp-0
+ 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/s00_bootstrap/simple_projects.yaml
new file mode 100644
index 0000000000..c4d359f336
--- /dev/null
+++ b/tests/fast/stages/s00_bootstrap/simple_projects.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.automation-project.google_project.project[0]:
+ auto_create_network: false
+ billing_account: 000000-111111-222222
+ name: fast-prod-iac-core-0
+ org_id: '123456789012'
+ project_id: fast-prod-iac-core-0
+ module.billing-export-project[0].google_project.project[0]:
+ auto_create_network: false
+ billing_account: 000000-111111-222222
+ name: fast-prod-billing-exp-0
+ org_id: '123456789012'
+ project_id: fast-prod-billing-exp-0
+ module.log-export-project.google_project.project[0]:
+ auto_create_network: false
+ billing_account: 000000-111111-222222
+ name: fast-prod-audit-logs-0
+ org_id: '123456789012'
+ project_id: fast-prod-audit-logs-0
diff --git a/tests/fast/stages/s00_bootstrap/simple_sas.yaml b/tests/fast/stages/s00_bootstrap/simple_sas.yaml
new file mode 100644
index 0000000000..ba84948d86
--- /dev/null
+++ b/tests/fast/stages/s00_bootstrap/simple_sas.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.automation-tf-bootstrap-sa.google_service_account.service_account[0]:
+ 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.
+ project: fast-prod-iac-core-0
diff --git a/tests/fast/stages/s00_bootstrap/test_plan.py b/tests/fast/stages/s00_bootstrap/test_plan.py
deleted file mode 100644
index 2201cfcc45..0000000000
--- a/tests/fast/stages/s00_bootstrap/test_plan.py
+++ /dev/null
@@ -1,33 +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_COUNT = {
-# 'module.organization': 28,
-# 'module.automation-project': 23,
-# 'module.automation-tf-bootstrap-gcs': 1,
-# 'module.automation-tf-bootstrap-sa': 1,
-# 'module.automation-tf-resman-gcs': 2,
-# 'module.automation-tf-resman-sa': 1,
-# 'module.billing-export-dataset': 1,
-# 'module.billing-export-project': 7,
-# 'module.log-export-dataset': 1,
-# 'module.log-export-project': 7,
-# }
-
-
-def test_counts(fast_e2e_plan_runner):
- "Test stage."
- # TODO: to re-enable per-module resource count check print _, then test
- num_modules, num_resources, _ = fast_e2e_plan_runner()
- assert num_modules > 0 and num_resources > 0
diff --git a/tests/fast/stages/s00_bootstrap/tftest.yaml b/tests/fast/stages/s00_bootstrap/tftest.yaml
new file mode 100644
index 0000000000..4656859bc2
--- /dev/null
+++ b/tests/fast/stages/s00_bootstrap/tftest.yaml
@@ -0,0 +1,12 @@
+# skip boilerplate check
+
+module: fast/stages/00-bootstrap
+
+tests:
+ simple:
+ tfvars:
+ - simple.tfvars
+ inventory:
+ - simple.yaml
+ - simple_projects.yaml
+ - simple_sas.yaml
diff --git a/tests/fast/stages/s01_resman/common.tfvars b/tests/fast/stages/s01_resman/common.tfvars
new file mode 100644
index 0000000000..f6d1d5acf4
--- /dev/null
+++ b/tests/fast/stages/s01_resman/common.tfvars
@@ -0,0 +1,29 @@
+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/s01_resman/fixture/main.tf b/tests/fast/stages/s01_resman/fixture/main.tf
deleted file mode 100644
index e4e1bdf355..0000000000
--- a/tests/fast/stages/s01_resman/fixture/main.tf
+++ /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.
- */
-
-module "stage" {
- source = "../../../../../fast/stages/01-resman"
- automation_project_id = "fast-prod-automation"
- 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/s01_resman/test_plan.py b/tests/fast/stages/s01_resman/test_plan.py
index 6189f62e3c..c8dce75082 100644
--- a/tests/fast/stages/s01_resman/test_plan.py
+++ b/tests/fast/stages/s01_resman/test_plan.py
@@ -13,8 +13,9 @@
# limitations under the License.
-def test_counts(fast_e2e_plan_runner):
+def test_counts(plan_summary):
"Test stage."
- num_modules, num_resources, _ = fast_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/01-resman",
+ tf_var_files=["common.tfvars"])
+ assert summary.counts["modules"] > 0
+ assert summary.counts["resources"] > 0
diff --git a/tests/fast/stages/s02_networking_nva/common.tfvars b/tests/fast/stages/s02_networking_nva/common.tfvars
new file mode 100644
index 0000000000..acfc641f33
--- /dev/null
+++ b/tests/fast/stages/s02_networking_nva/common.tfvars
@@ -0,0 +1,30 @@
+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_nva/fixture/main.tf b/tests/fast/stages/s02_networking_nva/fixture/main.tf
deleted file mode 100644
index e978cf9ef9..0000000000
--- a/tests/fast/stages/s02_networking_nva/fixture/main.tf
+++ /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.
- */
-
-module "stage" {
- source = "../../../../../fast/stages/02-networking-nva"
- data_dir = "../../../../../fast/stages/02-networking-nva/data/"
- 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_networking_nva/test_plan.py b/tests/fast/stages/s02_networking_nva/test_plan.py
index 6189f62e3c..24964f7abe 100644
--- a/tests/fast/stages/s02_networking_nva/test_plan.py
+++ b/tests/fast/stages/s02_networking_nva/test_plan.py
@@ -13,8 +13,9 @@
# limitations under the License.
-def test_counts(fast_e2e_plan_runner):
+def test_counts(plan_summary):
"Test stage."
- num_modules, num_resources, _ = fast_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/02-networking-nva",
+ tf_var_files=["common.tfvars"])
+ assert summary.counts["modules"] > 0
+ assert summary.counts["resources"] > 0
diff --git a/tests/fast/stages/s02_networking_peering/common.tfvars b/tests/fast/stages/s02_networking_peering/common.tfvars
new file mode 100644
index 0000000000..11b49d7c04
--- /dev/null
+++ b/tests/fast/stages/s02_networking_peering/common.tfvars
@@ -0,0 +1,35 @@
+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_peering/fixture/main.tf b/tests/fast/stages/s02_networking_peering/fixture/main.tf
deleted file mode 100644
index b06bad39fb..0000000000
--- a/tests/fast/stages/s02_networking_peering/fixture/main.tf
+++ /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.
- */
-
-module "stage" {
- source = "../../../../../fast/stages/02-networking-peering"
- data_dir = "../../../../../fast/stages/02-networking-peering/data/"
- 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_networking_peering/test_plan.py b/tests/fast/stages/s02_networking_peering/test_plan.py
index 6d38e66127..dff0fd47b6 100644
--- a/tests/fast/stages/s02_networking_peering/test_plan.py
+++ b/tests/fast/stages/s02_networking_peering/test_plan.py
@@ -27,23 +27,27 @@
STAGE_VPN = STAGES / '02-networking-vpn'
-def test_counts(fast_e2e_plan_runner):
- 'Test stage.'
- num_modules, num_resources, _ = fast_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/02-networking-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)
- ddiff = DeepDiff(plan_vpn, plan_peering, ignore_order=True,
- group_by='address', view='tree')
+ summary_peering = plan_summary("fast/stages/02-networking-peering",
+ tf_var_files=["common.tfvars"])
+ summary_vpn = plan_summary("fast/stages/02-networking-vpn",
+ tf_var_files=["common.tfvars"])
- removed_types = {x.t1['type'] for x in ddiff['dictionary_item_removed']}
- added_types = {x.t2['type'] for x in ddiff['dictionary_item_added']}
+ ddiff = DeepDiff(summary_vpn.values, summary_peering.values,
+ ignore_order=True)
+
+ 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_networking_separate_envs/__init__.py b/tests/fast/stages/s02_networking_separate_envs/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/fast/stages/s02_networking_separate_envs/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/fast/stages/s02_networking_separate_envs/common.tfvars b/tests/fast/stages/s02_networking_separate_envs/common.tfvars
new file mode 100644
index 0000000000..c6b793fd1d
--- /dev/null
+++ b/tests/fast/stages/s02_networking_separate_envs/common.tfvars
@@ -0,0 +1,28 @@
+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_networking_separate_envs/test_plan.py b/tests/fast/stages/s02_networking_separate_envs/test_plan.py
new file mode 100644
index 0000000000..89f6e25743
--- /dev/null
+++ b/tests/fast/stages/s02_networking_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/02-networking-separate-envs",
+ tf_var_files=["common.tfvars"])
+ assert summary.counts["modules"] > 0
+ assert summary.counts["resources"] > 0
diff --git a/tests/fast/stages/s02_networking_vpn/common.tfvars b/tests/fast/stages/s02_networking_vpn/common.tfvars
new file mode 100644
index 0000000000..7241594d1c
--- /dev/null
+++ b/tests/fast/stages/s02_networking_vpn/common.tfvars
@@ -0,0 +1,35 @@
+data_dir = "../../../../../fast/stages/02-networking-vpn/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_vpn/fixture/main.tf b/tests/fast/stages/s02_networking_vpn/fixture/main.tf
index 9a736685d8..2ddb024fc5 100644
--- a/tests/fast/stages/s02_networking_vpn/fixture/main.tf
+++ b/tests/fast/stages/s02_networking_vpn/fixture/main.tf
@@ -17,6 +17,9 @@
module "stage" {
source = "../../../../../fast/stages/02-networking-vpn"
data_dir = "../../../../../fast/stages/02-networking-vpn/data/"
+ automation = {
+ outputs_bucket = "test"
+ }
billing_account = {
id = "000000-111111-222222"
organization_id = 123456789012
@@ -29,9 +32,16 @@ module "stage" {
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"
}
diff --git a/tests/fast/stages/s02_networking_vpn/test_plan.py b/tests/fast/stages/s02_networking_vpn/test_plan.py
index 6189f62e3c..cc62dbf736 100644
--- a/tests/fast/stages/s02_networking_vpn/test_plan.py
+++ b/tests/fast/stages/s02_networking_vpn/test_plan.py
@@ -13,8 +13,9 @@
# limitations under the License.
-def test_counts(fast_e2e_plan_runner):
+def test_counts(plan_summary):
"Test stage."
- num_modules, num_resources, _ = fast_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/02-networking-vpn",
+ tf_var_files=["common.tfvars"])
+ assert summary.counts["modules"] > 0
+ assert summary.counts["resources"] > 0
diff --git a/tests/fast/stages/s02_security/common.tfvars b/tests/fast/stages/s02_security/common.tfvars
new file mode 100644
index 0000000000..b480a67320
--- /dev/null
+++ b/tests/fast/stages/s02_security/common.tfvars
@@ -0,0 +1,88 @@
+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/fixture/main.tf b/tests/fast/stages/s02_security/fixture/main.tf
deleted file mode 100644
index 14e2eb5b55..0000000000
--- a/tests/fast/stages/s02_security/fixture/main.tf
+++ /dev/null
@@ -1,111 +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"
- 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"
- project-factory-dev = "foobar@iam.gserviceaccount.com"
- project-factory-prod = "foobar@iam.gserviceaccount.com"
- }
- vpc_sc_ingress_policies = {
- iac = {
- ingress_from = {
- identities = [
- "serviceAccount:fast-prod-resman-security-0@fast-prod-iac-core-0.iam.gserviceaccount.com"
- ],
- source_access_levels = ["*"], identity_type = null, source_resources = null
- }
- ingress_to = {
- operations = [{ method_selectors = [], service_name = "*" }]
- resources = ["*"]
- }
- }
- }
- vpc_sc_perimeter_ingress_policies = {
- dev = ["iac"]
- landing = null
- prod = ["iac"]
- }
- vpc_sc_perimeter_projects = {
- dev = [
- "projects/345678912", # ludo-dev-sec-core-0
- ]
- landing = []
- prod = [
- "projects/234567891", # ludo-prod-sec-core-0
- ]
- }
-
- vpc_sc_access_levels = {
- all = {
- combining_function = null
- conditions = [{
- members = [
- "serviceAccount:quota-monitor@foobar.iam.gserviceaccount.com",
- ],
- ip_subnetworks = null, negate = null, regions = null,
- required_access_levels = null
- }]
- }
- }
-
- vpc_sc_perimeter_access_levels = {
- dev = ["all"]
- landing = null
- prod = ["all"]
- }
-
- vpc_sc_egress_policies = {
- iac-gcs = {
- egress_from = {
- identity_type = null
- identities = [
- "serviceAccount:fast-prod-resman-security-0@fast-prod-iac-core-0.iam.gserviceaccount.com"
- ]
- }
- egress_to = {
- operations = [{
- method_selectors = ["*"], service_name = "storage.googleapis.com"
- }]
- resources = ["projects/123456789"]
- }
- }
- }
-}
diff --git a/tests/fast/stages/s02_security/test_plan.py b/tests/fast/stages/s02_security/test_plan.py
index 6189f62e3c..004f6b8368 100644
--- a/tests/fast/stages/s02_security/test_plan.py
+++ b/tests/fast/stages/s02_security/test_plan.py
@@ -13,8 +13,9 @@
# limitations under the License.
-def test_counts(fast_e2e_plan_runner):
+def test_counts(plan_summary):
"Test stage."
- num_modules, num_resources, _ = fast_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/02-security",
+ 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/s03_data_platform/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/fast/stages/s03_data_platform/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/fast/stages/s03_data_platform/common.tfvars b/tests/fast/stages/s03_data_platform/common.tfvars
new file mode 100644
index 0000000000..f5aada165d
--- /dev/null
+++ b/tests/fast/stages/s03_data_platform/common.tfvars
@@ -0,0 +1,26 @@
+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/fixture/main.tf b/tests/fast/stages/s03_data_platform/fixture/main.tf
deleted file mode 100644
index e7fd8d4d43..0000000000
--- a/tests/fast/stages/s03_data_platform/fixture/main.tf
+++ /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.
- */
-
-# tfdoc: Data platform stage test
-
-module "stage" {
- source = "../../../../../fast/stages/03-data-platform/dev/"
- billing_account = {
- id = "012345-67890A-BCDEF0",
- organization_id = 123456
- }
- folder_ids = {
- data-platform = "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
index 6189f62e3c..0bb333e711 100644
--- a/tests/fast/stages/s03_data_platform/test_plan.py
+++ b/tests/fast/stages/s03_data_platform/test_plan.py
@@ -13,8 +13,9 @@
# limitations under the License.
-def test_counts(fast_e2e_plan_runner):
+def test_counts(plan_summary):
"Test stage."
- num_modules, num_resources, _ = fast_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/03-data-platform/dev/",
+ 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/s03_gke_multitenant/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/fast/stages/s03_gke_multitenant/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/fast/stages/s03_gke_multitenant/common.tfvars b/tests/fast/stages/s03_gke_multitenant/common.tfvars
new file mode 100644
index 0000000000..d22db1d265
--- /dev/null
+++ b/tests/fast/stages/s03_gke_multitenant/common.tfvars
@@ -0,0 +1,41 @@
+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
new file mode 100644
index 0000000000..2d196ec46b
--- /dev/null
+++ b/tests/fast/stages/s03_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/03-gke-multitenant/dev/",
+ tf_var_files=["common.tfvars"])
+ assert summary.counts["modules"] > 0
+ assert summary.counts["resources"] > 0
diff --git a/tests/fast/stages/s03_project_factory/common.tfvars b/tests/fast/stages/s03_project_factory/common.tfvars
new file mode 100644
index 0000000000..b65956b6b8
--- /dev/null
+++ b/tests/fast/stages/s03_project_factory/common.tfvars
@@ -0,0 +1,11 @@
+data_dir = "../../../../tests/fast/stages/s03_project_factory/data/projects/"
+defaults_file = "../../../../tests/fast/stages/s03_project_factory/data/defaults.yaml"
+prefix = "test"
+environment_dns_zone = "dev"
+billing_account = {
+ id = "000000-111111-222222"
+ organization_id = 123456789012
+}
+vpc_self_links = {
+ dev-spoke-0 = "link"
+}
diff --git a/tests/fast/stages/s03_project_factory/fixture/data/defaults.yaml b/tests/fast/stages/s03_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/s03_project_factory/data/defaults.yaml
diff --git a/tests/fast/stages/s03_project_factory/fixture/data/projects/project.yaml b/tests/fast/stages/s03_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/s03_project_factory/data/projects/project.yaml
diff --git a/tests/fast/stages/s03_project_factory/fixture/main.tf b/tests/fast/stages/s03_project_factory/fixture/main.tf
deleted file mode 100644
index 420cc25677..0000000000
--- a/tests/fast/stages/s03_project_factory/fixture/main.tf
+++ /dev/null
@@ -1,32 +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 "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
- }
- vpc_self_links = {
- dev-spoke-0 = "link"
- }
-}
-
-
diff --git a/tests/fast/stages/s03_project_factory/test_plan.py b/tests/fast/stages/s03_project_factory/test_plan.py
index 6189f62e3c..3b284abbfc 100644
--- a/tests/fast/stages/s03_project_factory/test_plan.py
+++ b/tests/fast/stages/s03_project_factory/test_plan.py
@@ -13,8 +13,9 @@
# limitations under the License.
-def test_counts(fast_e2e_plan_runner):
+def test_counts(plan_summary):
"Test stage."
- num_modules, num_resources, _ = fast_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/03-project-factory/dev",
+ tf_var_files=["common.tfvars"])
+ assert summary.counts["modules"] > 0
+ assert summary.counts["resources"] > 0
diff --git a/tests/fixtures.py b/tests/fixtures.py
new file mode 100644
index 0000000000..788f81786f
--- /dev/null
+++ b/tests/fixtures.py
@@ -0,0 +1,238 @@
+# 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.
+"""Common fixtures."""
+
+import collections
+import contextlib
+import itertools
+import os
+import shutil
+import tempfile
+from pathlib import Path
+
+import pytest
+import tftest
+import yaml
+
+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.
+ Otherwise, `path` is simply returned untouched.
+ """
+ if os.environ.get('TFTEST_COPY'):
+ # if the TFTEST_COPY is set, create temp dir and copy the root
+ # module there
+ with tempfile.TemporaryDirectory(dir=path.parent) as tmp_path:
+ tmp_path = Path(tmp_path)
+
+ # if we're copying the module, we might as well ignore files and
+ # directories that are automatically read by terraform. Useful
+ # to avoid surprises if, for example, you have an active fast
+ # deployment with links to configs)
+ ignore_patterns = shutil.ignore_patterns('*.auto.tfvars',
+ '*.auto.tfvars.json',
+ 'terraform.tfstate*',
+ 'terraform.tfvars', '.terraform')
+
+ shutil.copytree(path, tmp_path, dirs_exist_ok=True,
+ ignore=ignore_patterns)
+
+ yield tmp_path
+ else:
+ # if TFTEST_COPY is not set, just return the same path
+ yield path
+
+
+def plan_summary(module_path, basedir, tf_var_files=None, **tf_vars):
+ """
+ Run a Terraform plan on the module located at `module_path`.
+
+ - module_path: terraform root module to run. Can be an absolute
+ path or relative to the root of the repository
+
+ - basedir: directory root to use for relative paths in
+ tf_var_files.
+
+ - tf_var_files: set of terraform variable files (tfvars) to pass
+ in to terraform
+
+ Returns a PlanSummary object containing 3 attributes:
+ - values: dictionary where the keys are terraform plan addresses
+ and values are the JSON representation (converted to python
+ types) of the attribute values of the resource.
+
+ - counts: dictionary where the keys are the terraform resource
+ types and the values are the number of times that type appears
+ in the plan
+
+ - outputs: dictionary of the modules outputs that can be
+ determined at plan type.
+
+ Consult [1] for mode details on the structure of values and outputs
+
+ [1] https://developer.hashicorp.com/terraform/internals/json-format
+ """
+ # make the module_path relative to the root of the repo while still
+ # supporting absolute paths
+ module_path = Path(__file__).parents[1] / module_path
+ with _prepare_root_module(module_path) as test_path:
+ binary = os.environ.get('TERRAFORM', 'terraform')
+ tf = tftest.TerraformTest(test_path, binary=binary)
+ tf.setup(upgrade=True)
+ tf_var_files = [(basedir / x).resolve() for x in tf_var_files or []]
+ plan = tf.plan(output=True, tf_var_file=tf_var_files, tf_vars=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()
+
+ if 'type' in e:
+ counts[e['type']] += 1
+ if 'values' in e:
+ 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
+ outputs = plan.get('planned_values', {}).get('outputs', {})
+
+ return PlanSummary(values, dict(counts), outputs)
+
+
+@pytest.fixture(name='plan_summary')
+def plan_summary_fixture(request):
+ """Return a function to generate a PlanSummary.
+
+ In the returned function `basedir` becomes optional and it defaults
+ to the directory of the calling test
+ """
+
+ def inner(module_path, basedir=None, tf_var_files=None, **tf_vars):
+ if basedir is None:
+ basedir = Path(request.fspath).parent
+ return plan_summary(module_path=module_path, basedir=basedir,
+ tf_var_files=tf_var_files, **tf_vars)
+
+ return inner
+
+
+def plan_validator(module_path, inventory_paths, basedir, tf_var_files=None,
+ **tf_vars):
+ summary = plan_summary(module_path=module_path, tf_var_files=tf_var_files,
+ basedir=basedir, **tf_vars)
+
+ # allow single single string for inventory_paths
+ if not isinstance(inventory_paths, list):
+ inventory_paths = [inventory_paths]
+
+ for path in inventory_paths:
+ # allow tfvars and inventory to be relative to the caller
+ path = basedir / path
+ try:
+ inventory = yaml.safe_load(path.read_text())
+ except (IOError, OSError, yaml.YAMLError) as e:
+ raise Exception(f'cannot read test inventory {path}: {e}')
+
+ # don't fail if the inventory is empty
+ inventory = inventory or {}
+
+ # If you add additional asserts to this function:
+ # - put the values coming from the plan on the left side of
+ # any comparison operators
+ # - put the values coming from user's inventory the right
+ # side of any comparison operators.
+ # - include a descriptive error message to the assert
+
+ # for values:
+ # - verify each address in the user's inventory exists in the plan
+ # - for those address that exist on both the user's inventory and
+ # the plan output, ensure the set of keys on the inventory are a
+ # subset of the keys in the plan, and compare their values by
+ # equality
+ if 'values' in inventory:
+ 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'
+ for k, v in expected_value.items():
+ assert k in summary.values[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}`'
+
+ 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_}`'
+ plan_count = summary.counts[type_]
+ assert plan_count == expected_count, \
+ f'count of {type_} resources failed. Got {plan_count}, expected {expected_count}'
+
+ if 'outputs' in inventory:
+ 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}`'
+ output = summary.outputs[output_name]
+ # assert 'value' in output, \
+ # f'output `{output_name}` does not have a value (is it sensitive or dynamic?)'
+ plan_output = output.get('value', '__missing__')
+ assert plan_output == expected_output, \
+ f'output {output_name} failed. Got `{plan_output}`, expected `{expected_output}`'
+
+ return summary
+
+
+@pytest.fixture(name='plan_validator')
+def plan_validator_fixture(request):
+ """Return a function to build a PlanSummary and compare it to a YAML inventory.
+
+ In the returned function `basedir` becomes optional and it defaults
+ to the directory of the calling test'
+
+ """
+
+ def inner(module_path, inventory_paths, basedir=None, tf_var_files=None,
+ **tf_vars):
+ if basedir is None:
+ basedir = Path(request.fspath).parent
+ return plan_validator(module_path=module_path,
+ inventory_paths=inventory_paths, basedir=basedir,
+ tf_var_files=tf_var_files, **tf_vars)
+
+ return inner
+
+
+# @pytest.fixture
+# def repo_root():
+# 'Return a pathlib.Path to the root of the repository'
+# return Path(__file__).parents[1]
diff --git a/tests/legacy_fixtures.py b/tests/legacy_fixtures.py
new file mode 100644
index 0000000000..8b23ea5eee
--- /dev/null
+++ b/tests/legacy_fixtures.py
@@ -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.
+"""Legacy pytest fixtures.
+
+The fixtures contained in this file will eventually go away. Consider
+using one of the fixtures in fixtures.py
+"""
+
+import inspect
+import os
+import shutil
+import tempfile
+
+import pytest
+import tftest
+
+BASEDIR = os.path.dirname(os.path.dirname(__file__))
+
+
+@pytest.fixture(scope='session')
+def _plan_runner():
+ 'Return a function to run Terraform plan on a fixture.'
+
+ def run_plan(fixture_path=None, extra_files=None, tf_var_file=None,
+ targets=None, refresh=True, tmpdir=True, **tf_vars):
+ 'Run Terraform plan and returns parsed output.'
+ if fixture_path is None:
+ # find out the fixture directory from the caller's directory
+ caller = inspect.stack()[2]
+ fixture_path = os.path.join(os.path.dirname(caller.filename), 'fixture')
+
+ fixture_parent = os.path.dirname(fixture_path)
+ fixture_prefix = os.path.basename(fixture_path) + '_'
+ with tempfile.TemporaryDirectory(prefix=fixture_prefix,
+ dir=fixture_parent) as tmp_path:
+ # copy fixture to a temporary directory so we can execute
+ # multiple tests in parallel
+ if tmpdir:
+ shutil.copytree(fixture_path, tmp_path, dirs_exist_ok=True)
+ tf = tftest.TerraformTest(tmp_path if tmpdir else fixture_path, BASEDIR,
+ os.environ.get('TERRAFORM', 'terraform'))
+ tf.setup(extra_files=extra_files, upgrade=True)
+ plan = tf.plan(output=True, refresh=refresh, tf_var_file=tf_var_file,
+ tf_vars=tf_vars, targets=targets)
+ return plan
+
+ return run_plan
+
+
+@pytest.fixture(scope='session')
+def plan_runner(_plan_runner):
+ 'Return a function to run Terraform plan on a module fixture.'
+
+ def run_plan(fixture_path=None, extra_files=None, tf_var_file=None,
+ targets=None, **tf_vars):
+ 'Run Terraform plan and returns plan and module resources.'
+ plan = _plan_runner(fixture_path, extra_files=extra_files,
+ tf_var_file=tf_var_file, targets=targets, **tf_vars)
+ # skip the fixture
+ root_module = plan.root_module['child_modules'][0]
+ return plan, root_module['resources']
+
+ return run_plan
+
+
+@pytest.fixture(scope='session')
+def e2e_plan_runner(_plan_runner):
+ 'Return a function to run Terraform plan on an end-to-end fixture.'
+
+ def run_plan(fixture_path=None, tf_var_file=None, targets=None, refresh=True,
+ include_bare_resources=False, **tf_vars):
+ 'Run Terraform plan on an end-to-end module using defaults, returns data.'
+ plan = _plan_runner(fixture_path, tf_var_file=tf_var_file, targets=targets,
+ refresh=refresh, **tf_vars)
+ # skip the fixture
+ root_module = plan.root_module['child_modules'][0]
+ modules = dict((mod['address'], mod['resources'])
+ for mod in root_module['child_modules'])
+ resources = [r for m in modules.values() for r in m]
+ if include_bare_resources:
+ bare_resources = root_module['resources']
+ resources.extend(bare_resources)
+ return modules, resources
+
+ return run_plan
+
+
+@pytest.fixture(scope='session')
+def apply_runner():
+ 'Return a function to run Terraform apply on a fixture.'
+
+ def run_apply(fixture_path=None, **tf_vars):
+ 'Run Terraform plan and returns parsed output.'
+ if fixture_path is None:
+ # find out the fixture directory from the caller's directory
+ caller = inspect.stack()[1]
+ fixture_path = os.path.join(os.path.dirname(caller.filename), 'fixture')
+
+ fixture_parent = os.path.dirname(fixture_path)
+ fixture_prefix = os.path.basename(fixture_path) + '_'
+
+ with tempfile.TemporaryDirectory(prefix=fixture_prefix,
+ dir=fixture_parent) as tmp_path:
+ # copy fixture to a temporary directory so we can execute
+ # multiple tests in parallel
+ shutil.copytree(fixture_path, tmp_path, dirs_exist_ok=True)
+ tf = tftest.TerraformTest(tmp_path, BASEDIR,
+ os.environ.get('TERRAFORM', 'terraform'))
+ tf.setup(upgrade=True)
+ apply = tf.apply(tf_vars=tf_vars)
+ output = tf.output(json_format=True)
+ return apply, output
+
+ return run_apply
diff --git a/tests/modules/api_gateway/test_plan.py b/tests/modules/api_gateway/test_plan.py
index 0e94cdec97..18ecdd3292 100644
--- a/tests/modules/api_gateway/test_plan.py
+++ b/tests/modules/api_gateway/test_plan.py
@@ -12,15 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-import pytest
-
-@pytest.fixture
-def resources(plan_runner):
- _, resources = plan_runner()
- return resources
-
-
-def test_resource_count(resources):
+def test_resource_count(plan_runner):
"Test number of resources created."
+ _, resources = plan_runner()
assert len(resources) == 5
diff --git a/tests/modules/apigee/__init__.py b/tests/modules/apigee/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/modules/apigee/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/modules/apigee/fixture/main.tf b/tests/modules/apigee/fixture/main.tf
new file mode 100644
index 0000000000..7ab25f733a
--- /dev/null
+++ b/tests/modules/apigee/fixture/main.tf
@@ -0,0 +1,25 @@
+/**
+ * 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/apigee"
+ project_id = var.project_id
+ organization = var.organization
+ envgroups = var.envgroups
+ environments = var.environments
+ instances = var.instances
+ endpoint_attachments = var.endpoint_attachments
+}
diff --git a/tests/modules/apigee/fixture/test.all.tfvars b/tests/modules/apigee/fixture/test.all.tfvars
new file mode 100644
index 0000000000..d0c29921ca
--- /dev/null
+++ b/tests/modules/apigee/fixture/test.all.tfvars
@@ -0,0 +1,51 @@
+project_id = "my-project"
+organization = {
+ display_name = "My Organization"
+ description = "My Organization"
+ authorized_network = "my-vpc"
+ runtime_type = "CLOUD"
+ billing_type = "Pay-as-you-go"
+ database_encryption_key = "123456789"
+ analytics_region = "europe-west1"
+}
+envgroups = {
+ test = ["test.example.com"]
+ prod = ["prod.example.com"]
+}
+environments = {
+ apis-test = {
+ display_name = "APIs test"
+ description = "APIs Test"
+ envgroups = ["test"]
+ }
+ apis-prod = {
+ display_name = "APIs prod"
+ description = "APIs prod"
+ envgroups = ["prod"]
+ iam = {
+ "roles/viewer" = ["group:devops@myorg.com"]
+ }
+ }
+}
+instances = {
+ instance-test-ew1 = {
+ region = "europe-west1"
+ environments = ["apis-test"]
+ psa_ip_cidr_range = "10.0.4.0/22"
+ }
+ instance-prod-ew3 = {
+ region = "europe-west3"
+ environments = ["apis-prod"]
+ psa_ip_cidr_range = "10.0.5.0/22"
+ }
+}
+endpoint_attachments = {
+ endpoint-backend-1 = {
+ region = "europe-west1"
+ service_attachment = "projects/my-project-1/serviceAttachments/gkebackend1"
+ }
+ endpoint-backend-2 = {
+ region = "europe-west1"
+ service_attachment = "projects/my-project-2/serviceAttachments/gkebackend2"
+ }
+}
diff --git a/tests/modules/apigee/fixture/test.endpoint_attachment_only.tfvars b/tests/modules/apigee/fixture/test.endpoint_attachment_only.tfvars
new file mode 100644
index 0000000000..bd6cbcc410
--- /dev/null
+++ b/tests/modules/apigee/fixture/test.endpoint_attachment_only.tfvars
@@ -0,0 +1,7 @@
+project_id = "my-project"
+endpoint_attachments = {
+ endpoint-backend-1 = {
+ region = "europe-west1"
+ service_attachment = "projects/my-project-1/serviceAttachments/gkebackend1"
+ }
+}
diff --git a/tests/modules/apigee/fixture/test.env_only.tfvars b/tests/modules/apigee/fixture/test.env_only.tfvars
new file mode 100644
index 0000000000..0e4edc4866
--- /dev/null
+++ b/tests/modules/apigee/fixture/test.env_only.tfvars
@@ -0,0 +1,12 @@
+project_id = "my-project"
+environments = {
+ apis-test = {
+ display_name = "APIs test"
+ description = "APIs Test"
+ envgroups = ["test"]
+ node_config = {
+ min_node_count = 2
+ max_node_count = 5
+ }
+ }
+}
diff --git a/tests/modules/apigee/fixture/test.envgroup_only.tfvars b/tests/modules/apigee/fixture/test.envgroup_only.tfvars
new file mode 100644
index 0000000000..0e728a2306
--- /dev/null
+++ b/tests/modules/apigee/fixture/test.envgroup_only.tfvars
@@ -0,0 +1,4 @@
+project_id = "my-project"
+envgroups = {
+ test = ["test.example.com"]
+}
\ No newline at end of file
diff --git a/tests/modules/apigee/fixture/test.instance_only.tfvars b/tests/modules/apigee/fixture/test.instance_only.tfvars
new file mode 100644
index 0000000000..3d3eb1be1b
--- /dev/null
+++ b/tests/modules/apigee/fixture/test.instance_only.tfvars
@@ -0,0 +1,8 @@
+project_id = "my-project"
+instances = {
+ instance-test-ew1 = {
+ region = "europe-west1"
+ environments = ["apis-test"]
+ psa_ip_cidr_range = "10.0.4.0/22"
+ }
+}
\ No newline at end of file
diff --git a/tests/modules/apigee/fixture/test.no_instances.tfvars b/tests/modules/apigee/fixture/test.no_instances.tfvars
new file mode 100644
index 0000000000..f88722ceed
--- /dev/null
+++ b/tests/modules/apigee/fixture/test.no_instances.tfvars
@@ -0,0 +1,26 @@
+project_id = "my-project"
+organization = {
+ display_name = "My Organization"
+ description = "My Organization"
+ authorized_network = "my-vpc"
+ runtime_type = "CLOUD"
+ billing_type = "PAYG"
+ database_encryption_key = "123456789"
+ analytics_region = "europe-west1"
+}
+envgroups = {
+ test = ["test.example.com"]
+ prod = ["prod.example.com"]
+}
+environments = {
+ apis-test = {
+ display_name = "APIs test"
+ description = "APIs Test"
+ envgroups = ["test"]
+ }
+ apis-prod = {
+ display_name = "APIs prod"
+ description = "APIs prod"
+ envgroups = ["prod"]
+ }
+}
diff --git a/tests/modules/apigee/fixture/test.organization_only.tfvars b/tests/modules/apigee/fixture/test.organization_only.tfvars
new file mode 100644
index 0000000000..db2b709790
--- /dev/null
+++ b/tests/modules/apigee/fixture/test.organization_only.tfvars
@@ -0,0 +1,10 @@
+project_id = "my-project"
+organization = {
+ display_name = "My Organization"
+ description = "My Organization"
+ authorized_network = "my-vpc"
+ runtime_type = "CLOUD"
+ billing_type = "PAYG"
+ database_encryption_key = "123456789"
+ analytics_region = "europe-west1"
+}
\ No newline at end of file
diff --git a/tests/modules/apigee/fixture/variables.tf b/tests/modules/apigee/fixture/variables.tf
new file mode 100644
index 0000000000..266f0d34ed
--- /dev/null
+++ b/tests/modules/apigee/fixture/variables.tf
@@ -0,0 +1,78 @@
+/**
+ * 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 "endpoint_attachments" {
+ description = "Endpoint attachments."
+ type = map(object({
+ region = string
+ service_attachment = string
+ }))
+ default = null
+}
+
+variable "envgroups" {
+ description = "Environment groups (NAME => [HOSTNAMES])."
+ type = map(list(string))
+ default = null
+}
+
+variable "environments" {
+ description = "Environments."
+ type = map(object({
+ display_name = optional(string)
+ description = optional(string, "Terraform-managed")
+ node_config = optional(object({
+ min_node_count = optional(number)
+ max_node_count = optional(number)
+ }))
+ iam = optional(map(list(string)))
+ envgroups = list(string)
+ }))
+ default = null
+}
+
+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))
+ }))
+ default = null
+}
+
+variable "organization" {
+ description = "Apigee organization. If set to null the organization must already exist."
+ type = object({
+ display_name = optional(string)
+ description = optional(string, "Terraform-managed")
+ authorized_network = optional(string)
+ runtime_type = optional(string, "CLOUD")
+ billing_type = optional(string)
+ database_encryption_key = optional(string)
+ analytics_region = optional(string, "europe-west1")
+ })
+ default = null
+}
+
+variable "project_id" {
+ description = "Project ID."
+ type = string
+}
diff --git a/tests/modules/apigee/test_plan.py b/tests/modules/apigee/test_plan.py
new file mode 100644
index 0000000000..e693ddbb29
--- /dev/null
+++ b/tests/modules/apigee/test_plan.py
@@ -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.
+
+import collections
+
+def test_all(plan_runner):
+ "Test that creates all resources."
+ _, resources = plan_runner(tf_var_file='test.all.tfvars')
+ counts = collections.Counter(f'{r["type"]}.{r["name"]}' for r in resources)
+ assert counts == {
+ 'google_apigee_organization.organization': 1,
+ 'google_apigee_envgroup.envgroups': 2,
+ 'google_apigee_environment.environments': 2,
+ 'google_apigee_envgroup_attachment.envgroup_attachments': 2,
+ 'google_apigee_instance.instances': 2,
+ 'google_apigee_instance_attachment.instance_attachments': 2,
+ 'google_apigee_endpoint_attachment.endpoint_attachments': 2,
+ 'google_apigee_environment_iam_binding.binding': 1
+ }
+
+def test_organization_only(plan_runner):
+ "Test that creates only an organization."
+ _, resources = plan_runner(tf_var_file='test.organization_only.tfvars')
+ counts = collections.Counter(f'{r["type"]}.{r["name"]}' for r in resources)
+ assert counts == {
+ 'google_apigee_organization.organization': 1
+ }
+
+def test_envgroup_only(plan_runner):
+ "Test that creates only an environment group in an existing organization."
+ _, resources = plan_runner(tf_var_file='test.envgroup_only.tfvars')
+ counts = collections.Counter(f'{r["type"]}.{r["name"]}' for r in resources)
+ assert counts == {
+ 'google_apigee_envgroup.envgroups': 1,
+ }
+
+def test_env_only(plan_runner):
+ "Test that creates an environment in an existing environment group."
+ _, resources = plan_runner(tf_var_file='test.env_only.tfvars')
+ counts = collections.Counter(f'{r["type"]}.{r["name"]}' for r in resources)
+ assert counts == {
+ 'google_apigee_environment.environments': 1,
+ 'google_apigee_envgroup_attachment.envgroup_attachments': 1,
+ }
+
+def test_instance_only(plan_runner):
+ "Test that creates only an instance."
+ _, resources = plan_runner(tf_var_file='test.instance_only.tfvars')
+ counts = collections.Counter(f'{r["type"]}.{r["name"]}' for r in resources)
+ assert counts == {
+ 'google_apigee_instance.instances': 1,
+ 'google_apigee_instance_attachment.instance_attachments': 1
+ }
+
+def test_endpoint_attachment_only(plan_runner):
+ "Test that creates only an instance."
+ _, resources = plan_runner(tf_var_file='test.endpoint_attachment_only.tfvars')
+ counts = collections.Counter(f'{r["type"]}.{r["name"]}' for r in resources)
+ assert counts == {
+ 'google_apigee_endpoint_attachment.endpoint_attachments': 1,
+ }
+
+def test_no_instances(plan_runner):
+ "Test that creates everything but the instances."
+ _, resources = plan_runner(tf_var_file='test.no_instances.tfvars')
+ counts = collections.Counter(f'{r["type"]}.{r["name"]}' for r in resources)
+ assert counts == {
+ 'google_apigee_organization.organization': 1,
+ 'google_apigee_envgroup.envgroups': 2,
+ 'google_apigee_environment.environments': 2,
+ 'google_apigee_envgroup_attachment.envgroup_attachments': 2,
+ }
diff --git a/tests/modules/apigee_organization/fixture/main.tf b/tests/modules/apigee_organization/fixture/main.tf
deleted file mode 100644
index 49ad78b1cb..0000000000
--- a/tests/modules/apigee_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/apigee-organization"
- project_id = "my-project"
- analytics_region = var.analytics_region
- runtime_type = "CLOUD"
- authorized_network = var.network
- apigee_environments = [
- "eval1",
- "eval2"
- ]
- apigee_envgroups = {
- eval = {
- environments = [
- "eval1",
- "eval2"
- ]
- hostnames = [
- "eval.api.example.com"
- ]
- }
- }
-}
diff --git a/tests/modules/apigee_organization/fixture/variables.tf b/tests/modules/apigee_organization/fixture/variables.tf
deleted file mode 100644
index 50e6808966..0000000000
--- a/tests/modules/apigee_organization/fixture/variables.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.
- */
-
-variable "analytics_region" {
- type = string
- default = "europe-west1"
-}
-
-variable "network" {
- type = string
- default = "apigee-vpc"
-}
diff --git a/tests/modules/apigee_organization/test_plan.py b/tests/modules/apigee_organization/test_plan.py
deleted file mode 100644
index ec2312c963..0000000000
--- a/tests/modules/apigee_organization/test_plan.py
+++ /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.
-
-import pytest
-
-
-@pytest.fixture
-def resources(plan_runner):
- _, resources = plan_runner()
- return resources
-
-
-def test_resource_count(resources):
- "Test number of resources created."
- assert len(resources) == 6
-
-
-def test_envgroup_attachment(resources):
- "Test Apigee Envgroup Attachments."
- attachments = [r['values'] for r in resources if r['type']
- == 'google_apigee_envgroup_attachment']
- assert len(attachments) == 2
- assert set(a['environment'] for a in attachments) == set(['eval1', 'eval2'])
-
-
-def test_envgroup(resources):
- "Test env group."
- envgroups = [r['values'] for r in resources if r['type']
- == 'google_apigee_envgroup']
- assert len(envgroups) == 1
- assert envgroups[0]['name'] == 'eval'
- assert len(envgroups[0]['hostnames']) == 1
- assert envgroups[0]['hostnames'][0] == 'eval.api.example.com'
diff --git a/tests/modules/apigee_x_instance/fixture/main.tf b/tests/modules/apigee_x_instance/fixture/main.tf
deleted file mode 100644
index 19373a3b1a..0000000000
--- a/tests/modules/apigee_x_instance/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 "apigee-x-instance" {
- source = "../../../../modules/apigee-x-instance"
- name = var.name
- region = var.region
- ip_range = var.ip_range
-
- apigee_org_id = "my-project"
- apigee_environments = [
- "eval1",
- "eval2"
- ]
-}
diff --git a/tests/modules/apigee_x_instance/fixture/variables.tf b/tests/modules/apigee_x_instance/fixture/variables.tf
deleted file mode 100644
index b55e641fb9..0000000000
--- a/tests/modules/apigee_x_instance/fixture/variables.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.
- */
-
-variable "name" {
- type = string
- default = "my-test-instance"
-}
-
-variable "region" {
- type = string
- default = "europe-west1"
-}
-
-variable "ip_range" {
- type = string
- default = "10.0.0.0/22"
-}
\ No newline at end of file
diff --git a/tests/modules/apigee_x_instance/test_plan.py b/tests/modules/apigee_x_instance/test_plan.py
deleted file mode 100644
index e7d2f60aa4..0000000000
--- a/tests/modules/apigee_x_instance/test_plan.py
+++ /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.
-
-import pytest
-
-
-@pytest.fixture
-def resources(plan_runner):
- _, resources = plan_runner()
- return resources
-
-
-def test_resource_count(resources):
- "Test number of resources created."
- assert len(resources) == 3
-
-
-def test_instance_attachment(resources):
- "Test Apigee Instance Attachments."
- attachments = [r['values'] for r in resources if r['type']
- == 'google_apigee_instance_attachment']
- assert len(attachments) == 2
- assert set(a['environment'] for a in attachments) == set(['eval1', 'eval2'])
-
-
-def test_instance(resources):
- "Test Instance."
- instances = [r['values'] for r in resources if r['type']
- == 'google_apigee_instance']
- assert len(instances) == 1
- assert instances[0]['ip_range'] == '10.0.0.0/22'
- assert instances[0]['name'] == 'my-test-instance'
- assert instances[0]['location'] == 'europe-west1'
diff --git a/tests/modules/bigtable_instance/fixture/main.tf b/tests/modules/bigtable_instance/fixture/main.tf
index fa74a6c8e2..4fa83ce2b3 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/binauthz/__init__.py b/tests/modules/binauthz/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/modules/binauthz/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/tests/modules/binauthz/fixture/main.tf b/tests/modules/binauthz/fixture/main.tf
new file mode 100644
index 0000000000..95f76d634f
--- /dev/null
+++ b/tests/modules/binauthz/fixture/main.tf
@@ -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.
+ */
+
+module "test" {
+ source = "../../../../modules/binauthz"
+ project_id = var.project_id
+ global_policy_evaluation_mode = var.global_policy_evaluation_mode
+ default_admission_rule = var.default_admission_rule
+ attestors_config = var.attestors_config
+}
diff --git a/tests/modules/binauthz/fixture/variables.tf b/tests/modules/binauthz/fixture/variables.tf
new file mode 100644
index 0000000000..327ced2523
--- /dev/null
+++ b/tests/modules/binauthz/fixture/variables.tf
@@ -0,0 +1,103 @@
+/**
+ * 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_id" {
+ type = string
+ default = "my_project"
+}
+
+variable "global_policy_evaluation_mode" {
+ type = string
+ default = null
+}
+
+variable "admission_whitelist_patterns" {
+ type = list(string)
+ default = [
+ "gcr.io/google_containers/*"
+ ]
+}
+
+variable "default_admission_rule" {
+ type = object({
+ evaluation_mode = string
+ enforcement_mode = string
+ attestors = list(string)
+ })
+ default = {
+ evaluation_mode = "ALWAYS_ALLOW"
+ enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
+ attestors = null
+ }
+}
+
+variable "cluster_admission_rules" {
+ type = map(object({
+ evaluation_mode = string
+ enforcement_mode = string
+ attestors = list(string)
+ }))
+ default = {
+ "europe-west1-c.cluster" = {
+ evaluation_mode = "REQUIRE_ATTESTATION"
+ enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
+ attestors = ["test"]
+ }
+ }
+}
+
+variable "attestors_config" {
+ description = "Attestors configuration"
+ type = map(object({
+ note_reference = string
+ iam = map(list(string))
+ pgp_public_keys = list(string)
+ pkix_public_keys = list(object({
+ id = string
+ public_key_pem = string
+ signature_algorithm = string
+ }))
+ }))
+ default = {
+ "test" : {
+ note_reference = null
+ pgp_public_keys = [
+ <+
additive, •
conditional.')
for resource, resource_groups in resource_grouper:
- print(f'\n## {resource[0].title()} {resource[1].lower()}\n')
+ resource_type, resource_name = resource
+ print(f'\n## {resource_type.title()} {resource_name.lower()}\n')
principal_grouper = itertools.groupby(
resource_groups, key=lambda b: (b.member_type, b.member_id))
print('| members | roles |')
diff --git a/tools/tfdoc.py b/tools/tfdoc.py
index 68bbe00ccb..d06dedb9e3 100755
--- a/tools/tfdoc.py
+++ b/tools/tfdoc.py
@@ -70,7 +70,7 @@
# output open
(?:^\s*output\s*"([^"]+)"\s*\{\s*$) |
# attribute
- (?:^\s{2}([a-z]+)\s*=\s*"?(.*?)"?\s*$) |
+ (?:^\n?\s{2}([a-z]+)\s*=\s*"?(.*?)"?\s*$) |
# output close
(?:^\s?(\})\s*$) |
# comment
@@ -99,13 +99,13 @@
VAR_RE_TYPE = re.compile(r'([\(\{\}\)])')
VAR_TEMPLATE = ('default', 'description', 'type', 'nullable')
+Document = collections.namedtuple('Document', 'content files variables outputs')
File = collections.namedtuple('File', 'name description modules resources')
Output = collections.namedtuple(
'Output', 'name description sensitive consumers file line')
Variable = collections.namedtuple(
'Variable',
'name description type default required nullable source file line')
-
# parsing functions
@@ -206,7 +206,7 @@ def parse_variables(basepath, exclude_files=None):
except (IOError, OSError) as e:
raise SystemExit(f'Cannot open variables file {shortname}.')
for item in _parse(body):
- description = ''.join(item['description'])
+ description = (''.join(item['description'])).replace('|', '\\|')
vtype = '\n'.join(item['type'])
default = HEREDOC_RE.sub(r'\1', '\n'.join(item['default']))
required = not item['default']
@@ -247,7 +247,7 @@ def format_doc(outputs, variables, files, show_extra=False):
def format_files(items):
'Format files table.'
- items.sort(key=lambda i: i.name)
+ items = sorted(items, key=lambda i: i.name)
num_modules = sum(len(i.modules) for i in items)
num_resources = sum(len(i.resources) for i in items)
yield '| name | description |{}{}'.format(
@@ -271,7 +271,7 @@ def format_outputs(items, show_extra=True):
'Format outputs table.'
if not items:
return
- items.sort(key=lambda i: i.name)
+ items = sorted(items, key=lambda i: i.name)
yield '| name | description | sensitive |' + (' consumers |'
if show_extra else '')
yield '|---|---|:---:|' + ('---|' if show_extra else '')
@@ -289,8 +289,7 @@ def format_variables(items, show_extra=True):
'Format variables table.'
if not items:
return
- items.sort(key=lambda i: i.name)
- items.sort(key=lambda i: i.required, reverse=True)
+ items = sorted(items, key=lambda i: (not i.required, i.name))
yield '| name | description | type | required | default |' + (
' producer |' if show_extra else '')
yield '|---|---|:---:|:---:|:---:|' + (':---:|' if show_extra else '')
@@ -358,7 +357,8 @@ def create_doc(module_path, files=False, show_extra=False, exclude_files=None,
mod_outputs = list(parse_outputs(module_path, exclude_files))
except (IOError, OSError) as e:
raise SystemExit(e)
- return format_doc(mod_outputs, mod_variables, mod_files, show_extra)
+ doc = format_doc(mod_outputs, mod_variables, mod_files, show_extra)
+ return Document(doc, mod_files, mod_variables, mod_outputs)
def get_readme(readme_path):
@@ -402,7 +402,7 @@ def main(module_path=None, exclude_file=None, files=False, replace=True,
readme = get_readme(readme_path)
doc = create_doc(module_path, files, show_extra, exclude_file, readme)
if replace:
- replace_doc(readme_path, doc, readme)
+ replace_doc(readme_path, doc.content, readme)
else:
print(doc)
diff --git a/tools/tfeditor/go.mod b/tools/tfeditor/go.mod
new file mode 100644
index 0000000000..d1a4a41e46
--- /dev/null
+++ b/tools/tfeditor/go.mod
@@ -0,0 +1,8 @@
+module github.com/GoogleCloudPlatform/cloud-foundation-fabric/tools/tfeditor
+
+go 1.16
+
+require (
+ github.com/hashicorp/hcl/v2 v2.12.0 // indirect
+ github.com/zclconf/go-cty v1.8.0 // indirect
+)
diff --git a/tools/tfeditor/go.sum b/tools/tfeditor/go.sum
new file mode 100644
index 0000000000..1ed29912ff
--- /dev/null
+++ b/tools/tfeditor/go.sum
@@ -0,0 +1,51 @@
+github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
+github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
+github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
+github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
+github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
+github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
+github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
+github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/hashicorp/hcl/v2 v2.12.0 h1:PsYxySWpMD4KPaoJLnsHwtK5Qptvj/4Q6s0t4sUxZf4=
+github.com/hashicorp/hcl/v2 v2.12.0/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
+github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
+github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
+github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
+github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
+github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
+github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA=
+github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
+github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/tools/tfeditor/main.go b/tools/tfeditor/main.go
new file mode 100644
index 0000000000..f8a4e0a974
--- /dev/null
+++ b/tools/tfeditor/main.go
@@ -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.
+
+ This tool updates in-place versions.tf files to change module_name parameter
+ on an automated basis. In retains all existing comments and structures.
+*/
+package main
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "text/template"
+
+ "github.com/hashicorp/hcl/v2"
+ "github.com/hashicorp/hcl/v2/hclwrite"
+ "github.com/zclconf/go-cty/cty"
+)
+
+type Provider struct {
+ Source string
+ Version string
+}
+
+func main() {
+ var path string
+ var pattern string
+ var moduleName string
+ var terraformVersion string
+ var providerVersions string
+ var updateProviderVersions bool = false
+ var updateTerraformVersion bool = false
+ var updateModuleName bool = false
+
+ flag.StringVar(&path, "path", "./", "Path to search for file pattern (default ./")
+ flag.StringVar(&pattern, "pattern", "versions.tf", "Pattern to search (default versions.tf)")
+ flag.StringVar(&moduleName, "module-name", "", "module_name attribute for provider_meta (can be templated with {{ .Module }})")
+ flag.StringVar(&providerVersions, "provider-versions", "", "set provider versions (usage: hashicorp/google>= 4.0.0,hashicorp/googlegoogle-beta== 4.0.0)")
+ flag.StringVar(&terraformVersion, "terraform-version", "", "Update terraform required_version")
+ flag.Parse()
+
+ if !strings.HasSuffix(path, "/") {
+ path = fmt.Sprintf("%s/", path)
+ }
+
+ if moduleName != "" {
+ updateModuleName = true
+ log.Printf("Updating module_name to: %s", moduleName)
+ }
+
+ if terraformVersion != "" {
+ updateTerraformVersion = true
+ log.Printf("Updating Terraform version to: %s", terraformVersion)
+ }
+
+ providerVersionsMap := map[string]Provider{}
+ if providerVersions != "" {
+ for _, v := range strings.Split(providerVersions, ",") {
+ re := regexp.MustCompile("([a-zA-Z_/-]+)(.+)")
+ split := re.FindAllStringSubmatch(v, -1)
+ for _, splitN := range split {
+ providerKey := filepath.Base(splitN[1])
+ providerVersionsMap[providerKey] = Provider{Source: splitN[1], Version: splitN[2]}
+ log.Printf("Updating provider %s to: %s %s", providerKey, providerVersionsMap[providerKey].Source, providerVersionsMap[providerKey].Version)
+ }
+ }
+ updateProviderVersions = true
+ }
+
+ log.Printf("Looking for files (%s) in: %s", pattern, path)
+
+ var foundFiles []string
+ err := filepath.Walk(path, func(file string, f os.FileInfo, err error) error {
+ if isMatch, _ := filepath.Match(pattern, filepath.Base(file)); isMatch {
+ foundFiles = append(foundFiles, file)
+ }
+ return nil
+ })
+ log.Printf("Found %d files.", len(foundFiles))
+
+ for _, foundFile := range foundFiles {
+ fileBytes, fErr := ioutil.ReadFile(foundFile)
+ if fErr != nil {
+ panic(fErr)
+ }
+ fileBasename := filepath.Base(foundFile)
+
+ hclFile, diag := hclwrite.ParseConfig(fileBytes, fileBasename, hcl.Pos{})
+ if diag == nil {
+ hclBody := hclFile.Body()
+ for _, block := range hclBody.Blocks() {
+ if block.Type() == "terraform" {
+ if updateTerraformVersion {
+ for k, _ := range block.Body().Attributes() {
+ if k == "required_version" {
+ block.Body().SetAttributeValue("required_version", cty.StringVal(terraformVersion))
+ }
+ }
+ }
+
+ hasProviderMetaForGoogle := false
+ hasProviderMetaForGoogleBeta := false
+
+ // Expand template
+ tmpl, tErr := template.New("modulename").Parse(moduleName)
+ if tErr != nil {
+ panic(tErr)
+ }
+ expandedBuffer := new(bytes.Buffer)
+ tErr = tmpl.Execute(expandedBuffer, map[string]string{
+ "Module": filepath.Base(filepath.Dir(foundFile)),
+ })
+ if tErr != nil {
+ panic(tErr)
+ }
+ expandedModuleName := expandedBuffer.String()
+
+ for _, tfBlock := range block.Body().Blocks() {
+ if tfBlock.Type() == "required_providers" && updateProviderVersions {
+ for k, _ := range tfBlock.Body().Attributes() {
+ if provider, ok := providerVersionsMap[k]; ok {
+ tfBlock.Body().SetAttributeValue(k, cty.ObjectVal(map[string]cty.Value{
+ "source": cty.StringVal(provider.Source),
+ "version": cty.StringVal(provider.Version),
+ }))
+ }
+ }
+ }
+
+ if tfBlock.Type() == "provider_meta" && updateModuleName {
+ labels := tfBlock.Labels()
+ if len(labels) > 0 {
+ if labels[0] == "google" {
+ hasProviderMetaForGoogle = true
+ tfBlock.Body().SetAttributeValue("module_name", cty.StringVal(expandedModuleName))
+ }
+ if labels[0] == "google-beta" {
+ hasProviderMetaForGoogleBeta = true
+ tfBlock.Body().SetAttributeValue("module_name", cty.StringVal(expandedModuleName))
+ }
+ }
+ }
+ }
+
+ if updateModuleName {
+ if !hasProviderMetaForGoogle {
+ providerMetaGoogleBlock := hclwrite.NewBlock("provider_meta", []string{"google"})
+ providerMetaGoogleBlock.Body().SetAttributeValue("module_name", cty.StringVal(expandedModuleName))
+ block.Body().AppendBlock(providerMetaGoogleBlock)
+ }
+ if !hasProviderMetaForGoogleBeta {
+ providerMetaGoogleBlock := hclwrite.NewBlock("provider_meta", []string{"google-beta"})
+ providerMetaGoogleBlock.Body().SetAttributeValue("module_name", cty.StringVal(expandedModuleName))
+ block.Body().AppendBlock(providerMetaGoogleBlock)
+ }
+ }
+ }
+ }
+
+ log.Printf("Updating: %s", foundFile)
+ info, sErr := os.Stat(foundFile)
+ if sErr != nil {
+ panic(sErr)
+ }
+ tempFilePath := fmt.Sprintf("%s.tmp", foundFile)
+ wErr := os.WriteFile(tempFilePath, hclFile.Bytes(), info.Mode())
+ if wErr != nil {
+ panic(wErr)
+ }
+ os.Rename(tempFilePath, foundFile)
+ } else {
+ panic(diag)
+ }
+ }
+
+ if err != nil {
+ panic(err)
+ }
+ log.Printf("All done.")
+}