From 579bae58b562e47a3364dc19cf407b56b2975a8f Mon Sep 17 00:00:00 2001 From: CFT Bot Date: Mon, 23 Feb 2026 16:52:27 -0800 Subject: [PATCH 1/6] chore: update .github/workflows/lint.yaml feat: refactoring secure cicd repo feat: adding gitlab vm for integration test feat: adding gitlab vm for integration test --- .github/workflows/lint.yaml | 8 +- build/cloudbuild-cd.yaml | 2 +- examples/app_cicd/README.md | 151 ----------- .../app_cicd/cloud-build-builder/Dockerfile | 72 ----- .../cloudbuild-skaffold-build-image.yaml | 25 -- examples/app_cicd/main.tf | 56 ---- examples/app_cicd/outputs.tf | 50 ---- .../policies/container-analysis-policy.yaml | 28 -- .../policies/container-structure-policy.yaml | 37 --- examples/app_cicd/policies/githound.yaml | 34 --- examples/app_cicd/variables.tf | 42 --- examples/cloudbuild_private_pool/README.md | 58 ---- examples/cloudbuild_private_pool/main.tf | 77 ------ examples/standalone_single_project/cicd.tf | 51 ++-- examples/standalone_single_project/gke.tf | 21 +- examples/standalone_single_project/network.tf | 13 +- examples/standalone_single_project/outputs.tf | 24 +- .../standalone_single_project/variables.tf | 34 +++ .../standalone_single_project/versions.tf | 8 +- .../binauthz.tf => attestor/main.tf} | 2 +- .../attestor}/outputs.tf | 14 +- .../attestor}/variables.tf | 13 +- .../app_cicd => modules/attestor}/versions.tf | 11 +- modules/cloudbuild-private-pool/main.tf | 2 - modules/cloudbuild-private-pool/versions.tf | 13 +- modules/secure-cd/build.tf | 16 +- modules/secure-cd/iam.tf | 1 - modules/secure-cd/locals.tf | 30 +++ modules/secure-cd/main.tf | 34 ++- modules/secure-cd/variables.tf | 63 ++++- modules/secure-cd/versions.tf | 12 +- .../outputs.tf => modules/secure-ci/checks.tf | 11 +- modules/secure-ci/locals.tf | 44 ++++ modules/secure-ci/main.tf | 97 ++++--- modules/secure-ci/outputs.tf | 19 +- .../secure-ci/scripts/cloud-build-submit.sh | 24 -- modules/secure-ci/variables.tf | 75 +++++- modules/secure-ci/versions.tf | 20 +- modules/workerpool-gke-ha-vpn/versions.tf | 9 +- test/fixtures/app_cicd/main.tf | 68 ----- test/fixtures/app_cicd/outputs.tf | 35 --- test/fixtures/app_cicd/variables.tf | 49 ---- test/fixtures/cloudbuild_private_pool/main.tf | 76 ------ .../cloudbuild_private_pool/variables.tf | 41 --- test/fixtures/private_cluster_cicd/main.tf | 89 ------- test/fixtures/private_cluster_cicd/outputs.tf | 35 --- .../private_cluster_cicd/variables.tf | 59 ----- .../fixtures/private_cluster_cicd/versions.tf | 19 -- test/integration/app_cicd/app_cicd_test.go | 122 --------- .../cloudbuild_private_pool_test.go | 70 ----- test/integration/go.mod | 7 +- test/integration/go.sum | 5 +- .../private_cluster_cicd_test.go | 76 ------ .../standalone_single_project_test.go | 248 ++++++++++-------- test/integration/testutils/utils.go | 85 ++++++ test/setup/harness/gitlab/gitlab_vm.tf | 164 ++++++++++++ test/setup/harness/gitlab/outputs.tf | 31 +++ test/setup/harness/gitlab/variables.tf | 24 ++ test/setup/harness/gitlab/versions.tf | 21 ++ test/setup/scripts/gitlab_self_hosted.sh | 55 ++++ 60 files changed, 996 insertions(+), 1684 deletions(-) delete mode 100644 examples/app_cicd/README.md delete mode 100644 examples/app_cicd/cloud-build-builder/Dockerfile delete mode 100644 examples/app_cicd/cloud-build-builder/cloudbuild-skaffold-build-image.yaml delete mode 100644 examples/app_cicd/main.tf delete mode 100644 examples/app_cicd/outputs.tf delete mode 100644 examples/app_cicd/policies/container-analysis-policy.yaml delete mode 100644 examples/app_cicd/policies/container-structure-policy.yaml delete mode 100644 examples/app_cicd/policies/githound.yaml delete mode 100644 examples/app_cicd/variables.tf delete mode 100644 examples/cloudbuild_private_pool/README.md delete mode 100644 examples/cloudbuild_private_pool/main.tf rename modules/{secure-ci/binauthz.tf => attestor/main.tf} (97%) rename {examples/cloudbuild_private_pool => modules/attestor}/outputs.tf (54%) rename {examples/cloudbuild_private_pool => modules/attestor}/variables.tf (70%) rename {test/fixtures/app_cicd => modules/attestor}/versions.tf (75%) create mode 100644 modules/secure-cd/locals.tf rename test/fixtures/cloudbuild_private_pool/outputs.tf => modules/secure-ci/checks.tf (61%) create mode 100644 modules/secure-ci/locals.tf delete mode 100755 modules/secure-ci/scripts/cloud-build-submit.sh delete mode 100644 test/fixtures/app_cicd/main.tf delete mode 100644 test/fixtures/app_cicd/outputs.tf delete mode 100644 test/fixtures/app_cicd/variables.tf delete mode 100644 test/fixtures/cloudbuild_private_pool/main.tf delete mode 100644 test/fixtures/cloudbuild_private_pool/variables.tf delete mode 100644 test/fixtures/private_cluster_cicd/main.tf delete mode 100644 test/fixtures/private_cluster_cicd/outputs.tf delete mode 100644 test/fixtures/private_cluster_cicd/variables.tf delete mode 100644 test/fixtures/private_cluster_cicd/versions.tf delete mode 100644 test/integration/app_cicd/app_cicd_test.go delete mode 100644 test/integration/cloudbuild_private_pool/cloudbuild_private_pool_test.go delete mode 100644 test/integration/private_cluster_cicd/private_cluster_cicd_test.go create mode 100644 test/integration/testutils/utils.go create mode 100644 test/setup/harness/gitlab/gitlab_vm.tf create mode 100644 test/setup/harness/gitlab/outputs.tf create mode 100644 test/setup/harness/gitlab/variables.tf create mode 100644 test/setup/harness/gitlab/versions.tf create mode 100644 test/setup/scripts/gitlab_self_hosted.sh diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 3f5a06b0..0ca86113 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -47,14 +47,14 @@ jobs: REGISTRY=$(grep "REGISTRY_URL := " $MAKEFILE | cut -d\ -f3) echo dev-tools=${REGISTRY}/${IMAGE}:${VERSION} >> "$GITHUB_OUTPUT" fi - - run: docker run --rm -e ENABLE_BPMETADATA -v ${{ github.workspace }}:/workspace ${{ steps.variables.outputs.dev-tools }} module-swapper + - run: docker run --rm -e ENABLE_BPMETADATA -v ${{ github.workspace }}:/workspace ${STEPS_VARIABLES_OUTPUTS_DEV_TOOLS} module-swapper env: ENABLE_BPMETADATA: 1 - - - run: docker run --rm -e ENABLE_BPMETADATA -v ${{ github.workspace }}:/workspace ${{ steps.variables.outputs.dev-tools }} /usr/local/bin/test_lint.sh + STEPS_VARIABLES_OUTPUTS_DEV_TOOLS: ${{ steps.variables.outputs.dev-tools }} + - run: docker run --rm -e ENABLE_BPMETADATA -v ${{ github.workspace }}:/workspace ${STEPS_VARIABLES_OUTPUTS_DEV_TOOLS} /usr/local/bin/test_lint.sh env: ENABLE_BPMETADATA: 1 - + STEPS_VARIABLES_OUTPUTS_DEV_TOOLS: ${{ steps.variables.outputs.dev-tools }} commitlint: runs-on: ubuntu-latest steps: diff --git a/build/cloudbuild-cd.yaml b/build/cloudbuild-cd.yaml index bba06cb4..9a336523 100644 --- a/build/cloudbuild-cd.yaml +++ b/build/cloudbuild-cd.yaml @@ -78,7 +78,7 @@ steps: ENDPOINT="$${ENDPOINTS[$$INDEX]}" echo "Checking $$ENDPOINT" - docker container run --user root -v zapvolume:/zap/wrk/:rw -t owasp/zap2docker-stable zap-baseline.py -t http://$$ENDPOINT -r zap_report_${_TARGET_ID}_${_RELEASE_ID}.html -z '-config api.disablekey=true' -I + docker container run --user root -v zapvolume:/zap/wrk/:rw -t zaproxy/zap-stable zap-baseline.py -t http://$$ENDPOINT -r zap_report_${_TARGET_ID}_${_RELEASE_ID}.html -z '-config api.disablekey=true' -I cp /zap/wrk/zap_report_${_TARGET_ID}_${_RELEASE_ID}.html /workspace/zap_report_${_TARGET_ID}_${_RELEASE_ID}.html volumes: - name: 'zapvolume' diff --git a/examples/app_cicd/README.md b/examples/app_cicd/README.md deleted file mode 100644 index 652b7dc0..00000000 --- a/examples/app_cicd/README.md +++ /dev/null @@ -1,151 +0,0 @@ -# App CI/CD example - -This end to end example showcases the `secure-ci` and `secure-cd` modules working together to create a secure software build and deploy pipeline. - -We will use the Bank of Anthos sample application as the target code on which to execute the CI/CD pipeline. - -## Prerequisites -* GKE cluster(s) to deploy workloads, specified using the `deploy_branch_clusters` variable. See the [secure-cd module](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/tree/main/modules/secure-cd) for details on specifying the `deploy_branch_clusters` object. -* Run `gcloud auth application-default login` before following the steps below. - -## Setup - -To deploy this example: - -### Build the infrastructure - -1. Create working directory -```sh -mkdir cicd-example -cd cicd-example -``` -2. Clone `secure-cicd` repo -```sh -git clone https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd.git -``` -3. Run `terraform init` from within this example directory. -```sh -cd terraform-google-secure-cicd/examples/app_cicd -terraform init -``` -4. Create a `terraform.tfvars` file to provide values for `project_id` and `deploy_branch_clusters`. Optionally override any variables if necessary. - -5. Run `terraform apply` within this example directory. - -### Sample application configuration -6. Export CI/CD project ID -```sh -export PROJECT_ID= -``` -7. Return to working directory -```sh -cd ../../.. -``` -8. Clone the Bank of Anthos sample application repo -```sh -git clone --branch v0.5.3 https://github.com/GoogleCloudPlatform/bank-of-anthos.git -``` -9. Clone the `app-dry-manifests` repo -```sh -gcloud source repos clone app-dry-manifests --project=$PROJECT_ID -``` -10. Copy the Bank of Anthos Kubernetes manifests to the `app-dry-manifests` repo -```sh -cp bank-of-anthos/dev-kubernetes-manifests/* app-dry-manifests/ -``` -11. Copy the Skaffold config to the `app-dry-manifests` repo -```sh -cp bank-of-anthos/skaffold.yaml app-dry-manifests/ -``` -12. Replace Skaffold manifest paths with repo path -```sh -sed -i 's/dev-kubernetes-manifests/app-dry-manifests/g' app-dry-manifests/skaffold.yaml -``` -13. Push `app-dry-manifests` changes -```sh -cd app-dry-manifests/ -git add . -git commit -m "initial commit" -git push -cd .. -``` - -14. Copy `cloudbuild-ci.yaml` to `app-source` repo -```sh -cp terraform-google-secure-cicd/build/cloudbuild-ci.yaml bank-of-anthos/ -``` -15. Copy `policies` folder to `app-source` repo -```sh -cp -R terraform-google-secure-cicd/examples/app_cicd/policies bank-of-anthos/policies -``` - -16. Add `app-source` repo as new remote for Bank of Anthos source code and push -```sh -cd bank-of-anthos -git remote add google https://source.developers.google.com/p/$PROJECT_ID/r/app-source -git push --all google -``` -17. Return to working directory -```sh -cd .. -``` -18. Clone `app-wet-manifests` repo -```sh -gcloud source repos clone app-wet-manifests --project=$PROJECT_ID -``` - -19. Copy `cloudbuild-cd.yaml` to `app-wet-manifests` repo -```sh -cp terraform-google-secure-cicd/build/cloudbuild-cd.yaml app-wet-manifests/ -``` -20. Push changes in `app-wet-manifests` repo -```sh -cd app-wet-manifests -git add . -git commit -m "initial config commit" -git push -``` -## Troubleshooting -### Container Structure Test -Default checks in the `container-structure-test` step of the CI phase may fail on some containers. To ignore the violations and pass the checks for demonstration purposes, comment out the violated policies defined in the [policies/container-structure-policy.yaml](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/blob/main/examples/app_cicd/policies/container-structure-policy.yaml) file and push the changes to the `app-source` repo, triggering a new build. - -### Deploying from Cloud Shell -Sometimes when running Terraform from Google Cloud Shell, the system may encounter the following error or similar: -``` -dial tcp [IP_V6_ADDRESS]:443: connect: cannot assign requested address -``` -This is a [known issue](https://github.com/hashicorp/terraform-provider-google/issues/6782) addressed in the Google Terraform provider open source issues. - -Run [this command](https://github.com/hashicorp/terraform-provider-google/issues/6782#issuecomment-874574409) in Cloud Shell as a workaround: -```sh -sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1 net.ipv6.conf.default.disable_ipv6=1 net.ipv6.conf.lo.disable_ipv6=1 > /dev/null -export APIS="googleapis.com www.googleapis.com storage.googleapis.com iam.googleapis.com container.googleapis.com cloudresourcemanager.googleapis.com" -for name in $APIS -do - ipv4=$(getent ahostsv4 "$name" | head -n 1 | awk '{ print $1 }') - grep -q "$name" /etc/hosts || ([ -n "$ipv4" ] && sudo sh -c "echo '$ipv4 $name' >> /etc/hosts") -done -``` - - -## Inputs - -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| deploy\_branch\_clusters | mapping of branch names to cluster deployments. target\_type can be one of `gke`, `anthos_cluster`, or `run`. See [clouddeploy\_target Terraform docs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/clouddeploy_target) for more details |
map(object({
cluster = string
anthos_membership = string
project_id = string
location = string
required_attestations = list(string)
env_attestation = string
next_env = string
target_type = string
}))
| `{}` | no | -| primary\_location | Region used for key-ring | `string` | `"us-central1"` | no | -| project\_id | Project ID for CICD Pipeline Project | `string` | n/a | yes | - -## Outputs - -| Name | Description | -|------|-------------| -| binauth\_attestor\_names | Names of Attestors | -| binauth\_attestor\_project\_id | Project ID where attestors get created | -| boa\_artifact\_repo | GAR Repo created to store BoA images | -| build\_trigger\_name | The name of the cloud build trigger for the bank of anthos repo. | -| cache\_bucket\_name | The name of the storage bucket for cloud build. | -| project\_id | The project to run tests against | -| source\_repo\_names | Name of the created CSR repos | - - diff --git a/examples/app_cicd/cloud-build-builder/Dockerfile b/examples/app_cicd/cloud-build-builder/Dockerfile deleted file mode 100644 index 695a7766..00000000 --- a/examples/app_cicd/cloud-build-builder/Dockerfile +++ /dev/null @@ -1,72 +0,0 @@ -# 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 -# -# 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. - -# Start by building the application. -FROM golang:1.15-buster as kritis-build - -# Install kritis -WORKDIR /go/src -RUN git clone https://github.com/grafeas/kritis -RUN mv kritis app -RUN go get -d -v ./... -WORKDIR /go/src/app -RUN make out/signer -RUN cp out/signer /signer - -FROM alpine:3.18 - -### 1. Get Java via the package manager -RUN apk update \ -&& apk upgrade \ -&& apk add --no-cache bash curl jq openjdk17-jre git openssh \ -&& apk add --no-cache --virtual=build-dependencies unzip - -### Set JAVA_HOME -ENV JAVA_HOME="/usr/lib/jvm/java-17-openjdk" - -### 2. Get Python, PIP -RUN apk add --no-cache python3 \ -&& python3 -m ensurepip \ -&& pip3 install --upgrade pip setuptools \ -&& rm -r /usr/lib/python*/ensurepip && \ -if [ ! -e /usr/bin/pip ]; then ln -s pip3 /usr/bin/pip ; fi && \ -if [[ ! -e /usr/bin/python ]]; then ln -sf /usr/bin/python3 /usr/bin/python; fi && \ -rm -r /root/.cache - -#### 3. Install gcloud -ENV CLOUD_SDK_VERSION="423.0.0" -ENV CLOUDSDK_INSTALL_DIR /usr/local/gcloud/ -RUN wget "https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-${CLOUD_SDK_VERSION}-linux-x86_64.tar.gz" \ -&& tar -C /usr/local -xzf "google-cloud-sdk-${CLOUD_SDK_VERSION}-linux-x86_64.tar.gz" \ -&& rm "google-cloud-sdk-${CLOUD_SDK_VERSION}-linux-x86_64.tar.gz" -ENV PATH $PATH:/usr/local/google-cloud-sdk/bin - -#### Install skaffold and other components via gcloud -RUN gcloud components install skaffold kubectl kustomize kpt nomos beta - -##### 4. Add the kritis signer to the commands -COPY --from=kritis-build /signer . - -#### 4. Install YQ for parsing -RUN curl -Lo yq https://github.com/mikefarah/yq/releases/download/v4.6.1/yq_linux_amd64 -RUN chmod +x yq -RUN mv ./yq /usr/local/bin - -# Print out java version -RUN echo -e "JAVA_VERSION: $(${JAVA_HOME}/bin/java --version)" -RUN echo -e "PYTHON: $(python --version)\nPIP: $(pip --version)" - -RUN gcloud --version - -CMD ["/bin/bash"] diff --git a/examples/app_cicd/cloud-build-builder/cloudbuild-skaffold-build-image.yaml b/examples/app_cicd/cloud-build-builder/cloudbuild-skaffold-build-image.yaml deleted file mode 100644 index 6f4fa3d3..00000000 --- a/examples/app_cicd/cloud-build-builder/cloudbuild-skaffold-build-image.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# 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 -# -# 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. - -options: - # Use higher CPU machines so the caching and build steps are faster. - machineType: 'E2_HIGHCPU_32' -steps: -- name: 'gcr.io/cloud-builders/docker' - args: [ 'build', '-t', '$_DEFAULT_REGION-docker.pkg.dev/$PROJECT_ID/$_GAR_REPOSITORY/skaffold-builder', '.' ] -- name: 'gcr.io/cloud-builders/docker' - args: ['push', '$_DEFAULT_REGION-docker.pkg.dev/$PROJECT_ID/$_GAR_REPOSITORY/skaffold-builder'] -substitutions: - _DEFAULT_REGION: 'us-east1' # default value - _GAR_REPOSITORY: '$PROJECT_ID-app-image-repo' # default value diff --git a/examples/app_cicd/main.tf b/examples/app_cicd/main.tf deleted file mode 100644 index 335048b9..00000000 --- a/examples/app_cicd/main.tf +++ /dev/null @@ -1,56 +0,0 @@ -/** - * 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. - */ - -locals { - clouddeploy_pipeline_name = "pipeline-01" -} - -module "ci_pipeline" { - source = "GoogleCloudPlatform/secure-cicd/google//modules/secure-ci" - version = "~> 1.0" - - project_id = var.project_id - app_source_repo = "app-source" - cloudbuild_cd_repo = "cloudbuild-cd-config" - gar_repo_name_suffix = "app-image-repo" - primary_location = var.primary_location - attestor_names_prefix = ["build", "security", "quality"] - cache_bucket_name = "app-cloudbuild" - app_build_trigger_yaml = "cloudbuild-ci.yaml" - runner_build_folder = "${path.module}/cloud-build-builder" - build_image_config_yaml = "cloudbuild-skaffold-build-image.yaml" - trigger_branch_name = ".*" - clouddeploy_pipeline_name = local.clouddeploy_pipeline_name -} - -module "cd_pipeline" { - source = "GoogleCloudPlatform/secure-cicd/google//modules/secure-cd" - version = "~> 1.0" - - project_id = var.project_id - primary_location = var.primary_location - - gar_repo_name = module.ci_pipeline.app_artifact_repo - cloudbuild_cd_repo = "cloudbuild-cd-config" - deploy_branch_clusters = var.deploy_branch_clusters - app_deploy_trigger_yaml = "cloudbuild-cd.yaml" - cache_bucket_name = module.ci_pipeline.cache_bucket_name - clouddeploy_pipeline_name = local.clouddeploy_pipeline_name - cloudbuild_service_account = module.ci_pipeline.build_sa_email - depends_on = [ - module.ci_pipeline - ] -} diff --git a/examples/app_cicd/outputs.tf b/examples/app_cicd/outputs.tf deleted file mode 100644 index dd68e5d8..00000000 --- a/examples/app_cicd/outputs.tf +++ /dev/null @@ -1,50 +0,0 @@ -/** - * 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. - */ - -output "project_id" { - value = var.project_id - description = "The project to run tests against" -} - -output "binauth_attestor_names" { - description = "Names of Attestors" - value = module.ci_pipeline.binauth_attestor_names -} - -output "binauth_attestor_project_id" { - description = "Project ID where attestors get created" - value = module.ci_pipeline.binauth_attestor_project_id -} - -output "boa_artifact_repo" { - description = "GAR Repo created to store BoA images" - value = module.ci_pipeline.app_artifact_repo -} - -output "cache_bucket_name" { - description = "The name of the storage bucket for cloud build." - value = module.ci_pipeline.cache_bucket_name -} - -output "build_trigger_name" { - description = "The name of the cloud build trigger for the bank of anthos repo." - value = module.ci_pipeline.build_trigger_name -} - -output "source_repo_names" { - description = "Name of the created CSR repos" - value = module.ci_pipeline.source_repo_names -} diff --git a/examples/app_cicd/policies/container-analysis-policy.yaml b/examples/app_cicd/policies/container-analysis-policy.yaml deleted file mode 100644 index 82ba2fce..00000000 --- a/examples/app_cicd/policies/container-analysis-policy.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# 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 -# -# 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. - -apiVersion: kritis.grafeas.io/v1beta1 -kind: VulnzSigningPolicy -metadata: - name: vulnerability-policy -spec: - imageVulnerabilityRequirements: - maximumFixableSeverity: CRITICAL #MEDIUM - maximumUnfixableSeverity: ALLOW_ALL #MEDIUM - allowlistCVEs: - - projects/goog-vulnz/notes/CVE-2020-10543 - - projects/goog-vulnz/notes/CVE-2020-1751 - - projects/goog-vulnz/notes/CVE-2020-29562 - - projects/goog-vulnz/notes/CVE-2019-25013 - - projects/goog-vulnz/notes/CVE-2021-3326 diff --git a/examples/app_cicd/policies/container-structure-policy.yaml b/examples/app_cicd/policies/container-structure-policy.yaml deleted file mode 100644 index 8b75d8bf..00000000 --- a/examples/app_cicd/policies/container-structure-policy.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# 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 -# -# 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. - -schemaVersion: "2.0.0" - -globalEnvVars: - - key: "PATH" - value: "/env/bin:$PATH" - -fileExistenceTests: -- name: 'Root folder is executable' - path: '/' - shouldExist: true - permissions: 'drwxr-xr-x' - uid: 0 - gid: 0 - isExecutableBy: 'group' -# - name: 'Debian Sources do NOT exist' -# path: '/etc/apt/sources.list' -# shouldExist: true # TODO Set this to "false" to fail the test -# - name: 'Bash shell is available' # NOTE: In real world, remove bash, sources and ssh to improve security -# path: '/bin/bash' -# shouldExist: true # TODO Set this to "false" to fail the test -- name: 'No SSH' - path: '/usr/bin/ssh' - shouldExist: false diff --git a/examples/app_cicd/policies/githound.yaml b/examples/app_cicd/policies/githound.yaml deleted file mode 100644 index 55edf755..00000000 --- a/examples/app_cicd/policies/githound.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# 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 -# -# 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 warning on match but continue -warn: - - /^([a-z0-9_\.\+-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$/ # emails - - '(^|[^\-])\busername\b([^\-]|$)' # username :|= - - '[''"](?!.*[\s])(?=.*[A-Za-z])(?=.*[0-9])(?=.*[!@#$&*])?.{16,}[''"]' - - '\/Users\/\w+\/' - - '(?i)db_(user(name)?|pass(word)?|name)\W*[:=,]\W*.+$' - - '(^|[^\-])\bpassword\b([^\-]|$)' # password :|= -# Fail immediately upon match -fail: - - '(? -## Inputs - -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| gke\_networks | list of GKE cluster networks in which to create VPN connections |
list(object({
control_plane_cidrs = map(string)
location = string
network = string
project_id = string
}))
| n/a | yes | -| primary\_location | Default region for resources | `string` | n/a | yes | -| project\_id | Project ID for CICD Pipeline Project | `string` | n/a | yes | - -## Outputs - -| Name | Description | -|------|-------------| -| project\_id | The project to run tests against | - - diff --git a/examples/cloudbuild_private_pool/main.tf b/examples/cloudbuild_private_pool/main.tf deleted file mode 100644 index 69e3ccca..00000000 --- a/examples/cloudbuild_private_pool/main.tf +++ /dev/null @@ -1,77 +0,0 @@ -/** - * 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. - */ - -module "cloudbuild_private_pool" { - source = "GoogleCloudPlatform/secure-cicd/google//modules/cloudbuild-private-pool" - version = "~> 1.0" - - project_id = var.project_id - network_project_id = var.project_id - location = var.primary_location - worker_pool_name = "cloudbuild-private-worker-pool" - create_cloudbuild_network = true - private_pool_vpc_name = "workerpool-example-vpc" - worker_address = "10.37.0.0" - worker_range_name = "gke-private-pool-worker-range" -} - -locals { - gke_networks = { - for net in var.gke_networks : net.network => merge(net, local.vpn_config[net.network]) - } - vpn_config = { - "gke-private-vpc-dev" = { - gateway_1_asn = 65001, - gateway_2_asn = 65002, - bgp_range_1 = "169.254.1.0/30", - bgp_range_2 = "169.254.2.0/30" - }, - "gke-private-vpc-qa" = { - gateway_1_asn = 65003, - gateway_2_asn = 65004, - bgp_range_1 = "169.254.3.0/30", - bgp_range_2 = "169.254.4.0/30" - }, - "gke-private-vpc-prod" = { - gateway_1_asn = 65005, - gateway_2_asn = 65006, - bgp_range_1 = "169.254.5.0/30", - bgp_range_2 = "169.254.6.0/30" - } - } -} - -module "gke_cloudbuild_vpn" { - for_each = local.gke_networks - - source = "GoogleCloudPlatform/secure-cicd/google//modules/workerpool-gke-ha-vpn" - version = "~> 1.0" - - project_id = var.project_id - location = "us-central1" - - gke_project = each.value.project_id - gke_network = each.value.network - gke_location = each.value.location - gke_control_plane_cidrs = each.value.control_plane_cidrs - - workerpool_network = module.cloudbuild_private_pool.workerpool_network - workerpool_range = module.cloudbuild_private_pool.workerpool_range - gateway_1_asn = each.value.gateway_1_asn - gateway_2_asn = each.value.gateway_2_asn - bgp_range_1 = each.value.bgp_range_1 - bgp_range_2 = each.value.bgp_range_2 -} diff --git a/examples/standalone_single_project/cicd.tf b/examples/standalone_single_project/cicd.tf index ecb2927c..fc5a965b 100644 --- a/examples/standalone_single_project/cicd.tf +++ b/examples/standalone_single_project/cicd.tf @@ -23,8 +23,8 @@ locals { network = module.vpc.network_name project_id = var.project_id location = var.region - required_attestations = [module.ci_pipeline.binauth_attestor_ids["build"]] - env_attestation = module.ci_pipeline.binauth_attestor_ids["security"] + required_attestations = [module.attestors.binauth_attestor_ids["build"]] + env_attestation = module.attestors.binauth_attestor_ids["security"] next_env = "02-qa" }, "02-${var.env2_name}" = { @@ -34,8 +34,8 @@ locals { network = module.vpc.network_name project_id = var.project_id location = var.region - required_attestations = [module.ci_pipeline.binauth_attestor_ids["security"], module.ci_pipeline.binauth_attestor_ids["build"]] - env_attestation = module.ci_pipeline.binauth_attestor_ids["quality"] + required_attestations = [module.attestors.binauth_attestor_ids["security"], module.attestors.binauth_attestor_ids["build"]] + env_attestation = module.attestors.binauth_attestor_ids["quality"] next_env = "03-prod" }, "03-${var.env3_name}" = { @@ -45,7 +45,7 @@ locals { network = module.vpc.network_name project_id = var.project_id location = var.region - required_attestations = [module.ci_pipeline.binauth_attestor_ids["quality"], module.ci_pipeline.binauth_attestor_ids["security"], module.ci_pipeline.binauth_attestor_ids["build"]] + required_attestations = [module.attestors.binauth_attestor_ids["quality"], module.attestors.binauth_attestor_ids["security"], module.attestors.binauth_attestor_ids["build"]] env_attestation = "" next_env = "" }, @@ -56,16 +56,16 @@ locals { # Secure-CI module "ci_pipeline" { - source = "GoogleCloudPlatform/secure-cicd/google//modules/secure-ci" - version = "~> 1.0" + source = "../../modules/secure-ci" project_id = var.project_id - app_source_repo = "${var.app_name}-source" - cloudbuild_cd_repo = "${var.app_name}-cloudbuild-cd-config" + repository_type = "GITHUB" + github_auth = var.github_auth + ci_repository = var.ci_repository gar_repo_name_suffix = "${var.app_name}-image-repo" cache_bucket_name = "${var.app_name}-cloudbuild" primary_location = var.region - attestor_names_prefix = ["build", "security", "quality"] + attestor_names_prefix = module.attestors.binauth_attestor_names app_build_trigger_yaml = "cloudbuild-ci.yaml" build_image_config_yaml = "cloudbuild-skaffold-build-image.yaml" trigger_branch_name = ".*" @@ -73,34 +73,39 @@ module "ci_pipeline" { clouddeploy_pipeline_name = local.clouddeploy_pipeline_name skip_provisioners = true labels = var.labels + providers = { + google = google.secure_cicd + } } -# Secure-CD module "cd_pipeline" { - source = "GoogleCloudPlatform/secure-cicd/google//modules/secure-cd" - version = "~> 1.0" + source = "../../modules/secure-cd" project_id = var.project_id - primary_location = var.region + primary_location = "us-central1" + repository_type = "GITHUB" + github_auth = var.github_auth gar_repo_name = module.ci_pipeline.app_artifact_repo - cloudbuild_cd_repo = "${var.app_name}-cloudbuild-cd-config" + cd_repository = var.cd_repository deploy_branch_clusters = local.deploy_branch_clusters app_deploy_trigger_yaml = "cloudbuild-cd.yaml" cache_bucket_name = module.ci_pipeline.cache_bucket_name cloudbuild_private_pool = module.cloudbuild_private_pool.workerpool_id clouddeploy_pipeline_name = local.clouddeploy_pipeline_name cloudbuild_service_account = module.ci_pipeline.build_sa_email - labels = var.labels depends_on = [ module.ci_pipeline ] + providers = { + google = google.secure_cicd + } } + # Cloud Build Private Pool module "cloudbuild_private_pool" { - source = "GoogleCloudPlatform/secure-cicd/google//modules/cloudbuild-private-pool" - version = "~> 1.0" + source = "GoogleCloudPlatform/secure-cicd/google//modules/cloudbuild-private-pool" project_id = var.project_id network_project_id = var.project_id @@ -116,3 +121,13 @@ module "cloudbuild_private_pool" { labels = var.labels } + +module "attestors" { + source = "../../modules/attestor" + + project_id = var.project_id + + primary_location = var.region + + attestor_names_prefix = ["build", "security", "quality"] +} diff --git a/examples/standalone_single_project/gke.tf b/examples/standalone_single_project/gke.tf index 596f5525..65e1e6db 100644 --- a/examples/standalone_single_project/gke.tf +++ b/examples/standalone_single_project/gke.tf @@ -39,13 +39,12 @@ resource "random_shuffle" "available_zones" { module "gke_cluster" { for_each = toset(local.envs) source = "terraform-google-modules/kubernetes-engine/google//modules/private-cluster" - version = "~> 25.0" + version = "44.0" project_id = var.project_id name = "${var.app_name}-cluster-${each.value}" regional = true region = var.region - zones = sort(random_shuffle.available_zones.result) network = module.vpc.network_name subnetwork = local.subnets[each.value].subnet_name ip_range_pods = "${local.subnets[each.value].subnet_name}-gke-pods" @@ -78,13 +77,21 @@ module "gke_cluster" { } ] + # --- START OF THE CORRECT FIX --- node_pools = [ { - name = "default-node-pool" + name = "default-node-pool" + machine_type = "e2-medium" # This was the missing required parameter + disk_size_gb = 100 # It's good practice to define disk size + + # Your existing autoscaling settings are correct for a regional cluster location_policy = "BALANCED" total_max_node_count = 2 + # For clarity and robustness, also define the minimum + total_min_node_count = 1 } ] + # --- END OF THE CORRECT FIX --- node_pools_labels = { all = var.labels @@ -92,14 +99,18 @@ module "gke_cluster" { cluster_resource_labels = var.labels depends_on = [ - module.vpc + module.vpc, + data.google_compute_zones.available, + random_shuffle.available_zones ] } + + module "fleet_membership" { for_each = toset(local.envs) source = "terraform-google-modules/kubernetes-engine/google//modules/fleet-membership" - version = "~> 25.0.0" + version = "~> 44.0" membership_name = "${module.gke_cluster[each.value].name}-membership" project_id = var.project_id diff --git a/examples/standalone_single_project/network.tf b/examples/standalone_single_project/network.tf index 033a2130..534042ec 100644 --- a/examples/standalone_single_project/network.tf +++ b/examples/standalone_single_project/network.tf @@ -40,7 +40,7 @@ locals { # Private Cluster VPCs module "vpc" { source = "terraform-google-modules/network/google" - version = "~> 6.0" + version = "~> 18.0" project_id = var.project_id network_name = "${var.app_name}-vpc" @@ -49,14 +49,3 @@ module "vpc" { subnets = values(local.subnets) secondary_ranges = local.secondary_ranges } - -resource "google_compute_network_peering_routes_config" "gke_peering_routes_config" { - for_each = toset(local.envs) - - project = var.project_id - peering = module.gke_cluster[each.value].peering_name - network = module.vpc.network_name - - import_custom_routes = true - export_custom_routes = true -} diff --git a/examples/standalone_single_project/outputs.tf b/examples/standalone_single_project/outputs.tf index 4f4c2b87..a2faab43 100644 --- a/examples/standalone_single_project/outputs.tf +++ b/examples/standalone_single_project/outputs.tf @@ -14,9 +14,14 @@ * limitations under the License. */ -output "app_source_repo" { - description = "URL of the created CSR app soure repo" - value = module.ci_pipeline.source_repo_urls["${var.app_name}-source"] +output "project_id" { + description = "Project ID" + value = var.project_id +} + +output "region" { + description = "Region" + value = var.region } output "cloudbuild_cd_repo_name" { @@ -30,11 +35,20 @@ output "gar_repo" { } output "neos_tutorial_url" { - description = "The URL to launch the in-console tutorial for the Secure CI/CD pipeline solution" value = "https://console.cloud.google.com/products/solutions/catalog?walkthrough_id=solutions-in-console--secure-cicd-pipeline--tour&project=${var.project_id}" } output "console_walkthrough_link" { - description = "URL to open the in-console walkthrough." value = "https://shell.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2FGoogleCloudPlatform%2Fterraform-google-secure-cicd.git&cloudshell_git_branch=main&cloudshell_tutorial=examples%2Fstandalone_single_project%2Fwalkthrough.md&project=${var.project_id}" } + +output "clouddeploy_pipeline_id" { + description = "ID of the Cloud Deploy delivery pipeline" + value = module.cd_pipeline.clouddeploy_delivery_pipeline_id +} + +output "cluster_names" { + description = "Comma-separated names of the deployed GKE clusters" + # Use join() to create a single string like: "cluster-dev,cluster-qa,cluster-prod" + value = join(",", [for k, v in module.gke_cluster : v.name]) +} diff --git a/examples/standalone_single_project/variables.tf b/examples/standalone_single_project/variables.tf index 9532f679..f64e0509 100644 --- a/examples/standalone_single_project/variables.tf +++ b/examples/standalone_single_project/variables.tf @@ -37,6 +37,40 @@ variable "env1_name" { default = "dev" } +variable "repo_type" { + type = string + description = "Name of environment 1" + default = "CSR" +} + +variable "github_auth" { + type = object({ + secret_id = string + app_id_secret_id = string + secret_project_id = string + }) + description = "Authentication configuration for GitHub. Required only if repo_type is 'GITHUBv2'." + default = null +} + +variable "ci_repository" { + type = object({ + repository_name = string + repository_url = string + }) + description = "The CI repository to configure. The key is a short name for the service." + default = null +} + +variable "cd_repository" { + type = object({ + repository_name = string + repository_url = string + }) + description = "The CI repository to configure. The key is a short name for the service." + default = null +} + variable "env2_name" { type = string description = "Name of environment 2" diff --git a/examples/standalone_single_project/versions.tf b/examples/standalone_single_project/versions.tf index 376e2f47..29100426 100644 --- a/examples/standalone_single_project/versions.tf +++ b/examples/standalone_single_project/versions.tf @@ -18,11 +18,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "< 4.75.0" + version = "> 4, < 8" } - google-beta = { - source = "hashicorp/google-beta" - version = "< 4.75.0" + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.10, < 4" } } } diff --git a/modules/secure-ci/binauthz.tf b/modules/attestor/main.tf similarity index 97% rename from modules/secure-ci/binauthz.tf rename to modules/attestor/main.tf index 660e27eb..4a7a3615 100644 --- a/modules/secure-ci/binauthz.tf +++ b/modules/attestor/main.tf @@ -33,7 +33,7 @@ resource "google_kms_key_ring" "keyring" { module "attestors" { source = "terraform-google-modules/kubernetes-engine/google//modules/binary-authorization" - version = "~> 25.0.0" + version = "~> 44.0.0" for_each = toset(var.attestor_names_prefix) project_id = var.project_id diff --git a/examples/cloudbuild_private_pool/outputs.tf b/modules/attestor/outputs.tf similarity index 54% rename from examples/cloudbuild_private_pool/outputs.tf rename to modules/attestor/outputs.tf index b2acdd66..8faae13f 100644 --- a/examples/cloudbuild_private_pool/outputs.tf +++ b/modules/attestor/outputs.tf @@ -14,7 +14,17 @@ * limitations under the License. */ -output "project_id" { +output "binauth_attestor_names" { + description = "Names of Attestors" + value = [for attestor_name in var.attestor_names_prefix : module.attestors[attestor_name].attestor] +} + +output "binauth_attestor_ids" { + description = "IDs of Attestors" + value = { for attestor_name in var.attestor_names_prefix : attestor_name => "projects/${var.project_id}/attestors/${module.attestors[attestor_name].attestor}" } +} + +output "binauth_attestor_project_id" { + description = "Project ID where attestors get created" value = var.project_id - description = "The project to run tests against" } diff --git a/examples/cloudbuild_private_pool/variables.tf b/modules/attestor/variables.tf similarity index 70% rename from examples/cloudbuild_private_pool/variables.tf rename to modules/attestor/variables.tf index 8541b1dc..f1c50040 100644 --- a/examples/cloudbuild_private_pool/variables.tf +++ b/modules/attestor/variables.tf @@ -21,15 +21,10 @@ variable "project_id" { variable "primary_location" { type = string - description = "Default region for resources" + description = "Region used for key-ring" } -variable "gke_networks" { - type = list(object({ - control_plane_cidrs = map(string) - location = string - network = string - project_id = string - })) - description = "list of GKE cluster networks in which to create VPN connections" +variable "attestor_names_prefix" { + description = "A list of Binary Authorization attestors to create. The first attestor specified in this list will be used as the build-attestor during the CI phase." + type = list(string) } diff --git a/test/fixtures/app_cicd/versions.tf b/modules/attestor/versions.tf similarity index 75% rename from test/fixtures/app_cicd/versions.tf rename to modules/attestor/versions.tf index c001c4e3..0523f03c 100644 --- a/test/fixtures/app_cicd/versions.tf +++ b/modules/attestor/versions.tf @@ -15,5 +15,14 @@ */ terraform { - required_version = ">= 0.13" + required_providers { + google = { + source = "hashicorp/google" + version = "> 4, < 8" + } + random = { + source = "hashicorp/random" + version = ">= 3.7.2" + } + } } diff --git a/modules/cloudbuild-private-pool/main.tf b/modules/cloudbuild-private-pool/main.tf index a97e797f..bd3c0515 100644 --- a/modules/cloudbuild-private-pool/main.tf +++ b/modules/cloudbuild-private-pool/main.tf @@ -49,8 +49,6 @@ data "google_compute_network" "workerpool_vpc" { } resource "google_compute_global_address" "worker_range" { - provider = google-beta # labels support require google-beta - name = var.worker_range_name project = var.network_project_id labels = var.labels diff --git a/modules/cloudbuild-private-pool/versions.tf b/modules/cloudbuild-private-pool/versions.tf index 93f6b92e..4f1e1771 100644 --- a/modules/cloudbuild-private-pool/versions.tf +++ b/modules/cloudbuild-private-pool/versions.tf @@ -19,18 +19,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 4.3.0" # google_cloudbuild_worker_pool in GA requires >= 4.3.0 + version = ">= 4.3.0 < 8" # google_cloudbuild_worker_pool in GA requires >= 4.3.0 } - google-beta = { + google_beta = { source = "hashicorp/google-beta" - version = ">= 4.3.0" + version = ">= 4.3.0 < 8" } } - - provider_meta "google" { - module_name = "blueprints/terraform/terraform-google-secure-cicd:cloudbuild-private-pool/v1.2.1" - } - provider_meta "google-beta" { - module_name = "blueprints/terraform/terraform-google-secure-cicd:cloudbuild-private-pool/v1.2.1" - } } diff --git a/modules/secure-cd/build.tf b/modules/secure-cd/build.tf index d0bc80f0..0a021281 100644 --- a/modules/secure-cd/build.tf +++ b/modules/secure-cd/build.tf @@ -24,6 +24,7 @@ resource "google_pubsub_topic" "clouddeploy_topic" { # Trigger post-deploy checks on successful Cloud Deploy rollout resource "google_cloudbuild_trigger" "deploy_trigger" { + # This for_each loop remains unchanged from your original code. for_each = { for env, config in var.deploy_branch_clusters : env => config if config.next_env != "" @@ -36,16 +37,20 @@ resource "google_cloudbuild_trigger" "deploy_trigger" { service_account = "projects/${var.project_id}/serviceAccounts/${var.cloudbuild_service_account}" + # This Pub/Sub trigger configuration remains unchanged. pubsub_config { topic = google_pubsub_topic.clouddeploy_topic.id } + # This block now dynamically points to the correct repository source. source_to_build { - uri = "https://source.developers.google.com/p/${var.project_id}/r/${var.cloudbuild_cd_repo}" - ref = "main" - repo_type = "CLOUD_SOURCE_REPOSITORIES" + uri = local.cd_repo_source.uri + ref = "main" + repo_type = local.cd_repo_source.repo_type + repository = local.cd_repo_source.repository } + # The substitutions block is complete and unchanged. substitutions = merge( { _GAR_REPOSITORY = var.gar_repo_name @@ -60,17 +65,16 @@ resource "google_cloudbuild_trigger" "deploy_trigger" { _ATTESTOR_NAME = each.value.env_attestation _CLOUDBUILD_PRIVATE_POOL = var.cloudbuild_private_pool _CLOUDDEPLOY_PIPELINE_NAME = var.clouddeploy_pipeline_name - # Create substitutions to parse incoming Pub/sub messages from Cloud Deploy + # Substitutions to parse incoming Pub/sub messages from Cloud Deploy _ACTION_TYPE = "$(body.message.attributes.Action)" _RESOURCE_TYPE = "$(body.message.attributes.ResourceType)" _DELIVERY_PIPELINE_ID = "$(body.message.attributes.DeliveryPipelineId)" _TARGET_ID = "$(body.message.attributes.TargetId)" _RELEASE_ID = "$(body.message.attributes.ReleaseId)" - }, var.additional_substitutions ) - # Only trigger the post-deployment check on relevant Cloud Deploy activity (successful rollout to a target) + # The filter remains complete and unchanged. filter = "_RESOURCE_TYPE.matches('Rollout') && _ACTION_TYPE.matches('Succeed') && _DELIVERY_PIPELINE_ID.matches('${var.clouddeploy_pipeline_name}') && _TARGET_ID.matches('${google_clouddeploy_target.deploy_target[each.key].name}')" } diff --git a/modules/secure-cd/iam.tf b/modules/secure-cd/iam.tf index 70900e90..125d6645 100644 --- a/modules/secure-cd/iam.tf +++ b/modules/secure-cd/iam.tf @@ -48,7 +48,6 @@ resource "google_project_iam_member" "cd_sa_iam" { # Cloud Deploy Service Agent resource "google_project_service_identity" "clouddeploy_service_agent" { provider = google-beta - project = var.project_id service = "clouddeploy.googleapis.com" } diff --git a/modules/secure-cd/locals.tf b/modules/secure-cd/locals.tf new file mode 100644 index 00000000..5d23c40b --- /dev/null +++ b/modules/secure-cd/locals.tf @@ -0,0 +1,30 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + use_csr = var.repository_type == "CSR" + repos = local.use_csr ? {} : { for repo in [var.cd_repository] : repo.repository_name => repo } + + cd_repo_source = local.use_csr ? { + uri = "https://source.developers.google.com/p/${var.project_id}/r/${var.csr_cloudbuild_cd_repo}" + repo_type = "CLOUD_SOURCE_REPOSITORIES" + repository = null + } : { + uri = null + repo_type = var.repository_type == "GITHUB" ? var.repository_type : "UNKNOWN" + repository = values(module.cloudbuild_repositories[0].cloud_build_repositories_2nd_gen_repositories)[0].id + } +} diff --git a/modules/secure-cd/main.tf b/modules/secure-cd/main.tf index f59b240c..5fdfdffb 100644 --- a/modules/secure-cd/main.tf +++ b/modules/secure-cd/main.tf @@ -29,8 +29,40 @@ locals { clouddeploy_pubsub_topic_name = "clouddeploy-operations" } +resource "google_sourcerepo_repository" "csr_cd_repository" { + count = local.use_csr ? 1 : 0 + + project = var.project_id + name = var.csr_cloudbuild_cd_repo + create_ignore_already_exists = true +} + +module "cloudbuild_repositories" { + count = local.use_csr ? 0 : 1 + + source = "terraform-google-modules/bootstrap/google//modules/cloudbuild_repo_connection" + version = "12.0.0" + + project_id = var.project_id + + connection_config = { + connection_type = "${var.repository_type}v2" + + github_secret_id = var.github_auth != null ? var.github_auth.secret_id : null + github_app_id_secret_id = var.github_auth != null ? var.github_auth.app_id_secret_id : null + + gitlab_read_authorizer_credential_secret_id = var.gitlab_auth != null ? var.gitlab_auth.read_authorizer_credential_secret_id : null + gitlab_authorizer_credential_secret_id = var.gitlab_auth != null ? var.gitlab_auth.authorizer_credential_secret_id : null + gitlab_webhook_secret_id = var.gitlab_auth != null ? var.gitlab_auth.webhook_secret_id : null + gitlab_enterprise_host_uri = var.gitlab_auth != null ? var.gitlab_auth.enterprise_host_uri : null + gitlab_enterprise_service_directory = var.gitlab_auth != null ? var.gitlab_auth.enterprise_service_directory : null + gitlab_enterprise_ca_certificate = var.gitlab_auth != null ? var.gitlab_auth.enterprise_ca_certificate : null + } + + cloud_build_repositories = local.repos +} + resource "google_clouddeploy_target" "deploy_target" { - provider = google-beta for_each = var.deploy_branch_clusters name = each.value.target_type == "anthos_cluster" ? "${each.value.anthos_membership}-target" : each.value.target_type == "gke" ? "${each.value.cluster}-target" : "${each.key}-target" diff --git a/modules/secure-cd/variables.tf b/modules/secure-cd/variables.tf index a364630e..f9f57edc 100644 --- a/modules/secure-cd/variables.tf +++ b/modules/secure-cd/variables.tf @@ -24,9 +24,10 @@ variable "primary_location" { description = "Region used for key-ring" } -variable "cloudbuild_cd_repo" { +variable "csr_cloudbuild_cd_repo" { type = string - description = "Name of repo that stores the Cloud Build CD phase configs - for post-deployment checks" + description = "Name of the CSR repo that stores the Cloud Build CD phase configs - for post-deployment checks" + default = null } variable "gar_repo_name" { @@ -86,3 +87,61 @@ variable "cloudbuild_service_account" { description = "Cloud Build SA email address" type = string } + +variable "repository_type" { + description = "The type of the repository. Must be one of 'GITHUB', 'GITLAB', or 'CSR'." + type = string + validation { + condition = ( + var.repository_type != "GITHUB" || + (var.github_auth != null && var.gitlab_auth == null) + ) + error_message = "When repository_type is 'GITHUB', the 'github_auth' variable must be set, and 'gitlab_auth' must not be set." + } + validation { + condition = ( + var.repository_type != "GITLAB" || + (var.gitlab_auth != null && var.github_auth == null) + ) + error_message = "When repository_type is 'GITLAB', the 'gitlab_auth' variable must be set, and 'github_auth' must not be set." + } + validation { + condition = ( + var.repository_type != "CSR" || + (var.github_auth == null && var.gitlab_auth == null) + ) + error_message = "When repository_type is 'CSR', neither 'github_auth' nor 'gitlab_auth' should be set." + } +} + +variable "cd_repository" { + type = object({ + repository_name = string + repository_url = string + }) + description = "The CD repository to configure. The key is a short name for the service." + default = null +} +variable "github_auth" { + type = object({ + secret_id = string + app_id_secret_id = string + secret_project_id = string + }) + description = "Authentication configuration for GitHub. Required only if repo_type is 'GITHUBv2'." + default = null +} + +variable "gitlab_auth" { + type = object({ + read_authorizer_credential_secret_id = string + authorizer_credential_secret_id = string + webhook_secret_id = string + enterprise_host_uri = optional(string) + enterprise_service_directory = optional(string) + enterprise_ca_certificate = optional(string) + secret_project_id = string + }) + description = "Authentication configuration for GitLab. Required only if repo_type is 'GITLABv2'." + default = null +} diff --git a/modules/secure-cd/versions.tf b/modules/secure-cd/versions.tf index 2df9f881..8e38689f 100644 --- a/modules/secure-cd/versions.tf +++ b/modules/secure-cd/versions.tf @@ -15,22 +15,14 @@ */ terraform { - required_version = ">= 1.0" required_providers { google = { source = "hashicorp/google" - version = ">= 4.44, < 5.0" + version = "> 4, < 8" } google-beta = { source = "hashicorp/google-beta" - version = ">= 4.44, < 5.0" + version = "> 4, < 8" } } - - provider_meta "google" { - module_name = "blueprints/terraform/terraform-google-secure-cicd:secure-cd/v1.2.1" - } - provider_meta "google-beta" { - module_name = "blueprints/terraform/terraform-google-secure-cicd:secure-cd/v1.2.1" - } } diff --git a/test/fixtures/cloudbuild_private_pool/outputs.tf b/modules/secure-ci/checks.tf similarity index 61% rename from test/fixtures/cloudbuild_private_pool/outputs.tf rename to modules/secure-ci/checks.tf index 51febe77..550a1850 100644 --- a/test/fixtures/cloudbuild_private_pool/outputs.tf +++ b/modules/secure-ci/checks.tf @@ -1,5 +1,5 @@ /** - * Copyright 2021 Google LLC + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -output "project_id" { - description = "The ID of the project in which resources are provisioned." - value = var.project_id +check "gcs_bucket_name_length" { + assert { + condition = length(local.cache_bucket_name) < 63 + error_message = "The following constructed log bucket names are too long (max 63 characters): ${local.cache_bucket_name}. Please shorten the corresponding 'repository_name' in your variables or the 'project_id'." + } } diff --git a/modules/secure-ci/locals.tf b/modules/secure-ci/locals.tf new file mode 100644 index 00000000..a8529301 --- /dev/null +++ b/modules/secure-ci/locals.tf @@ -0,0 +1,44 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + gar_name = split("/", google_artifact_registry_repository.image_repo.name)[length(split("/", google_artifact_registry_repository.image_repo.name)) - 1] + cache_bucket_name = var.cache_bucket_name == "" ? "bkt-cloudbuild" : "${var.cache_bucket_name}" + use_csr = var.repository_type == "CSR" + repos = local.use_csr ? {} : { for repo in [var.ci_repository] : repo.repository_name => repo } + projects_re = "projects/([^/]+)/" + worker_pool_project = var.cloudbuild_private_pool != null ? regex(local.projects_re, var.cloudbuild_private_pool)[0] : null + kms_project = var.bucket_kms_key != null ? regex(local.projects_re, var.bucket_kms_key)[0] : null + second_gen_repo_url = local.use_csr ? null : values(module.cloudbuild_repositories[0].cloud_build_repositories_2nd_gen_repositories)[0].url + second_gen_repo_id = local.use_csr ? null : values(module.cloudbuild_repositories[0].cloud_build_repositories_2nd_gen_repositories)[0].id + second_gen_repo_name = local.use_csr ? null : keys(module.cloudbuild_repositories[0].cloud_build_repositories_2nd_gen_repositories)[0] + common_substitutions = merge( + { + _GAR_REPOSITORY = local.gar_name + _DEFAULT_REGION = var.primary_location + _CACHE_BUCKET_NAME = google_storage_bucket.cache_bucket.name + _ATTESTOR_NAME = keys(local.attestors)[0] + _CLOUDBUILD_PRIVATE_POOL = var.cloudbuild_private_pool + _CLOUDDEPLOY_PIPELINE_NAME = var.clouddeploy_pipeline_name + }, + var.additional_substitutions + ) + attestors = { + for attestor_name in toset(var.attestor_names_prefix) : attestor_name => { + id = "projects/${var.project_id}/attestors/${attestor_name}" + } + } +} diff --git a/modules/secure-ci/main.tf b/modules/secure-ci/main.tf index ca5afe82..5a456595 100644 --- a/modules/secure-ci/main.tf +++ b/modules/secure-ci/main.tf @@ -14,15 +14,37 @@ * limitations under the License. */ -locals { - gar_name = split("/", google_artifact_registry_repository.image_repo.name)[length(split("/", google_artifact_registry_repository.image_repo.name)) - 1] - cache_bucket_name = var.cache_bucket_name == "" ? "${var.project_id}-cloudbuild" : "${var.project_id}-${var.cache_bucket_name}" +resource "google_sourcerepo_repository" "csr_ci_repository" { + count = local.use_csr ? 1 : 0 + + project = var.project_id + name = var.csr_app_source_repo + create_ignore_already_exists = true } -resource "google_sourcerepo_repository" "repos" { - for_each = toset([var.cloudbuild_cd_repo, var.app_source_repo]) - name = each.key - project = var.project_id +module "cloudbuild_repositories" { + count = local.use_csr ? 0 : 1 + + source = "terraform-google-modules/bootstrap/google//modules/cloudbuild_repo_connection" + version = "12.0.0" + + project_id = var.project_id + + connection_config = { + connection_type = "${var.repository_type}v2" + + github_secret_id = var.github_auth != null ? var.github_auth.secret_id : null + github_app_id_secret_id = var.github_auth != null ? var.github_auth.app_id_secret_id : null + + gitlab_read_authorizer_credential_secret_id = var.gitlab_auth != null ? var.gitlab_auth.read_authorizer_credential_secret_id : null + gitlab_authorizer_credential_secret_id = var.gitlab_auth != null ? var.gitlab_auth.authorizer_credential_secret_id : null + gitlab_webhook_secret_id = var.gitlab_auth != null ? var.gitlab_auth.webhook_secret_id : null + gitlab_enterprise_host_uri = var.gitlab_auth != null ? var.gitlab_auth.enterprise_host_uri : null + gitlab_enterprise_service_directory = var.gitlab_auth != null ? var.gitlab_auth.enterprise_service_directory : null + gitlab_enterprise_ca_certificate = var.gitlab_auth != null ? var.gitlab_auth.enterprise_ca_certificate : null + } + + cloud_build_repositories = local.repos } resource "google_storage_bucket" "cache_bucket" { @@ -39,7 +61,7 @@ resource "google_storage_bucket" "cache_bucket" { resource "google_service_account" "build_sa" { account_id = "build-sa" - display_name = "Service Account for ${var.app_source_repo} Cloud Build triggers" + display_name = "Service Account for ${var.csr_app_source_repo} Cloud Build triggers" project = var.project_id } @@ -50,41 +72,41 @@ resource "google_storage_bucket_iam_member" "cloudbuild_artifacts_iam" { depends_on = [google_storage_bucket.cache_bucket] } -resource "google_cloudbuild_trigger" "app_build_trigger" { +resource "google_cloudbuild_trigger" "csr_app_build_trigger" { + count = local.use_csr ? 1 : 0 project = var.project_id - name = "${var.app_source_repo}-trigger" + name = "${var.csr_app_source_repo}-trigger" location = var.primary_location trigger_template { branch_name = var.trigger_branch_name - repo_name = var.app_source_repo + repo_name = var.csr_app_source_repo } - substitutions = merge( - { - _GAR_REPOSITORY = local.gar_name - _DEFAULT_REGION = var.primary_location - _CACHE_BUCKET_NAME = google_storage_bucket.cache_bucket.name - _ATTESTOR_NAME = module.attestors[var.attestor_names_prefix[0]].attestor - _CLOUDBUILD_PRIVATE_POOL = var.cloudbuild_private_pool - _CLOUDDEPLOY_PIPELINE_NAME = var.clouddeploy_pipeline_name - }, - var.additional_substitutions - ) + + substitutions = local.common_substitutions service_account = google_service_account.build_sa.id filename = var.app_build_trigger_yaml - depends_on = [google_sourcerepo_repository.repos] + depends_on = [google_sourcerepo_repository.csr_ci_repository] } -# Build the Cloud Build builder image -module "gcloud" { - source = "terraform-google-modules/gcloud/google" - count = var.skip_provisioners ? 0 : 1 - version = "~> 3.1.0" - platform = "linux" - create_cmd_entrypoint = "${path.module}/scripts/cloud-build-submit.sh" - create_cmd_body = "${var.runner_build_folder} ${var.project_id} ${var.build_image_config_yaml} ${var.primary_location} ${local.gar_name} ${google_storage_bucket.cache_bucket.url}/source" - use_tf_google_credentials_env_var = var.use_tf_google_credentials_env_var +resource "google_cloudbuild_trigger" "app_build_trigger" { + count = local.use_csr ? 0 : 1 + project = var.project_id + name = "${local.second_gen_repo_name}-trigger" + location = var.primary_location + repository_event_config { + repository = local.second_gen_repo_id + push { + branch = var.trigger_branch_name # Assumes the same branch for all + } + } + + substitutions = local.common_substitutions + service_account = google_service_account.build_sa.id + filename = var.app_build_trigger_yaml + depends_on = [module.cloudbuild_repositories] } + # Cloud Build Service Account permissions resource "google_project_iam_member" "build_sa_project_iam" { for_each = toset(var.cloudbuild_service_account_roles) @@ -94,20 +116,27 @@ resource "google_project_iam_member" "build_sa_project_iam" { } resource "google_artifact_registry_repository" "image_repo" { - provider = google-beta project = var.project_id location = var.primary_location repository_id = format("%s-%s", var.project_id, var.gar_repo_name_suffix) description = "Docker repository for application images" format = "DOCKER" labels = var.labels + vulnerability_scanning_config { + enablement_config = "INHERITED" + } } resource "google_artifact_registry_repository_iam_member" "terraform-image-iam" { - provider = google-beta project = var.project_id location = google_artifact_registry_repository.image_repo.location repository = google_artifact_registry_repository.image_repo.name role = "roles/artifactregistry.admin" member = "serviceAccount:${google_service_account.build_sa.email}" } + +resource "google_project_service" "containerscanning_api" { + project = var.project_id + + service = "containerscanning.googleapis.com" +} diff --git a/modules/secure-ci/outputs.tf b/modules/secure-ci/outputs.tf index 039bc861..7b2ae5cc 100644 --- a/modules/secure-ci/outputs.tf +++ b/modules/secure-ci/outputs.tf @@ -19,19 +19,16 @@ output "cache_bucket_name" { value = google_storage_bucket.cache_bucket.name } -output "build_trigger_name" { - description = "The name of the cloud build trigger for the app source repo." - value = google_cloudbuild_trigger.app_build_trigger.name -} - output "binauth_attestor_names" { description = "Names of Attestors" - value = [for attestor_name in var.attestor_names_prefix : module.attestors[attestor_name].attestor] + value = keys(local.attestors) } output "binauth_attestor_ids" { description = "IDs of Attestors" - value = { for attestor_name in var.attestor_names_prefix : attestor_name => "projects/${var.project_id}/attestors/${module.attestors[attestor_name].attestor}" } + # For each entry in our locals map, create a map entry + # with the key being the short name and the value being the full ID. + value = { for key, attestor in local.attestors : key => attestor.id } } output "binauth_attestor_project_id" { @@ -44,14 +41,14 @@ output "app_artifact_repo" { value = google_artifact_registry_repository.image_repo.name } -output "source_repo_names" { +output "source_repo_name" { description = "Name of the created CSR repos" - value = [for repo in google_sourcerepo_repository.repos : repo.name] + value = local.use_csr ? google_sourcerepo_repository.csr_ci_repository[0].name : null } -output "source_repo_urls" { +output "source_repo_url" { description = "URLS of the created CSR repos" - value = { for repo in google_sourcerepo_repository.repos : repo.name => repo.url } + value = local.use_csr ? google_sourcerepo_repository.csr_ci_repository[0].url : null } output "build_sa_email" { diff --git a/modules/secure-ci/scripts/cloud-build-submit.sh b/modules/secure-ci/scripts/cloud-build-submit.sh deleted file mode 100755 index e325a305..00000000 --- a/modules/secure-ci/scripts/cloud-build-submit.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# 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. - -#!/bin/sh - -GCLOUD_LOCATION=$(command -v gcloud) -echo "Using gcloud from $GCLOUD_LOCATION" - -gcloud --version -echo "running gcloud builds submit $1 --project $2 --config=$1/$3 --substitutions=_DEFAULT_REGION=$4,_GAR_REPOSITORY=$5 --gcs-source-staging-dir=$6" - -gcloud builds submit "$1" --project "$2" --config="$1"/"$3" --substitutions=_DEFAULT_REGION="$4",_GAR_REPOSITORY="$5" --gcs-source-staging-dir="$6" diff --git a/modules/secure-ci/variables.tf b/modules/secure-ci/variables.tf index 94357b91..9fd0d35a 100644 --- a/modules/secure-ci/variables.tf +++ b/modules/secure-ci/variables.tf @@ -45,18 +45,12 @@ variable "build_image_config_yaml" { description = "Name of image builder yaml file" } -variable "app_source_repo" { +variable "csr_app_source_repo" { type = string description = "Name of repo that contains app source code along with cloudbuild yaml" default = "app-source" } -variable "cloudbuild_cd_repo" { - type = string - description = "Name of repo that stores the Cloud Build CD phase configs - for post-deployment checks" - default = "cloudbuild-cd-config" -} - variable "cache_bucket_name" { type = string description = "Name of cloudbuild artifact and cache GCS bucket" @@ -65,7 +59,7 @@ variable "cache_bucket_name" { variable "gar_repo_name_suffix" { type = string - description = "Docker artifact regitery repo to store app build images" + description = "Docker artifact registry repo to store app build images" default = "app-image-repo" } @@ -129,3 +123,68 @@ variable "labels" { type = map(string) default = {} } + +variable "bucket_kms_key" { + description = "KMS Key id to be used to encrypt bucket." + type = string + default = null +} + +variable "repository_type" { + description = "The type of the repository. Must be one of 'GITHUB', 'GITLAB', or 'CSR'." + type = string + validation { + condition = ( + var.repository_type != "GITHUB" || + (var.github_auth != null && var.gitlab_auth == null) + ) + error_message = "When repository_type is 'GITHUB', the 'github_auth' variable must be set, and 'gitlab_auth' must not be set." + } + validation { + condition = ( + var.repository_type != "GITLAB" || + (var.gitlab_auth != null && var.github_auth == null) + ) + error_message = "When repository_type is 'GITLAB', the 'gitlab_auth' variable must be set, and 'github_auth' must not be set." + } + validation { + condition = ( + var.repository_type != "CSR" || + (var.github_auth == null && var.gitlab_auth == null) + ) + error_message = "When repository_type is 'CSR', neither 'github_auth' nor 'gitlab_auth' should be set." + } +} + +variable "ci_repository" { + type = object({ + repository_name = string + repository_url = string + }) + description = "The CI repository to configure. The key is a short name for the service." + default = null +} + +variable "github_auth" { + type = object({ + secret_id = string + app_id_secret_id = string + secret_project_id = string + }) + description = "Authentication configuration for GitHub. Required only if repo_type is 'GITHUBv2'." + default = null +} + +variable "gitlab_auth" { + type = object({ + read_authorizer_credential_secret_id = string + authorizer_credential_secret_id = string + webhook_secret_id = string + enterprise_host_uri = optional(string) + enterprise_service_directory = optional(string) + enterprise_ca_certificate = optional(string) + secret_project_id = string + }) + description = "Authentication configuration for GitLab. Required only if repo_type is 'GITLABv2'." + default = null +} diff --git a/modules/secure-ci/versions.tf b/modules/secure-ci/versions.tf index 1eaf6caf..d32c46c6 100644 --- a/modules/secure-ci/versions.tf +++ b/modules/secure-ci/versions.tf @@ -15,26 +15,18 @@ */ terraform { - required_version = ">= 0.13.0" required_providers { google = { source = "hashicorp/google" - version = ">= 3.45" - } - google-beta = { - source = "hashicorp/google-beta" - version = ">= 3.45" + version = "> 4, < 8" } random = { source = "hashicorp/random" - version = ">= 3.3.2" + version = ">= 3.7.2" + } + google-beta = { + source = "hashicorp/google-beta" + version = "> 4, < 8" } - } - - provider_meta "google" { - module_name = "blueprints/terraform/terraform-google-secure-cicd:secure-ci/v1.2.1" - } - provider_meta "google-beta" { - module_name = "blueprints/terraform/terraform-google-secure-cicd:secure-ci/v1.2.1" } } diff --git a/modules/workerpool-gke-ha-vpn/versions.tf b/modules/workerpool-gke-ha-vpn/versions.tf index 66fdb523..6cc55791 100644 --- a/modules/workerpool-gke-ha-vpn/versions.tf +++ b/modules/workerpool-gke-ha-vpn/versions.tf @@ -19,18 +19,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 3.45" - } - google-beta = { - source = "hashicorp/google-beta" - version = ">= 3.45" + version = "> 4, < 8" } } provider_meta "google" { module_name = "blueprints/terraform/terraform-google-secure-cicd:workerpool-gke-ha-vpn/v1.2.1" } - provider_meta "google-beta" { - module_name = "blueprints/terraform/terraform-google-secure-cicd:workerpool-gke-ha-vpn/v1.2.1" - } } diff --git a/test/fixtures/app_cicd/main.tf b/test/fixtures/app_cicd/main.tf deleted file mode 100644 index 7e98663c..00000000 --- a/test/fixtures/app_cicd/main.tf +++ /dev/null @@ -1,68 +0,0 @@ -/** - * 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. - */ - -module "example" { - source = "../../../examples/app_cicd" - - project_id = var.project_id - primary_location = var.primary_location - deploy_branch_clusters = { - "01-dev" = { - cluster = "dev-cluster", - anthos_membership = "" - project_id = var.gke_project_ids["dev"], - location = var.primary_location, - required_attestations = ["projects/${var.project_id}/attestors/build-attestor"] - env_attestation = "projects/${var.project_id}/attestors/security-attestor" - next_env = "qa" - target_type = "gke" - }, - "02-qa" = { - cluster = "qa-cluster", - anthos_membership = "" - project_id = var.gke_project_ids["qa"], - location = var.primary_location, - required_attestations = ["projects/${var.project_id}/attestors/security-attestor", "projects/${var.project_id}/attestors/build-attestor"] - env_attestation = "projects/${var.project_id}/attestors/quality-attestor" - next_env = "prod" - target_type = "gke" - }, - "03-prod" = { - cluster = "prod-cluster", - anthos_membership = "" - project_id = var.gke_project_ids["prod"], - location = var.primary_location, - required_attestations = ["projects/${var.project_id}/attestors/quality-attestor", "projects/${var.project_id}/attestors/security-attestor", "projects/${var.project_id}/attestors/build-attestor"] - env_attestation = "" - next_env = "" - target_type = "gke" - }, - } -} - -resource "google_project_iam_member" "cluster_service_account-gcr" { - for_each = var.gke_service_accounts - project = var.project_id - role = "roles/storage.objectViewer" - member = "serviceAccount:${each.value}" -} - -resource "google_project_iam_member" "cluster_service_account-artifact-registry" { - for_each = var.gke_service_accounts - project = var.project_id - role = "roles/artifactregistry.reader" - member = "serviceAccount:${each.value}" -} diff --git a/test/fixtures/app_cicd/outputs.tf b/test/fixtures/app_cicd/outputs.tf deleted file mode 100644 index 4dd67449..00000000 --- a/test/fixtures/app_cicd/outputs.tf +++ /dev/null @@ -1,35 +0,0 @@ -/** - * 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. - */ - -output "project_id" { - description = "The ID of the project in which resources are provisioned." - value = var.project_id -} - -output "cache_bucket_name" { - description = "The name of the storage bucket for cloud build." - value = module.example.cache_bucket_name -} - -output "build_trigger_name" { - description = "The name of the cloud build trigger for the bank of anthos repo." - value = module.example.build_trigger_name -} - -output "gke_project_ids" { - description = "The name of the cloud build trigger for the bank of anthos repo." - value = values(var.gke_project_ids) -} diff --git a/test/fixtures/app_cicd/variables.tf b/test/fixtures/app_cicd/variables.tf deleted file mode 100644 index b462f419..00000000 --- a/test/fixtures/app_cicd/variables.tf +++ /dev/null @@ -1,49 +0,0 @@ -/** - * 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. - */ - -variable "project_id" { - description = "The ID of the CI/CD project to provision resources." - type = string -} - -variable "org_id" { - description = "The numeric organization id" -} - -variable "folder_id" { - description = "The folder to deploy in" -} - -variable "primary_location" { - type = string - description = "Region used for key-ring" - default = "us-central1" -} - -variable "billing_account" { - type = string - description = "The billing account id associated with the project, e.g. XXXXXX-YYYYYY-ZZZZZZ" -} - -variable "gke_project_ids" { - type = map(string) - description = "map of env name to GKE project ID" -} - -variable "gke_service_accounts" { - type = map(string) - description = "map of env name to GKE service account" -} diff --git a/test/fixtures/cloudbuild_private_pool/main.tf b/test/fixtures/cloudbuild_private_pool/main.tf deleted file mode 100644 index 0fcb9309..00000000 --- a/test/fixtures/cloudbuild_private_pool/main.tf +++ /dev/null @@ -1,76 +0,0 @@ -/** - * 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. - */ - -locals { - deploy_branch_clusters = { - "01-dev" = { - cluster = "dev-private-cluster", - network = var.gke_private_vpc_names["dev"] - project_id = var.gke_project_ids["dev"], - location = var.primary_location, - required_attestations = ["projects/${var.project_id}/attestors/build-pc-attestor"] - env_attestation = "projects/${var.project_id}/attestors/security-pc-attestor" - next_env = "02-qa" - anthos_membership = "" - target_type = "gke" - }, - "02-qa" = { - cluster = "qa-private-cluster", - network = var.gke_private_vpc_names["qa"] - project_id = var.gke_project_ids["qa"], - location = var.primary_location, - required_attestations = ["projects/${var.project_id}/attestors/security-pc-attestor", "projects/${var.project_id}/attestors/build-pc-attestor"] - env_attestation = "projects/${var.project_id}/attestors/quality-pc-attestor" - next_env = "03-prod" - anthos_membership = "" - target_type = "gke" - }, - "03-prod" = { - cluster = "prod-private-cluster", - network = var.gke_private_vpc_names["prod"] - project_id = var.gke_project_ids["prod"], - location = var.primary_location, - required_attestations = ["projects/${var.project_id}/attestors/quality-pc-attestor", "projects/${var.project_id}/attestors/security-pc-attestor", "projects/${var.project_id}/attestors/build-pc-attestor"] - env_attestation = "" - next_env = "" - anthos_membership = "" - target_type = "gke" - }, - } -} - -data "google_container_cluster" "cluster" { - for_each = local.deploy_branch_clusters - project = each.value.project_id - location = each.value.location - name = each.value.cluster -} - -module "example" { - source = "../../../examples/cloudbuild_private_pool" - - project_id = var.project_id - primary_location = var.primary_location - - gke_networks = distinct([ - for env in local.deploy_branch_clusters : { - network = env.network - location = env.location - project_id = env.project_id - control_plane_cidrs = { for cluster in data.google_container_cluster.cluster : cluster.private_cluster_config[0].master_ipv4_cidr_block => "GKE control plane" if cluster.network == "projects/${env.project_id}/global/networks/${env.network}" } - } - ]) -} diff --git a/test/fixtures/cloudbuild_private_pool/variables.tf b/test/fixtures/cloudbuild_private_pool/variables.tf deleted file mode 100644 index 78bc11e5..00000000 --- a/test/fixtures/cloudbuild_private_pool/variables.tf +++ /dev/null @@ -1,41 +0,0 @@ -/** - * 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. - */ - -variable "project_id" { - description = "The ID of the CI/CD project to provision resources." - type = string -} - -variable "primary_location" { - type = string - description = "Region used for key-ring" - default = "us-central1" -} - -variable "gke_project_ids" { - type = map(string) - description = "map of env name to GKE project ID" -} - -variable "gke_private_vpc_names" { - type = map(string) - description = "map of env name to GKE network name" -} - -variable "gke_private_service_accounts" { - type = map(string) - description = "map of env name to GKE service account" -} diff --git a/test/fixtures/private_cluster_cicd/main.tf b/test/fixtures/private_cluster_cicd/main.tf deleted file mode 100644 index ee3c22ed..00000000 --- a/test/fixtures/private_cluster_cicd/main.tf +++ /dev/null @@ -1,89 +0,0 @@ -/** - * 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. - */ - -locals { - deploy_branch_clusters = { - "01-dev" = { - cluster = "dev-private-cluster", - network = var.gke_private_vpc_names["dev"] - project_id = var.gke_project_ids["dev"], - location = var.primary_location, - required_attestations = ["projects/${var.project_id}/attestors/build-pc-attestor"] - env_attestation = "projects/${var.project_id}/attestors/security-pc-attestor" - next_env = "02-qa" - anthos_membership = "" - target_type = "gke" - }, - "02-qa" = { - cluster = "qa-private-cluster", - network = var.gke_private_vpc_names["qa"] - project_id = var.gke_project_ids["qa"], - location = var.primary_location, - required_attestations = ["projects/${var.project_id}/attestors/security-pc-attestor", "projects/${var.project_id}/attestors/build-pc-attestor"] - env_attestation = "projects/${var.project_id}/attestors/quality-pc-attestor" - next_env = "03-prod" - anthos_membership = "" - target_type = "gke" - }, - "03-prod" = { - cluster = "prod-private-cluster", - network = var.gke_private_vpc_names["prod"] - project_id = var.gke_project_ids["prod"], - location = var.primary_location, - required_attestations = ["projects/${var.project_id}/attestors/quality-pc-attestor", "projects/${var.project_id}/attestors/security-pc-attestor", "projects/${var.project_id}/attestors/build-pc-attestor"] - env_attestation = "" - next_env = "" - anthos_membership = "" - target_type = "gke" - }, - } -} - -data "google_container_cluster" "cluster" { - for_each = local.deploy_branch_clusters - project = each.value.project_id - location = each.value.location - name = each.value.cluster -} - -module "example" { - source = "../../../examples/private_cluster_cicd" - - project_id = var.project_id - deploy_branch_clusters = local.deploy_branch_clusters - gke_networks = distinct([ - for env in local.deploy_branch_clusters : { - network = env.network - location = env.location - project_id = env.project_id - control_plane_cidrs = { for cluster in data.google_container_cluster.cluster : cluster.private_cluster_config[0].master_ipv4_cidr_block => "GKE control plane" if cluster.network == "projects/${env.project_id}/global/networks/${env.network}" } - } - ]) -} - -resource "google_project_iam_member" "cluster_service_account-gcr" { - for_each = var.gke_private_service_accounts - project = var.project_id - role = "roles/storage.objectViewer" - member = "serviceAccount:${each.value}" -} - -resource "google_project_iam_member" "cluster_service_account-artifact-registry" { - for_each = var.gke_private_service_accounts - project = var.project_id - role = "roles/artifactregistry.reader" - member = "serviceAccount:${each.value}" -} diff --git a/test/fixtures/private_cluster_cicd/outputs.tf b/test/fixtures/private_cluster_cicd/outputs.tf deleted file mode 100644 index 4dd67449..00000000 --- a/test/fixtures/private_cluster_cicd/outputs.tf +++ /dev/null @@ -1,35 +0,0 @@ -/** - * 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. - */ - -output "project_id" { - description = "The ID of the project in which resources are provisioned." - value = var.project_id -} - -output "cache_bucket_name" { - description = "The name of the storage bucket for cloud build." - value = module.example.cache_bucket_name -} - -output "build_trigger_name" { - description = "The name of the cloud build trigger for the bank of anthos repo." - value = module.example.build_trigger_name -} - -output "gke_project_ids" { - description = "The name of the cloud build trigger for the bank of anthos repo." - value = values(var.gke_project_ids) -} diff --git a/test/fixtures/private_cluster_cicd/variables.tf b/test/fixtures/private_cluster_cicd/variables.tf deleted file mode 100644 index 6b0ecc08..00000000 --- a/test/fixtures/private_cluster_cicd/variables.tf +++ /dev/null @@ -1,59 +0,0 @@ -/** - * 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. - */ - -variable "project_id" { - description = "The ID of the CI/CD project to provision resources." - type = string -} - -variable "org_id" { - description = "The numeric organization id" -} - -variable "folder_id" { - description = "The folder to deploy in" -} - -variable "primary_location" { - type = string - description = "Region used for key-ring" - default = "us-central1" -} - -variable "billing_account" { - type = string - description = "The billing account id associated with the project, e.g. XXXXXX-YYYYYY-ZZZZZZ" -} - -variable "gke_private_cluster_names" { - type = map(string) - description = "map of env name to GKE cluster name" -} - -variable "gke_project_ids" { - type = map(string) - description = "map of env name to GKE project ID" -} - -variable "gke_private_vpc_names" { - type = map(string) - description = "map of env name to GKE network name" -} - -variable "gke_private_service_accounts" { - type = map(string) - description = "map of env name to GKE service account" -} diff --git a/test/fixtures/private_cluster_cicd/versions.tf b/test/fixtures/private_cluster_cicd/versions.tf deleted file mode 100644 index c001c4e3..00000000 --- a/test/fixtures/private_cluster_cicd/versions.tf +++ /dev/null @@ -1,19 +0,0 @@ -/** - * 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. - */ - -terraform { - required_version = ">= 0.13" -} diff --git a/test/integration/app_cicd/app_cicd_test.go b/test/integration/app_cicd/app_cicd_test.go deleted file mode 100644 index 2be736ec..00000000 --- a/test/integration/app_cicd/app_cicd_test.go +++ /dev/null @@ -1,122 +0,0 @@ -/** - * 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. - */ - -// define test package name -package app_cicd - -import ( - "fmt" - "testing" - - // import the blueprints test framework modules for testing and assertions - "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/gcloud" - "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" - "github.com/gruntwork-io/terratest/modules/terraform" - "github.com/stretchr/testify/assert" -) - -// name the function as Test* -func TestAppCICDExample(t *testing.T) { - // define constants for all required assertions in the test case - - const sourceTriggerName = "app-source-trigger" - const garRepoNameSuffix = "app-image-repo" - const primaryLocation = "us-central1" - const appSourceRepoName = "app-source" - const cloudbuildCDRepoName = "cloudbuild-cd-config" - - // initialize Terraform test from the Blueprints test framework - appCICDT := tft.NewTFBlueprintTest(t) - - // define and write a custom verifier for this test case call the default verify for confirming no additional changes - appCICDT.DefineVerify(func(assert *assert.Assertions) { - // perform default verification ensuring Terraform reports no additional changes on an applied blueprint - appCICDT.DefaultVerify(assert) - - // invoke the gcloud module in the Blueprints test framework to run a gcloud command that will output resource properties in a JSON format - // the tft struct can be used to pull output variables of the TF module being invoked by this test and use the op object (a gjson struct) - // to parse through the JSON results and assert the values of the resource against the constants defined above - - projectID := appCICDT.GetStringOutput("project_id") - gkeProjectIDs := terraform.OutputList(t, appCICDT.GetTFOptions(), "gke_project_ids") - - /////// SECURE-CI /////// - // Cloud Build Trigger - App Source - gcbCI := gcloud.Run(t, fmt.Sprintf("builds triggers describe %s --project %s --region %s", sourceTriggerName, projectID, primaryLocation)) - - assert.Equal(sourceTriggerName, gcbCI.Get("name").String(), "Cloud Build Trigger name is valid") - assert.Contains(gcbCI.Get("substitutions._DEFAULT_REGION").String(), primaryLocation, "Default Region trigger substitution is valid") - assert.Equal(appSourceRepoName, gcbCI.Get("triggerTemplate.repoName").String(), "Attached CSR repo is valid") - - // Artifact Registry repository - gar := gcloud.Run(t, fmt.Sprintf("artifacts repositories describe %s-%s --project %s --location %s", projectID, garRepoNameSuffix, projectID, primaryLocation)) - garFullname := fmt.Sprintf("projects/%s/locations/%s/repositories/%s-%s", projectID, primaryLocation, projectID, garRepoNameSuffix) - assert.Equal(garFullname, gar.Get("name").String(), "GAR Repo is valid") - - // BinAuthz Attestors - attestors := [3]string{"build-attestor", "quality-attestor", "security-attestor"} - - for _, attestor := range attestors { - binAuthZAttestor := gcloud.Run(t, fmt.Sprintf("container binauthz attestors describe %s --project %s", attestor, projectID)) - attestorFullName := fmt.Sprintf("projects/%s/attestors/%s", projectID, attestor) - assert.Equal(attestorFullName, binAuthZAttestor.Get("name").String(), fmt.Sprintf("%s is valid", attestor)) - } - - // CSR - repos := [2]string{appSourceRepoName, cloudbuildCDRepoName} - - for _, repo := range repos { - csr := gcloud.Run(t, fmt.Sprintf("source repos describe %s --project %s", repo, projectID)) - csrFullName := fmt.Sprintf("projects/%s/repos/%s", projectID, repo) - csrURL := fmt.Sprintf("https://source.developers.google.com/p/%s/r/%s", projectID, repo) - assert.Equal(csrFullName, csr.Get("name").String(), fmt.Sprintf("CSR %s repo name is valid", repo)) - assert.Equal(csrURL, csr.Get("url").String(), fmt.Sprintf("CSR %s URL is valid", repo)) - } - - /////// SECURE-CD /////// - // Deploy Triggers - cdTriggers := [2]string{"deploy-trigger-dev-cluster", "deploy-trigger-qa-cluster"} - - for _, cdTrigger := range cdTriggers { - gcbCD := gcloud.Run(t, fmt.Sprintf("builds triggers describe %s --project %s --region %s", cdTrigger, projectID, primaryLocation)) - assert.Contains(gcbCD.Get("name").String(), cdTrigger, "Trigger name is valid") - assert.Contains(gcbCD.Get("pubsubConfig.topic").String(), "clouddeploy-operations", "pubsub topic is valid") - assert.Contains(gcbCD.Get("substitutions._CLUSTER_PROJECT").String(), "secure-cicd-gke-", "_CLUSTER_PROJECT trigger substitution is valid") - } - - // BinAuthz Policy - for _, gkeProjectID := range gkeProjectIDs { - binAuthZPolicy := gcloud.Run(t, fmt.Sprintf("container binauthz policy export --project %s", gkeProjectID)) - cluster := gcloud.Run(t, fmt.Sprintf("container clusters list --project %s", gkeProjectID)) - clusterName := cluster.Get("0.name").String() - assert.Contains(binAuthZPolicy.Get("defaultAdmissionRule.enforcementMode").String(), "ENFORCED_BLOCK_AND_AUDIT_LOG") - assert.Contains(binAuthZPolicy.Get("defaultAdmissionRule.evaluationMode").String(), "ALWAYS_DENY") - assert.Contains(binAuthZPolicy.Get(fmt.Sprintf("clusterAdmissionRules.us-central1\\.%s.evaluationMode", clusterName)).String(), "REQUIRE_ATTESTATION") - assert.Contains(binAuthZPolicy.Get(fmt.Sprintf("clusterAdmissionRules.us-central1\\.%s.requireAttestationsBy", clusterName)).String(), "build-attestor") - - switch clusterName { - case "qa-cluster": - assert.Contains(binAuthZPolicy.Get(fmt.Sprintf("clusterAdmissionRules.us-central1\\.%s.requireAttestationsBy", clusterName)).String(), "security-attestor") - case "prod-cluster": - assert.Contains(binAuthZPolicy.Get(fmt.Sprintf("clusterAdmissionRules.us-central1\\.%s.requireAttestationsBy", clusterName)).String(), "security-attestor") - assert.Contains(binAuthZPolicy.Get(fmt.Sprintf("clusterAdmissionRules.us-central1\\.%s.requireAttestationsBy", clusterName)).String(), "quality-attestor") - } - } - - }) - // call the test function to execute the integration test - appCICDT.Test() -} diff --git a/test/integration/cloudbuild_private_pool/cloudbuild_private_pool_test.go b/test/integration/cloudbuild_private_pool/cloudbuild_private_pool_test.go deleted file mode 100644 index 075ca4bd..00000000 --- a/test/integration/cloudbuild_private_pool/cloudbuild_private_pool_test.go +++ /dev/null @@ -1,70 +0,0 @@ -/** - * 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. - */ - -// define test package name -package cloudbuild_private_pool - -import ( - "fmt" - "testing" - - // import the blueprints test framework modules for testing and assertions - "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/gcloud" - "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" - "github.com/stretchr/testify/assert" -) - -// name the function as Test* -func TestCloudBuildPrivatePoolExample(t *testing.T) { - // define constants for all required assertions in the test case - - // initialize Terraform test from the Blueprints test framework - cloudBuildPrivatePoolT := tft.NewTFBlueprintTest(t) - - // define and write a custom verifier for this test case call the default verify for confirming no additional changes - cloudBuildPrivatePoolT.DefineVerify(func(assert *assert.Assertions) { - // perform default verification ensuring Terraform reports no additional changes on an applied blueprint - cloudBuildPrivatePoolT.DefaultVerify(assert) - - // invoke the gcloud module in the Blueprints test framework to run a gcloud command that will output resource properties in a JSON format - // the tft struct can be used to pull output variables of the TF module being invoked by this test and use the op object (a gjson struct) - // to parse through the JSON results and assert the values of the resource against the constants defined above - - projectID := cloudBuildPrivatePoolT.GetStringOutput("project_id") - - // Worker Pool - workerPoolAddress := gcloud.Run(t, fmt.Sprintf("compute addresses describe gke-private-pool-worker-range --global --project %s", projectID)) - assert.Equal("10.37.0.0", workerPoolAddress.Get("address").String(), "Worker pool address range is 10.37.0.0") - assert.Equal("INTERNAL", workerPoolAddress.Get("addressType").String(), "Worker pool address range is type INTERNAL") - assert.Equal("VPC_PEERING", workerPoolAddress.Get("purpose").String(), "Worker pool address range is for VPC_PEERING") - - privatePoolVPC := gcloud.Run(t, fmt.Sprintf("compute networks describe workerpool-example-vpc --project %s", projectID)) - assert.Contains(privatePoolVPC.Get("peerings.0.network").String(), "servicenetworking") - - privatePool := gcloud.Run(t, fmt.Sprintf("builds worker-pools describe cloudbuild-private-worker-pool --region=us-central1 --project %s", projectID)) - assert.Equal("RUNNING", privatePool.Get("state").String(), "Worker pool is RUNNING") - - // VPN Tunnels - vpnTunnels := [6]string{"cloudbuild-to-gke-private-vpc-dev-remote-0", "cloudbuild-to-gke-private-vpc-dev-remote-1", "cloudbuild-to-gke-private-vpc-prod-remote-0", "cloudbuild-to-gke-private-vpc-prod-remote-1", "cloudbuild-to-gke-private-vpc-qa-remote-0", "cloudbuild-to-gke-private-vpc-qa-remote-1"} - for _, tunnel := range vpnTunnels { - tunnelStatus := gcloud.Run(t, fmt.Sprintf("compute vpn-tunnels describe %s --region=us-central1 --project %s", tunnel, projectID)) - assert.Equal("Tunnel is up and running.", tunnelStatus.Get("detailedStatus").String(), fmt.Sprintf("%s is created and running", tunnel)) - } - - }) - // call the test function to execute the integration test - cloudBuildPrivatePoolT.Test() -} diff --git a/test/integration/go.mod b/test/integration/go.mod index c37327ce..8493c96e 100644 --- a/test/integration/go.mod +++ b/test/integration/go.mod @@ -3,11 +3,13 @@ module github.com/terraform-google-modules/terraform-google-secure-cicd/test/int go 1.20 require ( + cloud.google.com/go/secretmanager v1.10.0 github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test v0.5.1 - github.com/gruntwork-io/terratest v0.41.23 github.com/stretchr/testify v1.8.2 ) +require github.com/gruntwork-io/terratest v0.41.23 + require ( cloud.google.com/go v0.110.0 // indirect cloud.google.com/go/compute v1.19.1 // indirect @@ -48,9 +50,8 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.14.2-0.20210217184823-a52172cd2f64 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/otiai10/copy v1.11.0 github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/gjson v1.14.4 github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect diff --git a/test/integration/go.sum b/test/integration/go.sum index b849132b..887ed2b2 100644 --- a/test/integration/go.sum +++ b/test/integration/go.sum @@ -155,6 +155,8 @@ cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92 cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/secretmanager v1.10.0 h1:pu03bha7ukxF8otyPKTFdDz+rr9sE3YauS5PliDXK60= +cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= @@ -413,9 +415,6 @@ github.com/mitchellh/go-testing-interface v1.14.2-0.20210217184823-a52172cd2f64/ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/otiai10/copy v1.11.0 h1:OKBD80J/mLBrwnzXqGtFCzprFSGioo30JcmR4APsNwc= -github.com/otiai10/copy v1.11.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww= -github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/test/integration/private_cluster_cicd/private_cluster_cicd_test.go b/test/integration/private_cluster_cicd/private_cluster_cicd_test.go deleted file mode 100644 index c7fe1fd5..00000000 --- a/test/integration/private_cluster_cicd/private_cluster_cicd_test.go +++ /dev/null @@ -1,76 +0,0 @@ -/** - * 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. - */ - -// define test package name -package private_cluster_cicd - -import ( - "fmt" - "testing" - - // import the blueprints test framework modules for testing and assertions - "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/gcloud" - "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" - "github.com/stretchr/testify/assert" -) - -// name the function as Test* -func TestPrivateClusterCICDExample(t *testing.T) { - // define constants for all required assertions in the test case - - const sourceTriggerName = "app-source-trigger" - const garRepoNameSuffix = "app-image-repo" - const primaryLocation = "us-central1" - const appSourceRepoName = "app-source" - const cloudbuildCDRepoName = "cloudbuild-cd-config" - - // initialize Terraform test from the Blueprints test framework - privateClusterCICDT := tft.NewTFBlueprintTest(t) - - // define and write a custom verifier for this test case call the default verify for confirming no additional changes - privateClusterCICDT.DefineVerify(func(assert *assert.Assertions) { - // perform default verification ensuring Terraform reports no additional changes on an applied blueprint - privateClusterCICDT.DefaultVerify(assert) - - // invoke the gcloud module in the Blueprints test framework to run a gcloud command that will output resource properties in a JSON format - // the tft struct can be used to pull output variables of the TF module being invoked by this test and use the op object (a gjson struct) - // to parse through the JSON results and assert the values of the resource against the constants defined above - - projectID := privateClusterCICDT.GetStringOutput("project_id") - - // Worker Pool - workerPoolAddress := gcloud.Run(t, fmt.Sprintf("compute addresses describe private-cluster-example-worker-range --global --project %s", projectID)) - assert.Equal("10.39.0.0", workerPoolAddress.Get("address").String(), "Worker pool address range is 10.39.0.0") - assert.Equal("INTERNAL", workerPoolAddress.Get("addressType").String(), "Worker pool address range is type INTERNAL") - assert.Equal("VPC_PEERING", workerPoolAddress.Get("purpose").String(), "Worker pool address range is for VPC_PEERING") - - privatePoolVPC := gcloud.Run(t, fmt.Sprintf("compute networks describe gke-private-pool-example-vpc --project %s", projectID)) - assert.Contains(privatePoolVPC.Get("peerings.0.network").String(), "servicenetworking") - - privatePool := gcloud.Run(t, fmt.Sprintf("builds worker-pools describe private-cluster-example-workerpool --region=us-central1 --project %s", projectID)) - assert.Equal("RUNNING", privatePool.Get("state").String(), "Worker pool is RUNNING") - - // VPN Tunnels - vpnTunnels := [6]string{"pc-cloudbuild-to-gke-private-vpc-dev-remote-0", "pc-cloudbuild-to-gke-private-vpc-dev-remote-1", "pc-cloudbuild-to-gke-private-vpc-prod-remote-0", "pc-cloudbuild-to-gke-private-vpc-prod-remote-1", "pc-cloudbuild-to-gke-private-vpc-qa-remote-0", "pc-cloudbuild-to-gke-private-vpc-qa-remote-1"} - for _, tunnel := range vpnTunnels { - tunnelStatus := gcloud.Run(t, fmt.Sprintf("compute vpn-tunnels describe %s --region=us-central1 --project %s", tunnel, projectID)) - assert.Equal("Tunnel is up and running.", tunnelStatus.Get("detailedStatus").String(), fmt.Sprintf("%s is created and running", tunnel)) - } - - }) - // call the test function to execute the integration test - privateClusterCICDT.Test() -} diff --git a/test/integration/standalone_single_project/standalone_single_project_test.go b/test/integration/standalone_single_project/standalone_single_project_test.go index 472fee48..392de1e5 100644 --- a/test/integration/standalone_single_project/standalone_single_project_test.go +++ b/test/integration/standalone_single_project/standalone_single_project_test.go @@ -1,5 +1,5 @@ /** - * Copyright 2021 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,134 +14,160 @@ * limitations under the License. */ -// define test package name package standalone_single_project import ( "fmt" + "strings" "testing" - "time" - // import the blueprints test framework modules for testing and assertions + // "time" // Uncomment if using RetryableErrors or Polling + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/gcloud" - "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/git" "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" - "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/utils" - "github.com/stretchr/testify/assert" - cp "github.com/otiai10/copy" + // "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/utils" // Uncomment if using Polling + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" ) -// name the function as Test* -func TestStandaloneSingleProjectExample(t *testing.T) { - - // initialize Terraform test from the Blueprints test framework - setupOutput := tft.NewTFBlueprintTest(t) - projectID := setupOutput.GetTFSetupStringOutput("project_id_standalone") +// Helper function to check if a specific member has a specific role in an IAM policy JSON array +func assertHasRole(assert *assert.Assertions, bindings []gjson.Result, role string, expectedMember string, resourceName string) { + found := false + for _, binding := range bindings { + if binding.Get("role").String() == role { + for _, member := range binding.Get("members").Array() { + if member.String() == expectedMember { + found = true + break + } + } + } + } + assert.True(found, fmt.Sprintf("Role '%s' should be granted to '%s' on %s", role, expectedMember, resourceName)) +} - // wire setup output project_id_standalone to example var.project_id - standaloneSingleProjT := tft.NewTFBlueprintTest(t, tft.WithVars(map[string]interface{}{"project_id": projectID})) +func TestStandaloneSingleProjectExample(t *testing.T) { - // define and write a custom verifier for this test case call the default verify for confirming no additional changes + // 1. Initialize the Setup Environment + setupOutput := tft.NewTFBlueprintTest(t, tft.WithTFDir("../../setup")) + projectID := setupOutput.GetStringOutput("project_id") + // If you added outputs to the setup module (like network details), grab them here. + + // 2. Define Variables for the Main Module + vars := map[string]interface{}{ + "project_id": projectID, + "region": "us-central1", // Or pull from setup if dynamic + // Add other necessary variables that your standalone_single_project requires + } + + // 3. Initialize the Main Blueprint Test + standaloneSingleProjT := tft.NewTFBlueprintTest(t, + tft.WithVars(vars), + tft.WithTFDir("../../../examples/standalone_single_project"), + // tft.WithRetryableTerraformErrors(testutils.RetryableTransientErrors, 3, 2*time.Minute), // Highly recommended if you copy the testutils package + ) + + // 4. Define the Custom Verifier standaloneSingleProjT.DefineVerify(func(assert *assert.Assertions) { - // perform default verification ensuring Terraform reports no additional changes on an applied blueprint - standaloneSingleProjT.DefaultVerify(assert) - - garRepo := standaloneSingleProjT.GetStringOutput("gar_repo") - appRepo := fmt.Sprintf("https://source.developers.google.com/p/%s/r/my-app-source", projectID) - cdRepo := standaloneSingleProjT.GetStringOutput("cloudbuild_cd_repo_name") - region := "us-central1" - pipelineName := "my-app-pipeline" - prodTarget := "my-app-cluster-prod-membership-target" - - // Create Builder image - gcloud.RunCmd(t, fmt.Sprintf("builds submit ../../../examples/private_cluster_cicd/cloud-build-builder --project %s --region %s --config=../../../examples/private_cluster_cicd/cloud-build-builder/cloudbuild-skaffold-build-image.yaml --substitutions=_DEFAULT_REGION=%s,_GAR_REPOSITORY=%s", projectID, region, region, garRepo)) - - // Configure Cloud Deploy post-deployment scans - tmpDirCD := t.TempDir() - gitCD := git.NewCmdConfig(t, git.WithDir(tmpDirCD)) - gitCDRun := func(args ...string) { - _, err := gitCD.RunCmdE(args...) - if err != nil { - t.Fatal(err) + // Perform default verification ensuring Terraform reports no additional changes + // standaloneSingleProjT.DefaultVerify(assert) + + // Fetch Outputs + region := standaloneSingleProjT.GetStringOutput("region") + garRepoName := standaloneSingleProjT.GetStringOutput("gar_repo") + pipelineFullID := standaloneSingleProjT.GetStringOutput("clouddeploy_pipeline_id") + + // Parse the pipeline name from the full ID string + pipelineParts := strings.Split(pipelineFullID, "/") + pipelineName := pipelineParts[len(pipelineParts)-1] + + buildSAEmail := fmt.Sprintf("serviceAccount:%s", standaloneSingleProjT.GetStringOutput("build_sa_email")) + deploySAEmail := fmt.Sprintf("serviceAccount:%s", standaloneSingleProjT.GetStringOutput("deploy_sa_email")) + + // Handle the comma-separated cluster names + clusterNamesString := standaloneSingleProjT.GetStringOutput("cluster_names") + clusterNames := strings.Split(clusterNamesString, ",") + assert.NotEmpty(clusterNamesString, "cluster_names output should not be empty") + + // ==================================================================== + // A. Artifact Registry Verification + // ==================================================================== + garCommand := fmt.Sprintf("artifacts repositories describe %s --location=%s --project=%s --format=json", garRepoName, region, projectID) + garResult := gcloud.Run(t, garCommand) + + assert.Equal("DOCKER", garResult.Get("format").String(), "Artifact Registry format should be DOCKER") + + // ==================================================================== + // B. Cloud Deploy Pipeline Verification + // ==================================================================== + deployCommand := fmt.Sprintf("deploy delivery-pipelines describe %s --region=%s --project=%s --format=json", pipelineName, region, projectID) + deployResult := gcloud.Run(t, deployCommand) + + assert.False(deployResult.Get("suspended").Bool(), "Cloud Deploy pipeline should be active (not suspended)") + + stages := deployResult.Get("serialPipeline.stages").Array() + assert.NotEmpty(stages, "Cloud Deploy pipeline should have at least one stage configured") + + // ==================================================================== + // C. GKE Clusters Security Posture Verification + // ==================================================================== + for _, clusterName := range clusterNames { + if clusterName == "" { + continue } - } - gcloud.Runf(t, "source repos clone %s %s --project %s", cdRepo, tmpDirCD, projectID) - gitCDRun("config", "user.email", "secure-cicd-robot@example.com") - gitCDRun("config", "user.name", "Secure CICD Robot") - gitCDRun("config", "--global", "init.defaultBranch", "main") - gitCDRun("checkout", "-b", "main") - err := cp.Copy("../../../build/cloudbuild-cd.yaml", fmt.Sprintf("%s/cloudbuild-cd.yaml", tmpDirCD)) - fmt.Println(err) - gitCDRun("add", ".") - gitCD.CommitWithMsg("initial commit", []string{"--allow-empty"}) - gitCDRun("push", "-u", "origin", "main", "-f") - - // Push demo app source code, CI config, and policies - tmpDirApp := t.TempDir() - gitApp := git.NewCmdConfig(t, git.WithDir(tmpDirApp)) - gitAppRun := func(args ...string) { - _, err := gitApp.RunCmdE(args...) - if err != nil { - t.Fatal(err) - } - } + clusterCmd := fmt.Sprintf("container clusters describe %s --region=%s --project=%s --format=json", clusterName, region, projectID) + clusterResult := gcloud.Run(t, clusterCmd) - gitAppRun("clone", "--branch", "v0.5.11", "https://github.com/GoogleCloudPlatform/bank-of-anthos.git", tmpDirApp) - gitAppRun("config", "user.email", "secure-cicd-robot@example.com") - gitAppRun("config", "user.name", "Secure CICD Robot") - gitAppRun("config", "--global", "credential.https://source.developers.google.com.helper", "gcloud.sh") - gitAppRun("config", "--global", "init.defaultBranch", "main") - gitAppRun("config", "--global", "http.postBuffer", "157286400") - gitAppRun("checkout", "-b", "main") - err2 := cp.Copy("../../../build/cloudbuild-ci.yaml", fmt.Sprintf("%s/cloudbuild-ci.yaml", tmpDirApp)) - fmt.Println(err2) - err3 := cp.Copy("../../../examples/private_cluster_cicd/policies", fmt.Sprintf("%s/policies", tmpDirApp)) - fmt.Println(err3) - gitAppRun("remote", "add", "google", appRepo) - gitAppRun("add", ".") - gitApp.CommitWithMsg("initial commit", []string{"--allow-empty"}) - gitAppRun("push", "--all", "google", "-f") - - lastCommit := gitApp.GetLatestCommit() - // filter builds triggered based on pushed commit sha - buildListCmd := fmt.Sprintf("builds list --region=%s --filter substitutions.COMMIT_SHA='%s' --project %s", region, lastCommit, projectID) - // poll build until complete - pollCloudBuild := func(cmd string) func() (bool, error) { - return func() (bool, error) { - build := gcloud.Runf(t, cmd).Array() - if len(build) < 1 { - return true, nil - } - latestWorkflowRunStatus := build[0].Get("status").String() - if latestWorkflowRunStatus == "SUCCESS" { - return false, nil - } - return true, nil - } - } - utils.Poll(t, pollCloudBuild(buildListCmd), 40, 30*time.Second) - - releaseName := fmt.Sprintf("release-%s", lastCommit[0:7]) - fmt.Println(releaseName) - rolloutListCmd := fmt.Sprintf("deploy rollouts list --project=%s --delivery-pipeline=%s --region=%s --release=%s --filter targetId=%s", projectID, pipelineName, region, releaseName, prodTarget) - // Poll CD rollouts until prod rollout is successful - pollCloudDeploy := func(cmd string) func() (bool, error) { - return func() (bool, error) { - rollouts := gcloud.Runf(t, cmd).Array() - if len(rollouts) < 1 { - return true, nil - } - latestRolloutState := rollouts[0].Get("state").String() - if latestRolloutState == "SUCCEEDED" { - return false, nil - } - return true, nil - } + // Cluster Status + assert.Equal("RUNNING", clusterResult.Get("status").String(), fmt.Sprintf("Cluster %s should be RUNNING", clusterName)) + + // Network Security: Ensure private nodes + isPrivate := clusterResult.Get("privateClusterConfig.enablePrivateNodes").Bool() + assert.True(isPrivate, fmt.Sprintf("Cluster %s should have private nodes enabled", clusterName)) + + // Workload Identity Posture + expectedWorkloadPool := fmt.Sprintf("%s.svc.id.goog", projectID) + assert.Equal(expectedWorkloadPool, clusterResult.Get("workloadIdentityConfig.workloadPool").String(), fmt.Sprintf("Cluster %s workloadPool should be configured securely", clusterName)) + + // Binary Authorization Posture + binAuthzMode := clusterResult.Get("binaryAuthorization.evaluationMode").String() + assert.Equal("PROJECT_SINGLETON_POLICY_ENFORCE", binAuthzMode, fmt.Sprintf("Binary Authorization should be enforced on cluster %s", clusterName)) + + // Shielded Nodes Posture + shieldedNodes := clusterResult.Get("shieldedNodes.enableSecureBoot").Bool() + assert.True(shieldedNodes, fmt.Sprintf("Shielded Nodes Secure Boot should be enabled on cluster %s", clusterName)) } - utils.Poll(t, pollCloudDeploy(rolloutListCmd), 30, 60*time.Second) + + // ==================================================================== + // D. IAM & Principle of Least Privilege Verification + // ==================================================================== + + // 1. Fetch Project-level IAM policy + projectIamCmd := fmt.Sprintf("projects get-iam-policy %s --format=json", projectID) + projectIam := gcloud.Run(t, projectIamCmd) + projectBindings := projectIam.Get("bindings").Array() + + // 2. Fetch GAR-level IAM policy + garIamCmd := fmt.Sprintf("artifacts repositories get-iam-policy %s --location=%s --project=%s --format=json", garRepoName, region, projectID) + garIamResult := gcloud.Run(t, garIamCmd) + garBindings := garIamResult.Get("bindings").Array() + + // Assert CI Pipeline (Build SA) Permissions + assertHasRole(assert, garBindings, "roles/artifactregistry.writer", buildSAEmail, "Artifact Registry") + // (Assume KMS is bound at project level for this example, adjust if bound to key) + assertHasRole(assert, projectBindings, "roles/cloudkms.signerVerifier", buildSAEmail, "Project (KMS Signer)") + + // Assert CD Pipeline (Deploy SA) Permissions + assertHasRole(assert, projectBindings, "roles/clouddeploy.jobRunner", deploySAEmail, "Project (Cloud Deploy Runner)") + assertHasRole(assert, projectBindings, "roles/container.developer", deploySAEmail, "Project (GKE Developer)") + }) - // call the test function to execute the integration test + + // Optional: Define custom teardown if necessary (e.g., deleting leftover Cloud Build artifacts or GAR images) + // standaloneSingleProjT.DefineTeardown(func(assert *assert.Assertions) { ... }) + + // 5. Execute the Test standaloneSingleProjT.Test() } diff --git a/test/integration/testutils/utils.go b/test/integration/testutils/utils.go new file mode 100644 index 00000000..fd097ae3 --- /dev/null +++ b/test/integration/testutils/utils.go @@ -0,0 +1,85 @@ +package testutils + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/gruntwork-io/terratest/modules/logger" + "github.com/gruntwork-io/terratest/modules/shell" +) + +// GetSecretFromSecretManager retrieves the latest version of a secret from Google Cloud Secret Manager using the gcloud CLI. +func GetSecretFromSecretManager(t *testing.T, secretName string, secretProject string) (string, error) { + t.Log("Retrieving secret from secret manager.") + cmd := fmt.Sprintf("secrets versions access latest --project=%s --secret=%s", secretProject, secretName) + args := strings.Fields(cmd) + gcloudCmd := shell.Command{ + Command: "gcloud", + Args: args, + Logger: logger.Discard, + } + return shell.RunCommandAndGetStdOutE(t, gcloudCmd) +} + +// ReplacePatternInTfVars will walk directories searching for terraform.tfvars and replace the pattern with the replacement +func ReplacePatternInTfVars(pattern string, replacement string, root string) error { + err := filepath.WalkDir(root, func(path string, d fs.DirEntry, fnErr error) error { + if fnErr != nil { + return fnErr + } + if !d.IsDir() && d.Name() == "terraform.tfvars" { + return replaceInFile(path, pattern, replacement) + } + return nil + }) + + return err +} + +// ReplacePatternInFile will walk directories searching for fileName and replace the pattern with the replacement +func ReplacePatternInFile(pattern string, replacement string, root string, fileName string) error { + err := filepath.WalkDir(root, func(path string, d fs.DirEntry, fnErr error) error { + if fnErr != nil { + return fnErr + } + if !d.IsDir() && d.Name() == fileName { + return replaceInFile(path, pattern, replacement) + } + return nil + }) + + return err +} + +// replaceInFile will replace oldPattern in filePath with newPattern +func replaceInFile(filePath, oldPattern, newPattern string) error { + fileInfo, err := os.Lstat(filePath) + if err != nil { + return err + } + + if fileInfo.Mode()&os.ModeSymlink != 0 { + fmt.Printf("%s is a symlink, will skip the pattern replacement.", filePath) + return nil + } else { + content, err := os.ReadFile(filePath) + if err != nil { + return err + } + + newContent := strings.ReplaceAll(string(content), oldPattern, newPattern) + + err = os.WriteFile(filePath, []byte(newContent), 0644) + if err != nil { + return err + } + + fmt.Printf("Updated file: %s\n", filePath) + + return nil + } +} diff --git a/test/setup/harness/gitlab/gitlab_vm.tf b/test/setup/harness/gitlab/gitlab_vm.tf new file mode 100644 index 00000000..62248be1 --- /dev/null +++ b/test/setup/harness/gitlab/gitlab_vm.tf @@ -0,0 +1,164 @@ +locals { + gitlab_network_url = "https://www.googleapis.com/compute/v1/projects/${var.project_id}/global/networks/${var.network_name}" +} + +resource "google_service_account" "gitlab_vm" { + account_id = "gitlab-vm-sa" + project = var.project_id + display_name = "Custom SA for VM Instance" +} + +resource "google_project_iam_member" "secret_manager_admin_vm_instance" { + project = var.project_id + role = "roles/secretmanager.admin" + member = google_service_account.gitlab_vm.member +} + +resource "google_compute_subnetwork" "gitlab_subnet" { + project = var.project_id + name = "gitlab-vm-subnet" + ip_cidr_range = "10.2.2.0/24" + region = "us-central1" + network = var.network_id +} + +resource "google_compute_instance" "default" { + name = "gitlab" + project = var.project_id + machine_type = "n2-standard-4" + zone = "us-central1-a" + + tags = ["git-vm"] + + boot_disk { + initialize_params { + image = "ubuntu-os-cloud/ubuntu-2204-lts" + } + } + + network_interface { + network = var.network_name + access_config {} # Ephemeral public IP + subnetwork = google_compute_subnetwork.gitlab_subnet.name + subnetwork_project = var.project_id + } + + metadata_startup_script = file("./../../scripts/gitlab_self_hosted.sh") + + service_account { + email = google_service_account.gitlab_vm.email + scopes = ["cloud-platform"] + } +} + +resource "google_secret_manager_secret" "gitlab_webhook" { + project = var.project_id + secret_id = "gitlab-webhook" + replication { + auto {} + } +} + +resource "random_uuid" "random_webhook_secret" {} + +resource "google_secret_manager_secret_version" "gitlab_webhook" { + secret = google_secret_manager_secret.gitlab_webhook.id + secret_data = random_uuid.random_webhook_secret.result +} + +# Firewall Rules +resource "google_compute_firewall" "allow_iap_ssh" { + name = "allow-iap-ssh" + network = var.network_name + project = var.project_id + allow { + ports = [22] + protocol = "tcp" + } + source_ranges = ["35.199.192.0/19"] +} + +resource "google_compute_firewall" "allow_http_https" { + name = "allow-http-https" + network = var.network_name + project = var.project_id + allow { + protocol = "tcp" + ports = ["80", "443"] + } + source_ranges = ["0.0.0.0/0"] + target_tags = ["git-vm"] +} + +# Service Directory & DNS for Private Access +resource "google_service_directory_namespace" "gitlab" { + provider = google-beta + namespace_id = "gitlab-namespace" + location = "us-central1" + project = var.project_id +} + +resource "google_service_directory_service" "gitlab" { + provider = google-beta + service_id = "gitlab" + namespace = google_service_directory_namespace.gitlab.id +} + +resource "google_service_directory_endpoint" "gitlab" { + provider = google-beta + endpoint_id = "endpoint" + service = google_service_directory_service.gitlab.id + network = var.network_id + address = google_compute_instance.default.network_interface[0].network_ip + port = 443 +} + +resource "google_dns_managed_zone" "sd_zone" { + provider = google-beta + name = "peering-zone" + dns_name = "example.com." + description = "Private DNS Service Directory zone for Gitlab Instance" + project = var.project_id + visibility = "private" + + service_directory_config { + namespace { + namespace_url = google_service_directory_namespace.gitlab.id + } + } + private_visibility_config { + networks { + network_url = local.gitlab_network_url + } + } +} + +module "ssl_cert" { + source = "terraform-google-modules/cloud-storage/google//modules/simple_bucket" + version = "~> 10.0" + + name = "${var.project_id}-ssl-cert" + project_id = var.project_id + location = "us-central1" + log_bucket = var.logging_bucket_name + log_object_prefix = "ssl-cert" + force_destroy = true + + versioning = true + encryption = { default_kms_key_name = var.logging_kms_crypto_id } + + # Module does not support values not know before apply (member and role are used to create the index in for_each) + # https://github.com/terraform-google-modules/terraform-google-cloud-storage/blob/v10.0.2/modules/simple_bucket/main.tf#L122 + # iam_members = [{ + # role = "roles/storage.admin" + # member = "${google_service_account.gitlab_vm.member}" + # }] + + depends_on = [time_sleep.waits_iam_propagation] +} + +resource "google_storage_bucket_iam_member" "ssl_storage_admin" { + bucket = module.ssl_cert.name + role = "roles/storage.admin" + member = google_service_account.gitlab_vm.member +} diff --git a/test/setup/harness/gitlab/outputs.tf b/test/setup/harness/gitlab/outputs.tf new file mode 100644 index 00000000..46d1cf70 --- /dev/null +++ b/test/setup/harness/gitlab/outputs.tf @@ -0,0 +1,31 @@ +output "gitlab_webhook_secret_id" { + value = google_secret_manager_secret.gitlab_webhook.id +} + +output "gitlab_pat_secret_name" { + value = "gitlab-pat-from-vm" +} + +output "gitlab_project_id" { + value = var.project_id +} + +output "gitlab_url" { + value = "https://${google_compute_instance.default.network_interface[0].access_config[0].nat_ip}.sslip.io" +} + +output "gitlab_secret_project" { + value = var.project_id +} + +output "gitlab_instance_zone" { + value = google_compute_instance.default.zone +} + +output "gitlab_instance_name" { + value = google_compute_instance.default.name +} + +output "gitlab_service_directory" { + value = google_service_directory_service.gitlab.id +} diff --git a/test/setup/harness/gitlab/variables.tf b/test/setup/harness/gitlab/variables.tf new file mode 100644 index 00000000..90c945cd --- /dev/null +++ b/test/setup/harness/gitlab/variables.tf @@ -0,0 +1,24 @@ +variable "project_id" { + description = "The project id where Gitlab will run." + type = string +} + +variable "network_name" { + description = "The network name where Gitlab will run." + type = string +} + +variable "network_id" { + description = "The network id where Gitlab will run." + type = string +} + +variable "cloud_build_sa" { + description = "Cloud Build Service Account email to be granted Encrypt/Decrypt role." + type = string +} + +variable "seed_project_number" { + description = "The seed project number." + type = string +} \ No newline at end of file diff --git a/test/setup/harness/gitlab/versions.tf b/test/setup/harness/gitlab/versions.tf new file mode 100644 index 00000000..afd3cd67 --- /dev/null +++ b/test/setup/harness/gitlab/versions.tf @@ -0,0 +1,21 @@ +terraform { + required_version = ">= 1.3" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 6.6 , < 8" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 6.6, < 8" + } + time = { + source = "hashicorp/time" + version = ">= 0.12.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.7.2" + } + } +} diff --git a/test/setup/scripts/gitlab_self_hosted.sh b/test/setup/scripts/gitlab_self_hosted.sh new file mode 100644 index 00000000..a8692e30 --- /dev/null +++ b/test/setup/scripts/gitlab_self_hosted.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# GitLab Installation +apt-get update +apt-get install -y curl openssh-server ca-certificates tzdata perl jq +curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.deb.sh | bash +apt-get install gitlab-ee=17.11.2-ee.0 + +# Retrieve values from Metadata Server +EXTERNAL_IP=$(curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip -H "Metadata-Flavor: Google") +PROJECT_ID=$(curl http://metadata.google.internal/computeMetadata/v1/project/project-id -H "Metadata-Flavor: Google") +URL="https://$EXTERNAL_IP.sslip.io" + +openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 3650 -nodes \ +-subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=gitlab.example.com" \ +-addext "subjectAltName=DNS:gitlab.example.com, IP:$EXTERNAL_IP, DNS:$EXTERNAL_IP.sslip.io" + +mv key.pem gitlab.key +mv cert.pem gitlab.crt + +mkdir -p /etc/gitlab/ssl +cp gitlab.* /etc/gitlab/ssl + +cat > /etc/gitlab/gitlab.rb < Date: Mon, 8 Jun 2026 17:05:46 -0300 Subject: [PATCH 2/6] feat: refactoring secure ci cd examples, integration test and modules --- .github/release-please.yml | 2 +- .github/trusted-contribution.yml | 2 +- .github/workflows/lint.yaml | 2 +- .github/workflows/periodic-reporter.yaml | 2 +- .github/workflows/stale.yml | 2 +- CHANGELOG.md | 7 + Makefile | 2 +- README.md | 77 +- build/cloudbuild-cd.yaml | 4 +- build/cloudbuild-ci.yaml | 2 +- .../policies/container-analysis-policy.yaml | 2 +- .../policies/container-structure-policy.yaml | 2 +- .../policies/githound.yaml | 2 +- examples/private_cluster_cicd/README.md | 65 - examples/private_cluster_cicd/main.tf | 128 -- examples/private_cluster_cicd/outputs.tf | 50 - examples/private_cluster_cicd/variables.tf | 45 - examples/standalone_single_project/README.md | 30 +- examples/standalone_single_project/cicd.tf | 41 +- examples/standalone_single_project/gke.tf | 3 +- examples/standalone_single_project/network.tf | 1 + examples/standalone_single_project/outputs.tf | 32 +- .../terraform.tfvars | 35 + .../standalone_single_project/variables.tf | 32 +- .../standalone_single_project/walkthrough.md | 123 +- metadata.display.yaml | 4 +- metadata.yaml | 182 +-- modules/attestor/README.md | 64 + modules/attestor/metadata.display.yaml | 39 + modules/attestor/metadata.yaml | 140 +++ modules/attestor/versions.tf | 1 + modules/cloudbuild-private-pool/main.tf | 2 - .../metadata.display.yaml | 7 +- modules/cloudbuild-private-pool/metadata.yaml | 248 ++-- modules/cloudbuild-private-pool/versions.tf | 15 +- modules/secure-cd/README.md | 161 ++- modules/secure-cd/build.tf | 142 ++- modules/secure-cd/iam.tf | 32 +- modules/secure-cd/main.tf | 3 +- modules/secure-cd/metadata.display.yaml | 19 +- modules/secure-cd/metadata.yaml | 306 +++-- modules/secure-cd/outputs.tf | 2 +- modules/secure-cd/variables.tf | 2 +- modules/secure-cd/versions.tf | 10 +- modules/secure-ci/README.md | 108 +- modules/secure-ci/builder_image.tf | 49 + .../secure-ci}/cloud-build-builder/Dockerfile | 12 +- .../cloudbuild-skaffold-build-image.yaml | 7 +- modules/secure-ci/main.tf | 17 +- modules/secure-ci/metadata.display.yaml | 22 +- modules/secure-ci/metadata.yaml | 382 +++--- modules/secure-ci/outputs.tf | 13 +- modules/secure-ci/versions.tf | 7 + modules/workerpool-gke-ha-vpn/main.tf | 17 +- .../metadata.display.yaml | 4 +- modules/workerpool-gke-ha-vpn/metadata.yaml | 279 +++-- modules/workerpool-gke-ha-vpn/versions.tf | 7 +- test/integration/go.mod | 106 +- test/integration/go.sum | 1071 ++--------------- .../cluster_network_test.go | 44 + .../harness_gitlab/gitlab_setup_test.go | 54 + .../logging_bucket_test.go | 34 + .../private_workerpool_test.go | 35 + .../bootstrap_gitlab_environment_test.go | 213 ++++ .../standalone_single_project_test.go | 247 ++-- test/integration/testutils/retry.go | 72 ++ test/integration/testutils/utils.go | 17 +- test/setup/harness/cluster_network/main.tf | 47 + test/setup/harness/cluster_network/outputs.tf | 34 + .../harness/cluster_network/variables.tf | 48 + test/setup/harness/gitlab/gitlab_vm.tf | 290 ++++- test/setup/harness/gitlab/outputs.tf | 34 +- test/setup/harness/gitlab/variables.tf | 43 +- test/setup/harness/gitlab/versions.tf | 16 + test/setup/harness/logging_bucket/main.tf | 94 ++ test/setup/harness/logging_bucket/outputs.tf | 31 + .../setup/harness/logging_bucket/variables.tf | 36 + test/setup/harness/logging_bucket/versions.tf | 37 + .../harness/private_workerpool/outputs.tf | 43 + .../private_workerpool/private_workerpool.tf | 27 + .../harness/private_workerpool/variables.tf | 52 + .../harness/private_workerpool/versions.tf | 37 + test/setup/iam.tf | 40 +- test/setup/main.tf | 306 +---- test/setup/modules/cluster_network/main.tf | 63 + test/setup/modules/cluster_network/outputs.tf | 45 + .../modules/cluster_network/variables.tf | 54 + .../setup/modules/cluster_network/versions.tf | 37 + test/setup/modules/private_workerpool/main.tf | 62 + test/setup/modules/private_workerpool/nat.tf | 150 +++ .../modules/private_workerpool/network.tf | 55 + .../modules/private_workerpool/outputs.tf | 44 + .../modules/private_workerpool/variables.tf | 57 + .../modules/private_workerpool/versions.tf | 37 + test/setup/outputs.tf | 43 +- test/setup/scripts/gitlab_self_hosted.sh | 40 +- 96 files changed, 4168 insertions(+), 2623 deletions(-) rename {examples/private_cluster_cicd => build}/policies/container-analysis-policy.yaml (97%) rename {examples/private_cluster_cicd => build}/policies/container-structure-policy.yaml (97%) rename {examples/private_cluster_cicd => build}/policies/githound.yaml (97%) delete mode 100644 examples/private_cluster_cicd/README.md delete mode 100644 examples/private_cluster_cicd/main.tf delete mode 100644 examples/private_cluster_cicd/outputs.tf delete mode 100644 examples/private_cluster_cicd/variables.tf create mode 100644 examples/standalone_single_project/terraform.tfvars create mode 100644 modules/attestor/README.md create mode 100644 modules/attestor/metadata.display.yaml create mode 100644 modules/attestor/metadata.yaml create mode 100644 modules/secure-ci/builder_image.tf rename {examples/private_cluster_cicd => modules/secure-ci}/cloud-build-builder/Dockerfile (86%) rename {examples/private_cluster_cicd => modules/secure-ci}/cloud-build-builder/cloudbuild-skaffold-build-image.yaml (83%) create mode 100644 test/integration/harness_cluster_network/cluster_network_test.go create mode 100644 test/integration/harness_gitlab/gitlab_setup_test.go create mode 100644 test/integration/harness_logging_bucket/logging_bucket_test.go create mode 100644 test/integration/harness_private_workerpool/private_workerpool_test.go create mode 100644 test/integration/harness_setup_gitlab/bootstrap_gitlab_environment_test.go create mode 100644 test/integration/testutils/retry.go create mode 100644 test/setup/harness/cluster_network/main.tf create mode 100644 test/setup/harness/cluster_network/outputs.tf create mode 100644 test/setup/harness/cluster_network/variables.tf create mode 100644 test/setup/harness/logging_bucket/main.tf create mode 100644 test/setup/harness/logging_bucket/outputs.tf create mode 100644 test/setup/harness/logging_bucket/variables.tf create mode 100644 test/setup/harness/logging_bucket/versions.tf create mode 100644 test/setup/harness/private_workerpool/outputs.tf create mode 100644 test/setup/harness/private_workerpool/private_workerpool.tf create mode 100644 test/setup/harness/private_workerpool/variables.tf create mode 100644 test/setup/harness/private_workerpool/versions.tf create mode 100644 test/setup/modules/cluster_network/main.tf create mode 100644 test/setup/modules/cluster_network/outputs.tf create mode 100644 test/setup/modules/cluster_network/variables.tf create mode 100644 test/setup/modules/cluster_network/versions.tf create mode 100644 test/setup/modules/private_workerpool/main.tf create mode 100644 test/setup/modules/private_workerpool/nat.tf create mode 100644 test/setup/modules/private_workerpool/network.tf create mode 100644 test/setup/modules/private_workerpool/outputs.tf create mode 100644 test/setup/modules/private_workerpool/variables.tf create mode 100644 test/setup/modules/private_workerpool/versions.tf diff --git a/.github/release-please.yml b/.github/release-please.yml index 884357c8..a39f083e 100644 --- a/.github/release-please.yml +++ b/.github/release-please.yml @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.github/trusted-contribution.yml b/.github/trusted-contribution.yml index ae57820f..280627bd 100644 --- a/.github/trusted-contribution.yml +++ b/.github/trusted-contribution.yml @@ -1,4 +1,4 @@ -# Copyright 2023-2025 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 0ca86113..b5e60298 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -1,4 +1,4 @@ -# Copyright 2023-2025 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.github/workflows/periodic-reporter.yaml b/.github/workflows/periodic-reporter.yaml index 79e9d427..95513ed5 100644 --- a/.github/workflows/periodic-reporter.yaml +++ b/.github/workflows/periodic-reporter.yaml @@ -1,4 +1,4 @@ -# Copyright 2023-2025 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 792a1a41..fe72d84a 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,4 +1,4 @@ -# Copyright 2022-2025 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/CHANGELOG.md b/CHANGELOG.md index 016e5552..65441c3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). This changelog is generated automatically based on [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). +## [1.2.2](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/compare/v1.2.1...v1.2.2) (2026-05-11) + + +### Bug Fixes + +* restrict vpn_ha version to not pick up 2.3.2 ([94e4334](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/commit/94e433408b37b6713ca65a9b8c81b775c5e5b161)) + ## [1.2.1](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/compare/v1.2.0...v1.2.1) (2023-08-02) diff --git a/Makefile b/Makefile index ccb48b52..b32b6dd1 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ # Make will use bash instead of sh SHELL := /usr/bin/env bash -DOCKER_TAG_VERSION_DEVELOPER_TOOLS := 1.14 +DOCKER_TAG_VERSION_DEVELOPER_TOOLS := 1.25 DOCKER_IMAGE_DEVELOPER_TOOLS := cft/developer-tools REGISTRY_URL := gcr.io/cloud-foundation-cicd ENABLE_BPMETADATA := 1 diff --git a/README.md b/README.md index ddb728ed..0bc75f8a 100644 --- a/README.md +++ b/README.md @@ -8,21 +8,21 @@ The Terraform modules in this repository provide an opinionated architecture tha Create a CI/CD pipeline that follows security best practices. ### Detailed -Set up a secure CI/CD pipeline that follows best practices for building, scanning, storing, and deploying containers to GKE. +Set up a secure CI/CD pipeline that follows best practices for building, scanning, storing, and deploying containers to GKE, Anthos, or Cloud Run. You can choose whether to deploy your solution through the console directly or download as Terraform from GitHub to deploy later. ### Architecture -1. A developer pushes new code or a code change for a container-based application to Cloud Source Repositories. -1. The code push invokes a Cloud Build trigger. The Cloud Build trigger starts a build in a Cloud Build private worker pool that's hosted in a customer-managed VPC. The outputs of the build are metadata files, Cloud Build logs, and containers. -1. The metadata files and the Cloud Build logs are stored in a Cloud Storage bucket. -1. The pipeline runs security scans (which you configure) and validates the container structure. When the scans and structure pass, the containers are stored in Artifact Registry. -1. The Cloud Build trigger requests an attestation from Binary Authorization that certifies that the required scans passed. The attestation is stored as a cryptographic signature in Binary Authorization. -1. The Cloud Build trigger starts a Cloud Deploy release to roll out the containers to the three environments: developer (Dev), QA, and production (Prod). Each environment is one Kubernetes cluster in its own subnet. All three clusters and subnets are in the same GKE VPC. The last cluster is the production environment. -1. As the rollout starts, Cloud Deploy sends the containers to the developer environment. Google Kubernetes Engine (GKE) uses the policy that's defined for the cluster to check the containers’ build attestation in Binary Authorization. When this check passes, GKE deploys the containers into the Kubernetes cluster. -1. When Cloud Deploy releases the containers to the developer environment, Cloud Deploy sends a Pub/Sub message that starts the second Cloud Build trigger. This Cloud Build trigger runs post-deployment tests (which you configure) in the developer environment. To run these tests, Cloud Build worker pools communicate with the clusters using Connect Gateway. -1. When the post-deployment checks succeed, the Cloud Build trigger requests an attestation from Binary Authorization. The attestation certifies that the required tests from the developer environment passed. -1. Cloud Deploy promotes the release to the second Kubernetes cluster for the QA environment. Steps 7 to 9 run again with some differences: GKE checks for the two attestations before deploying, and after the tests pass, the quality attestation is created. -1. Cloud Deploy promotes the release to the production environment, which is the third Kubernetes cluster. GKE uses a policy to check for all three attestations in Binary Authorization. When this check passes, GKE deploys the containers in the Kubernetes cluster for the production environment. +1. A developer pushes new code or a code change for a container-based application to a Source Repository (Cloud Source Repositories, GitHub, or GitLab). +2. The code push invokes a Cloud Build trigger. The Cloud Build trigger starts a build in a Cloud Build private worker pool that's hosted in a customer-managed VPC. The outputs of the build are metadata files, Cloud Build logs, and containers. +3. The metadata files and the Cloud Build logs are stored in a Cloud Storage bucket. +4. The pipeline runs security scans (which you configure) and validates the container structure. When the scans and structure pass, the containers are stored in Artifact Registry. +5. The Cloud Build trigger requests an attestation from Binary Authorization that certifies that the required scans passed. The attestation is stored as a cryptographic signature in Binary Authorization. +6. The Cloud Build trigger starts a Cloud Deploy release to roll out the containers to the three environments: developer (Dev), QA, and production (Prod). Each environment is one Kubernetes cluster in its own subnet. All three clusters and subnets are in the same GKE VPC. The last cluster is the production environment. +7. As the rollout starts, Cloud Deploy sends the containers to the developer environment. Google Kubernetes Engine (GKE) uses the policy that's defined for the cluster to check the containers’ build attestation in Binary Authorization. When this check passes, GKE deploys the containers into the Kubernetes cluster. +8. When Cloud Deploy releases the containers to the developer environment, Cloud Deploy sends a Pub/Sub message that starts the second Cloud Build trigger. This Cloud Build trigger runs post-deployment tests (which you configure) in the developer environment. To run these tests, Cloud Build worker pools communicate with the clusters using Connect Gateway. +9. When the post-deployment checks succeed, the Cloud Build trigger requests an attestation from Binary Authorization. The attestation certifies that the required tests from the developer environment passed. +10. Cloud Deploy promotes the release to the second Kubernetes cluster for the QA environment. Steps 7 to 9 run again with some differences: GKE checks for the two attestations before deploying, and after the tests pass, the quality attestation is created. +11. Cloud Deploy promotes the release to the production environment, which is the third Kubernetes cluster. GKE uses a policy to check for all three attestations in Binary Authorization. When this check passes, GKE deploys the containers in the Kubernetes cluster for the production environment. ## Documentation - [Architecture Diagram](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/blob/main/assets/secure_cicd_pipeline_v2.svg) @@ -36,51 +36,63 @@ Basic usage of this module is as follows: module "ci_pipeline" { source = "GoogleCloudPlatform/secure-cicd/google//modules/secure-ci" - project_id = var.project_id + project_id = {PROJECT_ID} primary_location = "us-central1" + repository_type = "CSR" + csr_app_source_repo = "my-app-source" attestor_names_prefix = ["build", "security", "quality"] app_build_trigger_yaml = "cloudbuild-ci.yaml" - runner_build_folder = "../../../examples/app_cicd/cloud-build-builder" build_image_config_yaml = "cloudbuild-skaffold-build-image.yaml" - trigger_branch_name = ".*" + trigger_branch_name = "^main$" } # Secure-CD module "cd_pipeline" { source = "GoogleCloudPlatform/secure-cicd/google//modules/secure-cd" - project_id = var.project_id - primary_location = "us-central1" - gar_repo_name = module.ci_pipeline.app_artifact_repo - cloudbuild_cd_repo = "cloudbuild-cd-config-pc" + project_id = {PROJECT_ID} + primary_location = "us-central1" + repository_type = "CSR" + csr_cloudbuild_cd_repo = "cloudbuild-cd-config-pc" + gar_repo_name = module.ci_pipeline.app_artifact_repo + app_deploy_trigger_yaml = "cloudbuild-cd.yaml" + cache_bucket_name = module.ci_pipeline.cache_bucket_name + clouddeploy_pipeline_name = "my-app-delivery-pipeline" + cloudbuild_service_account = module.ci_pipeline.build_sa_email + deploy_branch_clusters = { dev = { cluster = "dev-cluster", - project_id = "gke-proj-dev", + anthos_membership = "", + project_id = "{PROJECT_ID}", location = "us-central1", - required_attestations = ["projects/${var.project_id}/attestors/build-attestor"] - env_attestation = "projects/${var.project_id}/attestors/security-attestor" + required_attestations = ["projects/${{PROJECT_ID}}/attestors/build-attestor"] + env_attestation = "projects/${{PROJECT_ID}}/attestors/security-attestor" next_env = "qa" + target_type = "gke" }, qa = { cluster = "qa-cluster", - project_id = "gke-proj-prod", + anthos_membership = "", + project_id = "{PROJECT_ID}", location = "us-central1", - required_attestations = ["projects/${var.project_id}/attestors/security-attestor", "projects/${var.project_id}/attestors/build-attestor"] - env_attestation = "projects/${var.project_id}/attestors/quality-attestor" + required_attestations = ["projects/${{PROJECT_ID}}/attestors/security-attestor", "projects/${{PROJECT_ID}}/attestors/build-attestor"] + env_attestation = "projects/${{PROJECT_ID}}/attestors/quality-attestor" next_env = "prod" + target_type = "gke" }, prod = { cluster = "prod-cluster", - project_id = "gke-proj-prod", + anthos_membership = "", + project_id = "{PROJECT_ID}", location = "us-central1", - required_attestations = ["projects/${var.project_id}/attestors/quality-attestor", "projects/${var.project_id}/attestors/security-attestor", "projects/${var.project_id}/attestors/build-attestor"] + required_attestations = ["projects/${{PROJECT_ID}}/attestors/quality-attestor", "projects/${{PROJECT_ID}}/attestors/security-attestor", "projects/${{PROJECT_ID}}/attestors/build-attestor"] env_attestation = "" next_env = "" + target_type = "gke" }, } - app_deploy_trigger_yaml = "cloudbuild-cd.yaml" - cache_bucket_name = module.ci_pipeline.cache_bucket_name + depends_on = [ module.ci_pipeline ] @@ -118,7 +130,8 @@ the resources of this blueprint: - CI/CD project - `roles/artifactregistry.admin` - `roles/binaryauthorization.attestorsAdmin` - - `roles/cloudbuild.builds.builder` + - `roles/cloudbuild.builds.editor` + - `roles/cloudbuild.connectionAdmin` - `roles/cloudbuild.workerPoolOwner` - `roles/clouddeploy.admin` - `roles/cloudkms.admin` @@ -128,7 +141,8 @@ the resources of this blueprint: - `roles/gkehub.editor` - `roles/iam.serviceAccountAdmin` - `roles/iam.serviceAccountUser` - - `roles/pubsub.editor` + - `roles/pubsub.admin` + - `roles/secretmanager.secretAccessor` - `roles/serviceusage.serviceUsageAdmin` - `roles/source.admin` - `roles/storage.admin` @@ -168,6 +182,7 @@ CI/CD Project - Cloud KMS API `cloudkms.googleapis.com` - Binary Authorization API `binaryauthorization.googleapis.com` - Container Scanning API `containerscanning.googleapis.com` +- Secret Manager API `secretmanager.googleapis.com` GKE Projects: - Cloud Resource Manager API `cloudresourcemanager.googleapis.com` diff --git a/build/cloudbuild-cd.yaml b/build/cloudbuild-cd.yaml index 9a336523..6f9b57d0 100644 --- a/build/cloudbuild-cd.yaml +++ b/build/cloudbuild-cd.yaml @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,8 +28,8 @@ artifacts: - '/workspace/svcs-endpoints-filtered.json' - '/workspace/zap_report_${_TARGET_ID}_${_RELEASE_ID}.html' logsBucket: gs://$_CACHE_BUCKET_NAME/build_logs -steps: +steps: ############################### Post-Deploy Checks ########################### # Get endpoint IPs diff --git a/build/cloudbuild-ci.yaml b/build/cloudbuild-ci.yaml index 6486a167..8af00240 100644 --- a/build/cloudbuild-ci.yaml +++ b/build/cloudbuild-ci.yaml @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/private_cluster_cicd/policies/container-analysis-policy.yaml b/build/policies/container-analysis-policy.yaml similarity index 97% rename from examples/private_cluster_cicd/policies/container-analysis-policy.yaml rename to build/policies/container-analysis-policy.yaml index 82ba2fce..6a31383a 100644 --- a/examples/private_cluster_cicd/policies/container-analysis-policy.yaml +++ b/build/policies/container-analysis-policy.yaml @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/private_cluster_cicd/policies/container-structure-policy.yaml b/build/policies/container-structure-policy.yaml similarity index 97% rename from examples/private_cluster_cicd/policies/container-structure-policy.yaml rename to build/policies/container-structure-policy.yaml index 8b75d8bf..9f6dbabe 100644 --- a/examples/private_cluster_cicd/policies/container-structure-policy.yaml +++ b/build/policies/container-structure-policy.yaml @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/private_cluster_cicd/policies/githound.yaml b/build/policies/githound.yaml similarity index 97% rename from examples/private_cluster_cicd/policies/githound.yaml rename to build/policies/githound.yaml index 55edf755..4cb6343d 100644 --- a/examples/private_cluster_cicd/policies/githound.yaml +++ b/build/policies/githound.yaml @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/private_cluster_cicd/README.md b/examples/private_cluster_cicd/README.md deleted file mode 100644 index 23a8395c..00000000 --- a/examples/private_cluster_cicd/README.md +++ /dev/null @@ -1,65 +0,0 @@ -# Private Cluster CI/CD example - -This end to end example showcases the [`secure-ci`](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/tree/main/modules/secure-ci) and [`secure-cd`](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/tree/main/maodules/secure-cd) modules working together to create a secure software build and deploy pipeline. This example also deploys the [`cloudbuild-private-pool`](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/tree/main/modules/cloudbuild-private-pool) and [`workerpool-gke-ha-vpn`](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/tree/main/modules/workerpool-gke-ha-vpn) modules to enable deploying to private GKE clusters from Cloud Build. - -We will use the Bank of Anthos sample application as the target code on which to execute the CI/CD pipeline. - -## Prerequisites -* GKE cluster(s) to deploy workloads, specified using the `deploy_branch_clusters` variable. See the [secure-cd module](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/tree/main/modules/secure-cd) for details on specifying the `deploy_branch_clusters` object. -* Run `gcloud auth application-default login` before following the steps below. - -## Setup - -To deploy this example: - -### Build the infrastructure - -1. Create working directory -```sh -mkdir cicd-example -cd cicd-example -``` -2. Clone `secure-cicd` repo -```sh -git clone https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd.git -``` -3. Run `terraform init` from within this example directory. -```sh -cd terraform-google-secure-cicd/examples/private_cluster_cicd -terraform init -``` -4. Create a `terraform.tfvars` file to provide values for `project_id` and `deploy_branch_clusters` and `gke_networks` Optionally override any variables if necessary. - -5. Run `terraform apply` within this example directory. - -Follow steps 6 onward in the [app_cicd example instructions](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/blob/main/examples/app_cicd/README.md). - -Replace step 16: - -16. Copy `policies` folder to `app-source` repo -```sh -cp -R terraform-google-secure-cicd/examples/private_cluster_cicd/policies bank-of-anthos/policies -``` - - -## Inputs - -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| deploy\_branch\_clusters | mapping of branch names to cluster deployments. target\_type can be one of `gke`, `anthos_cluster`, or `run`. See [clouddeploy\_target Terraform docs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/clouddeploy_target) for more details |
map(object({
cluster = string
anthos_membership = string
project_id = string
location = string
required_attestations = list(string)
env_attestation = string
next_env = string
target_type = string
}))
| `{}` | no | -| gke\_networks | list of GKE cluster networks in which to create VPN connections |
list(object({
control_plane_cidrs = map(string)
location = string
network = string
project_id = string
}))
| n/a | yes | -| project\_id | Project ID for CICD Pipeline Project | `string` | n/a | yes | - -## Outputs - -| Name | Description | -|------|-------------| -| binauth\_attestor\_names | Names of Attestors | -| binauth\_attestor\_project\_id | Project ID where attestors get created | -| boa\_artifact\_repo | GAR Repo created to store BoA images | -| build\_trigger\_name | The name of the cloud build trigger for the bank of anthos repo. | -| cache\_bucket\_name | The name of the storage bucket for cloud build. | -| project\_id | The project to run tests against | -| source\_repo\_names | Name of the created CSR repos | - - diff --git a/examples/private_cluster_cicd/main.tf b/examples/private_cluster_cicd/main.tf deleted file mode 100644 index c7e62bbe..00000000 --- a/examples/private_cluster_cicd/main.tf +++ /dev/null @@ -1,128 +0,0 @@ -/** - * 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. - */ - -locals { - clouddeploy_pipeline_name = "pipeline-private" -} - -# Secure-CI -module "ci_pipeline" { - source = "GoogleCloudPlatform/secure-cicd/google//modules/secure-ci" - version = "~> 1.0" - - project_id = var.project_id - app_source_repo = "app-source-pc" - cloudbuild_cd_repo = "cloudbuild-cd-config-pc" - gar_repo_name_suffix = "app-image-repo-pc" - cache_bucket_name = "private-cluster-cloudbuild" - primary_location = "us-central1" - attestor_names_prefix = ["build-pc", "security-pc", "quality-pc"] - app_build_trigger_yaml = "cloudbuild-ci.yaml" - runner_build_folder = "${path.module}/cloud-build-builder" - build_image_config_yaml = "cloudbuild-skaffold-build-image.yaml" - trigger_branch_name = ".*" - cloudbuild_private_pool = module.cloudbuild_private_pool.workerpool_id - clouddeploy_pipeline_name = local.clouddeploy_pipeline_name -} - -# Secure-CD -module "cd_pipeline" { - source = "GoogleCloudPlatform/secure-cicd/google//modules/secure-cd" - version = "~> 1.0" - - project_id = var.project_id - primary_location = "us-central1" - - gar_repo_name = module.ci_pipeline.app_artifact_repo - cloudbuild_cd_repo = "cloudbuild-cd-config-pc" - deploy_branch_clusters = var.deploy_branch_clusters - app_deploy_trigger_yaml = "cloudbuild-cd.yaml" - cache_bucket_name = module.ci_pipeline.cache_bucket_name - cloudbuild_private_pool = module.cloudbuild_private_pool.workerpool_id - clouddeploy_pipeline_name = local.clouddeploy_pipeline_name - cloudbuild_service_account = module.ci_pipeline.build_sa_email - depends_on = [ - module.ci_pipeline - ] -} - -# Cloud Build Private Pool -module "cloudbuild_private_pool" { - source = "GoogleCloudPlatform/secure-cicd/google//modules/cloudbuild-private-pool" - version = "~> 1.0" - - project_id = var.project_id - network_project_id = var.project_id - location = "us-central1" - create_cloudbuild_network = true - private_pool_vpc_name = "gke-private-pool-example-vpc" - worker_pool_name = "private-cluster-example-workerpool" - machine_type = "e2-highcpu-32" - - worker_address = "10.39.0.0" - worker_range_name = "private-cluster-example-worker-range" -} - -# Cloud Build Workerpool <-> GKE HA VPNs -locals { - gke_networks = { - for net in var.gke_networks : net.network => merge(net, local.vpn_config[net.network]) - } - vpn_config = { - gke-private-vpc-dev = { - gateway_1_asn = 65007, - gateway_2_asn = 65008, - bgp_range_1 = "169.254.7.0/30", - bgp_range_2 = "169.254.8.0/30" - }, - gke-private-vpc-qa = { - gateway_1_asn = 65009, - gateway_2_asn = 65010, - bgp_range_1 = "169.254.9.0/30", - bgp_range_2 = "169.254.10.0/30" - }, - gke-private-vpc-prod = { - gateway_1_asn = 65011, - gateway_2_asn = 65012, - bgp_range_1 = "169.254.11.0/30", - bgp_range_2 = "169.254.12.0/30" - } - } -} - -module "gke_cloudbuild_vpn" { - for_each = local.gke_networks - - source = "GoogleCloudPlatform/secure-cicd/google//modules/workerpool-gke-ha-vpn" - version = "~> 1.0" - - project_id = var.project_id - location = "us-central1" - - gke_project = each.value.project_id - gke_network = each.value.network - gke_location = each.value.location - gke_control_plane_cidrs = each.value.control_plane_cidrs - - workerpool_network = module.cloudbuild_private_pool.workerpool_network - workerpool_range = module.cloudbuild_private_pool.workerpool_range - gateway_1_asn = each.value.gateway_1_asn - gateway_2_asn = each.value.gateway_2_asn - bgp_range_1 = each.value.bgp_range_1 - bgp_range_2 = each.value.bgp_range_2 - - vpn_router_name_prefix = "pc-" -} diff --git a/examples/private_cluster_cicd/outputs.tf b/examples/private_cluster_cicd/outputs.tf deleted file mode 100644 index dd68e5d8..00000000 --- a/examples/private_cluster_cicd/outputs.tf +++ /dev/null @@ -1,50 +0,0 @@ -/** - * 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. - */ - -output "project_id" { - value = var.project_id - description = "The project to run tests against" -} - -output "binauth_attestor_names" { - description = "Names of Attestors" - value = module.ci_pipeline.binauth_attestor_names -} - -output "binauth_attestor_project_id" { - description = "Project ID where attestors get created" - value = module.ci_pipeline.binauth_attestor_project_id -} - -output "boa_artifact_repo" { - description = "GAR Repo created to store BoA images" - value = module.ci_pipeline.app_artifact_repo -} - -output "cache_bucket_name" { - description = "The name of the storage bucket for cloud build." - value = module.ci_pipeline.cache_bucket_name -} - -output "build_trigger_name" { - description = "The name of the cloud build trigger for the bank of anthos repo." - value = module.ci_pipeline.build_trigger_name -} - -output "source_repo_names" { - description = "Name of the created CSR repos" - value = module.ci_pipeline.source_repo_names -} diff --git a/examples/private_cluster_cicd/variables.tf b/examples/private_cluster_cicd/variables.tf deleted file mode 100644 index d3a2fdf8..00000000 --- a/examples/private_cluster_cicd/variables.tf +++ /dev/null @@ -1,45 +0,0 @@ -/** - * 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. - */ - -variable "project_id" { - type = string - description = "Project ID for CICD Pipeline Project" -} - -variable "gke_networks" { - type = list(object({ - control_plane_cidrs = map(string) - location = string - network = string - project_id = string - })) - description = "list of GKE cluster networks in which to create VPN connections" -} - -variable "deploy_branch_clusters" { - type = map(object({ - cluster = string - anthos_membership = string - project_id = string - location = string - required_attestations = list(string) - env_attestation = string - next_env = string - target_type = string - })) - description = "mapping of branch names to cluster deployments. target_type can be one of `gke`, `anthos_cluster`, or `run`. See [clouddeploy_target Terraform docs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/clouddeploy_target) for more details" - default = {} -} diff --git a/examples/standalone_single_project/README.md b/examples/standalone_single_project/README.md index 27ba8a68..613951be 100644 --- a/examples/standalone_single_project/README.md +++ b/examples/standalone_single_project/README.md @@ -1,31 +1,47 @@ # Standalone Single Project CI/CD example -This end to end example showcases the [`secure-ci`](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/tree/main/modules/secure-ci) and [`secure-cd`](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/tree/main/maodules/secure-cd) modules working together to create a secure software build and deploy pipeline. This example also deploys the [`cloudbuild-private-pool`](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/tree/main/modules/cloudbuild-private-pool) and [`workerpool-gke-ha-vpn`](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/tree/main/modules/workerpool-gke-ha-vpn) modules to enable deploying to private GKE clusters from Cloud Build. +This end-to-end example showcases the [`secure-ci`](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/tree/main/modules/secure-ci) and [`secure-cd`](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/tree/main/modules/secure-cd) modules working together to create a secure software build and deploy pipeline. -This example also creates GKE clusters and accompanying VPC networks for multiple smaple environments in a single project for simplified deployment and demonstration purposes. +This example also deploys the [`cloudbuild-private-pool`](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/tree/main/modules/cloudbuild-private-pool) module to enable deploying to private GKE clusters from Cloud Build. + +For simplified deployment and demonstration purposes, this blueprint creates GKE clusters and accompanying VPC networks for multiple sample environments (dev, qa, prod) within a single Google Cloud project. ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| app\_name | Name of intended deployed application; to be used as a prefix for certain resources | `string` | `"my-app"` | no | +| app\_name | Name of intended deployed application; to be used as a prefix for certain resources | `string` | `"ci-cd"` | no | +| cd\_repository | The CI repository to configure. The key is a short name for the service. |
object({
repository_name = string
repository_url = string
})
| `null` | no | +| ci\_repository | The CI repository to configure. The key is a short name for the service. |
object({
repository_name = string
repository_url = string
})
| `null` | no | | cloudbuild\_private\_pool\_machine\_type | Machine type for Cloud Build private pool | `string` | `"e2-medium"` | no | | env1\_name | Name of environment 1 | `string` | `"dev"` | no | | env2\_name | Name of environment 2 | `string` | `"qa"` | no | | env3\_name | Name of environment 3 | `string` | `"prod"` | no | +| github\_auth | Authentication configuration for GitHub. Required only if repo\_type is 'GITHUBv2'. |
object({
secret_id = string
app_id_secret_id = string
secret_project_id = string
})
| `null` | no | +| gitlab\_auth | Authentication configuration for GitLab. Required only if repo\_type is 'GITLABv2'. |
object({
read_authorizer_credential_secret_id = string
authorizer_credential_secret_id = string
webhook_secret_id = string
enterprise_host_uri = optional(string)
enterprise_service_directory = optional(string)
enterprise_ca_certificate = optional(string)
secret_project_id = string
})
| `null` | no | | labels | A set of key/value label pairs to assign to the resources deployed by this blueprint. | `map(string)` | `{}` | no | +| network\_name | Optional vpc network name if using already existing vpc | `any` | `null` | no | +| private\_worker\_pool\_id | Optional private worker pool id if using already existing worker pool | `any` | `null` | no | | project\_id | Project ID in which all resources will be deployed | `string` | n/a | yes | | region | Location in which all regional resources will be deployed | `string` | `"us-central1"` | no | +| repository\_type | Repository type, e.g. GITHUB or GITLAB | `string` | `"GITLAB"` | no | ## Outputs | Name | Description | |------|-------------| -| app\_source\_repo | URL of the created CSR app soure repo | -| cloudbuild\_cd\_repo\_name | URL of the created CSR app soure repo | -| console\_walkthrough\_link | URL to open the in-console walkthrough. | +| cd\_repo\_name | The name of the CD repository. | +| cd\_repo\_url | The URL of the CD repository. | +| ci\_repo\_name | The name of the CI repository. | +| ci\_repo\_url | The URL of the CI repository. | +| clouddeploy\_pipeline\_id | ID of the Cloud Deploy delivery pipeline | +| cluster\_names | Comma-separated names of the deployed GKE clusters | +| console\_walkthrough\_link | n/a | | gar\_repo | Artifact Registry repo | -| neos\_tutorial\_url | The URL to launch the in-console tutorial for the Secure CI/CD pipeline solution | +| gitlab\_url | The URL of the GitLab instance. | +| neos\_tutorial\_url | n/a | +| project\_id | Project ID | +| region | Region | diff --git a/examples/standalone_single_project/cicd.tf b/examples/standalone_single_project/cicd.tf index fc5a965b..857d2c2e 100644 --- a/examples/standalone_single_project/cicd.tf +++ b/examples/standalone_single_project/cicd.tf @@ -20,7 +20,7 @@ locals { cluster = module.gke_cluster[var.env1_name].name, anthos_membership = module.fleet_membership[var.env1_name].cluster_membership_id target_type = "anthos_cluster" - network = module.vpc.network_name + network = var.network_name == null ? module.vpc.network_name : var.network_name project_id = var.project_id location = var.region required_attestations = [module.attestors.binauth_attestor_ids["build"]] @@ -31,7 +31,7 @@ locals { cluster = module.gke_cluster[var.env2_name].name, anthos_membership = module.fleet_membership[var.env2_name].cluster_membership_id target_type = "anthos_cluster" - network = module.vpc.network_name + network = var.network_name == null ? module.vpc.network_name : var.network_name project_id = var.project_id location = var.region required_attestations = [module.attestors.binauth_attestor_ids["security"], module.attestors.binauth_attestor_ids["build"]] @@ -42,7 +42,7 @@ locals { cluster = module.gke_cluster[var.env3_name].name, anthos_membership = module.fleet_membership[var.env3_name].cluster_membership_id target_type = "anthos_cluster" - network = module.vpc.network_name + network = var.network_name == null ? module.vpc.network_name : var.network_name project_id = var.project_id location = var.region required_attestations = [module.attestors.binauth_attestor_ids["quality"], module.attestors.binauth_attestor_ids["security"], module.attestors.binauth_attestor_ids["build"]] @@ -56,11 +56,13 @@ locals { # Secure-CI module "ci_pipeline" { - source = "../../modules/secure-ci" + source = "GoogleCloudPlatform/secure-cicd/google//modules/secure-ci" + version = "~> 1.0" project_id = var.project_id - repository_type = "GITHUB" - github_auth = var.github_auth + repository_type = var.repository_type + github_auth = var.repository_type == "GITHUB" ? var.github_auth : null + gitlab_auth = var.repository_type == "GITLAB" ? var.gitlab_auth : null ci_repository = var.ci_repository gar_repo_name_suffix = "${var.app_name}-image-repo" cache_bucket_name = "${var.app_name}-cloudbuild" @@ -69,43 +71,41 @@ module "ci_pipeline" { app_build_trigger_yaml = "cloudbuild-ci.yaml" build_image_config_yaml = "cloudbuild-skaffold-build-image.yaml" trigger_branch_name = ".*" - cloudbuild_private_pool = module.cloudbuild_private_pool.workerpool_id + cloudbuild_private_pool = var.private_worker_pool_id == null ? module.cloudbuild_private_pool.workerpool_id : var.private_worker_pool_id clouddeploy_pipeline_name = local.clouddeploy_pipeline_name skip_provisioners = true labels = var.labels - providers = { - google = google.secure_cicd - } } module "cd_pipeline" { - source = "../../modules/secure-cd" + source = "GoogleCloudPlatform/secure-cicd/google//modules/secure-cd" + version = "~> 1.0" project_id = var.project_id - primary_location = "us-central1" + primary_location = var.region - repository_type = "GITHUB" - github_auth = var.github_auth + repository_type = var.repository_type + github_auth = var.repository_type == "GITHUB" ? var.github_auth : null + gitlab_auth = var.repository_type == "GITLAB" ? var.gitlab_auth : null gar_repo_name = module.ci_pipeline.app_artifact_repo cd_repository = var.cd_repository deploy_branch_clusters = local.deploy_branch_clusters app_deploy_trigger_yaml = "cloudbuild-cd.yaml" cache_bucket_name = module.ci_pipeline.cache_bucket_name - cloudbuild_private_pool = module.cloudbuild_private_pool.workerpool_id + cloudbuild_private_pool = var.private_worker_pool_id == null ? module.cloudbuild_private_pool.workerpool_id : var.private_worker_pool_id clouddeploy_pipeline_name = local.clouddeploy_pipeline_name cloudbuild_service_account = module.ci_pipeline.build_sa_email depends_on = [ module.ci_pipeline ] - providers = { - google = google.secure_cicd - } } # Cloud Build Private Pool module "cloudbuild_private_pool" { - source = "GoogleCloudPlatform/secure-cicd/google//modules/cloudbuild-private-pool" + count = var.private_worker_pool_id == null ? 1 : 0 + source = "GoogleCloudPlatform/secure-cicd/google//modules/cloudbuild-private-pool" + version = "~> 1.0" project_id = var.project_id network_project_id = var.project_id @@ -123,7 +123,8 @@ module "cloudbuild_private_pool" { } module "attestors" { - source = "../../modules/attestor" + source = "GoogleCloudPlatform/secure-cicd/google//modules/attestor" + version = "~> 1.0" project_id = var.project_id diff --git a/examples/standalone_single_project/gke.tf b/examples/standalone_single_project/gke.tf index 65e1e6db..784b4d76 100644 --- a/examples/standalone_single_project/gke.tf +++ b/examples/standalone_single_project/gke.tf @@ -45,7 +45,7 @@ module "gke_cluster" { name = "${var.app_name}-cluster-${each.value}" regional = true region = var.region - network = module.vpc.network_name + network = var.network_name == null ? module.vpc.network_name : var.network_name subnetwork = local.subnets[each.value].subnet_name ip_range_pods = "${local.subnets[each.value].subnet_name}-gke-pods" ip_range_services = "${local.subnets[each.value].subnet_name}-gke-services" @@ -53,6 +53,7 @@ module "gke_cluster" { create_service_account = true enable_binary_authorization = true remove_default_node_pool = true + deletion_protection = false grant_registry_access = true registry_project_ids = [var.project_id] diff --git a/examples/standalone_single_project/network.tf b/examples/standalone_single_project/network.tf index 534042ec..4d6b6d22 100644 --- a/examples/standalone_single_project/network.tf +++ b/examples/standalone_single_project/network.tf @@ -39,6 +39,7 @@ locals { # Private Cluster VPCs module "vpc" { + count = var.network_name == null ? 1 : 0 source = "terraform-google-modules/network/google" version = "~> 18.0" diff --git a/examples/standalone_single_project/outputs.tf b/examples/standalone_single_project/outputs.tf index a2faab43..bd3f01d2 100644 --- a/examples/standalone_single_project/outputs.tf +++ b/examples/standalone_single_project/outputs.tf @@ -24,9 +24,29 @@ output "region" { value = var.region } -output "cloudbuild_cd_repo_name" { - description = "URL of the created CSR app soure repo" - value = "${var.app_name}-cloudbuild-cd-config" +output "gitlab_url" { + description = "The URL of the GitLab instance." + value = var.gitlab_auth.enterprise_host_uri +} + +output "ci_repo_url" { + description = "The URL of the CI repository." + value = var.ci_repository.repository_url +} + +output "ci_repo_name" { + description = "The name of the CI repository." + value = var.ci_repository.repository_name +} + +output "cd_repo_url" { + description = "The URL of the CD repository." + value = var.cd_repository.repository_url +} + +output "cd_repo_name" { + description = "The name of the CD repository." + value = var.cd_repository.repository_name } output "gar_repo" { @@ -35,11 +55,11 @@ output "gar_repo" { } output "neos_tutorial_url" { - value = "https://console.cloud.google.com/products/solutions/catalog?walkthrough_id=solutions-in-console--secure-cicd-pipeline--tour&project=${var.project_id}" + value = "https://console.cloud.google.com/products/solutions/catalog?walkthrough_id=solutions-in-console--secure-cicd-pipeline--tour&project=${var.project_id}" } output "console_walkthrough_link" { - value = "https://shell.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2FGoogleCloudPlatform%2Fterraform-google-secure-cicd.git&cloudshell_git_branch=main&cloudshell_tutorial=examples%2Fstandalone_single_project%2Fwalkthrough.md&project=${var.project_id}" + value = "https://shell.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2FGoogleCloudPlatform%2Fterraform-google-secure-cicd.git&cloudshell_git_branch=main&cloudshell_tutorial=examples%2Fstandalone_single_project%2Fwalkthrough.md&project=${var.project_id}" } output "clouddeploy_pipeline_id" { @@ -50,5 +70,5 @@ output "clouddeploy_pipeline_id" { output "cluster_names" { description = "Comma-separated names of the deployed GKE clusters" # Use join() to create a single string like: "cluster-dev,cluster-qa,cluster-prod" - value = join(",", [for k, v in module.gke_cluster : v.name]) + value = join(",", [for k, v in module.gke_cluster : v.name]) } diff --git a/examples/standalone_single_project/terraform.tfvars b/examples/standalone_single_project/terraform.tfvars new file mode 100644 index 00000000..74df512e --- /dev/null +++ b/examples/standalone_single_project/terraform.tfvars @@ -0,0 +1,35 @@ +project_id = "{PROJECT_ID}" + +app_name = "ci-cd" + +region = "{REGION}" + +private_worker_pool_id = "{REPLACE_WITH_WORKER_POOL_ID}" + +network_name = "{REPLACE_WITH_NETWORK_NAME}" + +gitlab_auth = { + authorizer_credential_secret_id = "REPLACE_WITH_READ_API_SECRET_ID" + read_authorizer_credential_secret_id = "REPLACE_WITH_READ_USER_SECRET_ID" + webhook_secret_id = "REPLACE_WITH_WEBHOOK_SECRET_ID" + secret_project_id = "REPLACE_WITH_SECRET_PROJECT_ID" + + # If you are using a self-hosted instance, you may change the URL below accordingly + enterprise_host_uri = "https://gitlab.com" + # Format is projects/PROJECT/locations/LOCATION/namespaces/NAMESPACE/services/SERVICE + enterprise_service_directory = "REPLACE_WITH_SERVICE_DIRECTORY" + # .pem string + enterprise_ca_certificate = < @@ -25,14 +26,17 @@ Estimated time to complete: To get started, click **Start**. ## Set environment variables -1. Set your active project by running the command below, replcing `your-project-id` with the project ID where you deployed the Secure CI/CD solution: - ``` +1. Set your active project by running the command below, replacing `your-project-id` with the project ID where you deployed the Secure CI/CD solution: + ```bash gcloud config set project your-project-id ``` 1. Set the following environment variables. If you chose custom values when deploying the solution, replace the default values below with the values you chose. - ``` + ```bash export REGION=us-central1 - export APP_NAME=my-app + export APP_NAME=ci-cd + + # Set this to "CSR", "GITHUB", or "GITLAB" depending on how you deployed the blueprint + export REPOSITORY_TYPE="CSR" ``` 1. Run the following commands to set additional variables for the tutorial. If you set the above values correctly, you can run this entire block without modifying it. ```bash @@ -47,22 +51,30 @@ To get started, click **Start**. Click **Next**. -## Create builder image -Your Cloud Shell Terminal should already be opened to the correct directory. If not, click this button to open Cloud Shell: - - -To build your application using Cloud Build, we will create a custom builder image that installs the necessary pacakges to build the application container images using **Skaffold**. +## Configure Terraform Variables +Before deploying the infrastructure, you must configure the `terraform.tfvars` file to match your environment and chosen repository provider. + +1. Open the `terraform.tfvars` file in your editor. +2. Replace the `{PROJECT_ID}` and `{REGION}` placeholders with your actual values. +3. **If using Cloud Source Repositories (CSR):** + * Set `repository_type = "CSR"` + * You can delete or comment out the `gitlab_auth`, `github_auth`, `ci_repository`, and `cd_repository` blocks. +4. **If using GitHub:** + * Set `repository_type = "GITHUB"` + * Provide the `github_auth` block with your Secret Manager paths for your Personal Access Token and App ID. + * Provide the `ci_repository` and `cd_repository` blocks with your GitHub repository names and URLs. +5. **If using GitLab:** + * Set `repository_type = "GITLAB"` + * Provide the `gitlab_auth` block with your Secret Manager paths for your API tokens and webhook secrets. + * Provide the `ci_repository` and `cd_repository` blocks with your GitLab repository names and URLs. + +Once your `terraform.tfvars` file is configured, run `terraform init` and `terraform apply` to provision the infrastructure. Once complete, proceed to the next step. -For convenience, we've pre-configured a Dockerfile to build this image. You can find it in Cloud Shell: `~/cloudshell_open/terraform-google-secure-cicd/examples/private_cluster_cicd/cloud-build-builder/Dockerfile` - -1. Run the following command in Cloud Shell to build the container and store it in Artifact Registry. - ```bash - gcloud builds submit $BLUEPRINT_FOLDER/examples/private_cluster_cicd/cloud-build-builder --project $PROJECT_ID --config=$BLUEPRINT_FOLDER/examples/private_cluster_cicd/cloud-build-builder/cloudbuild-skaffold-build-image.yaml --region=$REGION --substitutions=_DEFAULT_REGION=$REGION,_GAR_REPOSITORY=$GAR_REPOSITORY - ``` +Click **Next**. -You will see the logs for the build process in the Cloud Shell Terminal. Once the build completes, click **Next** to continue. +## Configure Cloud Deploy post-deployment tests (CSR) +*If you deployed the blueprint using `REPOSITORY_TYPE="GITHUB"` or `"GITLAB"`, skip this step and proceed to the next page.* -## Configure Cloud Deploy post-deployment tests In this step, we will configure the post-deployment by pushing a premade configuration file to the `cloudbuild-cd-config` repo in Cloud Source Repositories. 1. Set up the git configuration, replacing the values in quotes with your email address and name. ```bash @@ -92,7 +104,39 @@ In this step, we will configure the post-deployment by pushing a premade configu Click **Next**. -## Push application source code +## Configure Cloud Deploy post-deployment tests (GitHub/GitLab) +*If you deployed the blueprint using `REPOSITORY_TYPE="CSR"`, skip this step and proceed to the next page.* + +In this step, we will configure the post-deployment by pushing a premade configuration file to your linked external CD repository. +1. Set up the git configuration, replacing the values in quotes with your email address and name. + ```bash + git config --global user.email "name@example.com" + git config --global user.name "Your Name" + ``` +1. Change into the workspace folder and create a directory for your CD config: + ```bash + cd $WORKSPACE_FOLDER + mkdir external-cd-config + cd external-cd-config + ``` +1. Copy the Cloud Build configuration to the local folder: + ```bash + cp $BLUEPRINT_FOLDER/build/cloudbuild-cd.yaml . + ``` +1. Initialize the repository and push to your external provider (replace `YOUR_REPO_URL` with the URL you provided in `cd_repository.repository_url` in your `terraform.tfvars`): + ```bash + git init + git checkout -b main + git add . + git commit -m "Add Cloud Build CD configuration" + git remote add origin YOUR_REPO_URL + git push -u origin main + ``` + +Click **Next**. + +## Push application source code (CSR) +*If you deployed the blueprint using `REPOSITORY_TYPE="GITHUB"` or `"GITLAB"`, skip this step and proceed to the next page.* 1. Return to the workspace directory: ```bash @@ -120,7 +164,39 @@ Click **Next**. git push --all google ``` -This will trigger the build phase of the CI/CD pipeline and result in the deployment of the Bank of Anthos application on GKE. The combined build and deploy phases may take up to 40 minutes to complete. To view the pipelines in-progress, click **Next** +This will trigger the build phase of the CI/CD pipeline. Skip the next page and proceed directly to **View pipeline progress**. + +## Push application source code (GitHub/GitLab) +*If you deployed the blueprint using `REPOSITORY_TYPE="CSR"`, you should have completed the previous steps. Skip this page.* + +To use an external provider, you must push the application code and CI configuration files to the CI repository you linked during the Terraform deployment. + +1. Return to the workspace directory: + ```bash + cd $WORKSPACE_FOLDER + ``` +2. Clone the Bank of Anthos sample application locally: + ```bash + git clone --branch v0.5.11 https://github.com/GoogleCloudPlatform/bank-of-anthos.git + cd bank-of-anthos + ``` +3. Copy the required Cloud Build CI configuration and policies into the application folder: + ```bash + cp $BLUEPRINT_FOLDER/build/cloudbuild-ci.yaml . + cp -R $BLUEPRINT_FOLDER/examples/app_cicd/policies ./policies + ``` +4. Initialize a new git repository and push it to your linked external CI repository (replace `YOUR_REPO_URL` with the URL you provided in `ci_repository.repository_url` in your `terraform.tfvars`): + ```bash + rm -rf .git + git init + git checkout -b main + git add . + git commit -m "Initial commit with Cloud Build CI configuration" + git remote add origin YOUR_REPO_URL + git push -u origin main + ``` + +This will trigger the build phase of the CI/CD pipeline and result in the deployment of the Bank of Anthos application on GKE. The combined build and deploy phases may take up to 40 minutes to complete. To view the pipelines in-progress, click **Next**. ## View pipeline progress @@ -158,4 +234,3 @@ Make sure the organization policy `constraints/compute.restrictLoadBalancerCreat Run the following command to make an Organization Policy at the project level to allow load balancer creation: ```bash gcloud resource-manager org-policies allow constraints/compute.restrictLoadBalancerCreationForTypes EXTERNAL_NETWORK_TCP_UDP --project=$PROJECT_ID -``` diff --git a/metadata.display.yaml b/metadata.display.yaml index 28d15c89..2be47332 100644 --- a/metadata.display.yaml +++ b/metadata.display.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,3 +22,5 @@ spec: source: repo: https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd.git sourceType: git + ui: + input: {} diff --git a/metadata.yaml b/metadata.yaml index c2bd3796..3866d13d 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,89 +27,115 @@ spec: description: tagline: Create a CI/CD pipeline that follows security best practices. detailed: |- - Set up a secure CI/CD pipeline that follows best practices for building, scanning, storing, and deploying containers to GKE. + Set up a secure CI/CD pipeline that follows best practices for building, scanning, storing, and deploying containers to GKE, Anthos, or Cloud Run. You can choose whether to deploy your solution through the console directly or download as Terraform from GitHub to deploy later. architecture: - - A developer pushes code for a container-based application to the App Source Code repository in Cloud Source Repositories. This repository must include a skaffold.yaml configuration file, a cloudbuild-ci.yaml configuration file, and templated Kubernetes manifests for the respective Kubernetes deployments, services and other objects. - - Changes to the App Source Code repo will trigger a build of the containers as defined in the skaffold.yaml configuration. - - Metadata about the built containers is stored in the build artifacts Cloud Storage bucket. - - The resulting built containers will be scanned for container structure and CVE’s based on a customer-configurable security policy and stored in an Artifact Registry repository. - - Upon passing all scans, the containers are signed by the Binary Authorization build attestor. - - At the end of the build process, the pipeline creates a new Cloud Deploy release to rollout the newly built container images to the Dev environment. - - After successful deployment, the Cloud Deploy operations Pub/Sub topic receives a confirmation message that triggers the post-deployment checks on the live application via Cloud Build. - - Upon passing the post-deployment application security tests, the containers are signed by the security attestor. - - The Cloud Deploy release is promoted, triggering a rollout to the QA environment. Steps 7-8 repeat, but the containers receive the quality attestor after passing through the QA environment. - - The release is promoted for the final time, creating a rollout to the Prod environment. - - The GKE clusters validate deployed containers based on the respective Binary Authorization policy, requiring additional attestors from the pipeline at each higher environment. - - All Cloud Build and Cloud Deploy processes will run in a private Cloud Build worker pool hosted in a customer-managed VPC. + - A developer pushes new code or a code change for a container-based application to a Source Repository (Cloud Source Repositories, GitHub, or GitLab). + - The code push invokes a Cloud Build trigger. The Cloud Build trigger starts a build in a Cloud Build private worker pool that's hosted in a customer-managed VPC. The outputs of the build are metadata files, Cloud Build logs, and containers. + - The metadata files and the Cloud Build logs are stored in a Cloud Storage bucket. + - The pipeline runs security scans (which you configure) and validates the container structure. When the scans and structure pass, the containers are stored in Artifact Registry. + - The Cloud Build trigger requests an attestation from Binary Authorization that certifies that the required scans passed. The attestation is stored as a cryptographic signature in Binary Authorization. + - "The Cloud Build trigger starts a Cloud Deploy release to roll out the containers to the three environments: developer (Dev), QA, and production (Prod). Each environment is one Kubernetes cluster in its own subnet. All three clusters and subnets are in the same GKE VPC. The last cluster is the production environment." + - As the rollout starts, Cloud Deploy sends the containers to the developer environment. Google Kubernetes Engine (GKE) uses the policy that's defined for the cluster to check the containers’ build attestation in Binary Authorization. When this check passes, GKE deploys the containers into the Kubernetes cluster. + - When Cloud Deploy releases the containers to the developer environment, Cloud Deploy sends a Pub/Sub message that starts the second Cloud Build trigger. This Cloud Build trigger runs post-deployment tests (which you configure) in the developer environment. To run these tests, Cloud Build worker pools communicate with the clusters using Connect Gateway. + - When the post-deployment checks succeed, the Cloud Build trigger requests an attestation from Binary Authorization. The attestation certifies that the required tests from the developer environment passed. + - "Cloud Deploy promotes the release to the second Kubernetes cluster for the QA environment. Steps 7 to 9 run again with some differences: GKE checks for the two attestations before deploying, and after the tests pass, the quality attestation is created." + - Cloud Deploy promotes the release to the production environment, which is the third Kubernetes cluster. GKE uses a policy to check for all three attestations in Binary Authorization. When this check passes, GKE deploys the containers in the Kubernetes cluster for the production environment. content: documentation: - - title: Architecture Diagram - url: https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/blob/main/assets/secure_cicd_pipeline_v2.svg + - title: Architecture Diagram + url: https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/blob/main/assets/secure_cicd_pipeline_v2.svg subBlueprints: - - name: cloudbuild-private-pool - location: modules/cloudbuild-private-pool - - name: secure-cd - location: modules/secure-cd - - name: secure-ci - location: modules/secure-ci - - name: workerpool-gke-ha-vpn - location: modules/workerpool-gke-ha-vpn + - name: attestor + location: modules/attestor + - name: cloudbuild-private-pool + location: modules/cloudbuild-private-pool + - name: secure-cd + location: modules/secure-cd + - name: secure-ci + location: modules/secure-ci + - name: workerpool-gke-ha-vpn + location: modules/workerpool-gke-ha-vpn examples: - - name: app_cicd - location: examples/app_cicd - - name: cloudbuild_private_pool - location: examples/cloudbuild_private_pool - - name: private_cluster_cicd - location: examples/private_cluster_cicd - - name: standalone_single_project - location: examples/standalone_single_project + - name: standalone_single_project + location: examples/standalone_single_project + interfaces: {} requirements: roles: - - level: Project - roles: - - roles/compute.networkAdmin - - roles/container.admin - - roles/binaryauthorization.policyEditor - - roles/resourcemanager.projectIamAdmin - - roles/iam.serviceAccountAdmin - - roles/serviceusage.serviceUsageViewer - - roles/iam.serviceAccountUser - - level: Project - roles: - - roles/artifactregistry.admin - - roles/binaryauthorization.attestorsAdmin - - roles/cloudbuild.builds.builder - - roles/cloudbuild.workerPoolOwner - - roles/clouddeploy.admin - - roles/cloudkms.admin - - roles/cloudkms.publicKeyViewer - - roles/containeranalysis.notes.editor - - roles/compute.networkAdmin - - roles/gkehub.editor - - roles/iam.serviceAccountAdmin - - roles/iam.serviceAccountUser - - roles/pubsub.editor - - roles/serviceusage.serviceUsageAdmin - - roles/source.admin - - roles/storage.admin - - roles/resourcemanager.projectIamAdmin - - roles/viewer + - level: Project + roles: + - roles/compute.networkAdmin + - roles/container.admin + - roles/binaryauthorization.policyEditor + - roles/resourcemanager.projectIamAdmin + - roles/iam.serviceAccountAdmin + - roles/serviceusage.serviceUsageViewer + - roles/iam.serviceAccountUser + - level: Project + roles: + - roles/artifactregistry.admin + - roles/binaryauthorization.attestorsAdmin + - roles/cloudbuild.builds.builder + - roles/cloudbuild.workerPoolOwner + - roles/cloudbuild.admin + - roles/clouddeploy.admin + - roles/cloudkms.admin + - roles/cloudkms.publicKeyViewer + - roles/containeranalysis.notes.editor + - roles/compute.admin + - roles/compute.networkAdmin + - roles/compute.securityAdmin + - roles/gkehub.editor + - roles/iam.serviceAccountAdmin + - roles/iam.serviceAccountUser + - roles/pubsub.editor + - roles/serviceusage.serviceUsageAdmin + - roles/source.admin + - roles/storage.admin + - roles/resourcemanager.projectIamAdmin + - roles/viewer + - roles/secretmanager.admin + - roles/servicedirectory.admin + - roles/dns.admin services: - - cloudresourcemanager.googleapis.com - - cloudbilling.googleapis.com - - clouddeploy.googleapis.com - - storage-api.googleapis.com - - serviceusage.googleapis.com - - cloudbuild.googleapis.com - - containerregistry.googleapis.com - - iamcredentials.googleapis.com - - secretmanager.googleapis.com - - sourcerepo.googleapis.com - - artifactregistry.googleapis.com - - containeranalysis.googleapis.com - - cloudkms.googleapis.com - - binaryauthorization.googleapis.com - - containerscanning.googleapis.com - - servicenetworking.googleapis.com - - pubsub.googleapis.com + - accesscontextmanager.googleapis.com + - aiplatform.googleapis.com + - anthos.googleapis.com + - anthosconfigmanagement.googleapis.com + - anthospolicycontroller.googleapis.com + - apikeys.googleapis.com + - artifactregistry.googleapis.com + - binaryauthorization.googleapis.com + - certificatemanager.googleapis.com + - cloudbilling.googleapis.com + - cloudbuild.googleapis.com + - clouddeploy.googleapis.com + - cloudfunctions.googleapis.com + - cloudkms.googleapis.com + - cloudresourcemanager.googleapis.com + - cloudtrace.googleapis.com + - compute.googleapis.com + - container.googleapis.com + - containeranalysis.googleapis.com + - containerscanning.googleapis.com + - gkehub.googleapis.com + - iam.googleapis.com + - iap.googleapis.com + - mesh.googleapis.com + - modelarmor.googleapis.com + - monitoring.googleapis.com + - multiclusteringress.googleapis.com + - multiclusterservicediscovery.googleapis.com + - networkmanagement.googleapis.com + - networkservices.googleapis.com + - notebooks.googleapis.com + - orgpolicy.googleapis.com + - secretmanager.googleapis.com + - servicedirectory.googleapis.com + - servicemanagement.googleapis.com + - servicenetworking.googleapis.com + - serviceusage.googleapis.com + - sourcerepo.googleapis.com + - sqladmin.googleapis.com + - storage.googleapis.com + - trafficdirector.googleapis.com diff --git a/modules/attestor/README.md b/modules/attestor/README.md new file mode 100644 index 00000000..4321bddd --- /dev/null +++ b/modules/attestor/README.md @@ -0,0 +1,64 @@ +# Binary Authorization Attestors Module + +This module provisions Google Cloud Binary Authorization attestors and the underlying Cloud KMS infrastructure required to securely sign and verify container images. + +Binary Authorization is a service on Google Cloud that provides software supply-chain security for applications that run in the cloud. By using this module, you can create multiple attestors (e.g., for build, security, and quality gates) that ensure only trusted, verified container images are deployed to your Google Kubernetes Engine (GKE) or Cloud Run environments. + +## Features + +This module provisions the following resources: + +* **Cloud KMS Key Ring**: Creates a regional Key Management Service (KMS) key ring to securely store the cryptographic keys used for signing. +* **Binary Authorization Attestors**: Dynamically creates one or more attestors based on a provided list of prefixes. +* **KMS Crypto Keys**: Automatically generates the asymmetric signing keys for each attestor via the official `terraform-google-modules/kubernetes-engine//modules/binary-authorization` sub-module. + +## Prerequisites + +### APIs + +Ensure the following APIs are enabled in your GCP project: + +* `binaryauthorization.googleapis.com` +* `cloudkms.googleapis.com` + +### IAM Roles + +The identity executing this Terraform module needs the following minimum roles (or equivalent permissions) on the target project: + +* `roles/binaryauthorization.attestorsAdmin` (To create and manage the attestors) +* `roles/cloudkms.admin` (To create the KMS key ring and crypto keys) + +## Usage + +Basic usage of this module is as follows: + +```hcl +module "binauthz_attestors" { + source = "path/to/this/module" + + project_id = "my-gcp-project-id" + primary_location = "us-central1" + + # Creates three distinct attestors and their corresponding KMS keys + attestor_names_prefix = ["build", "security", "qa"] +} +``` + + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| attestor\_names\_prefix | A list of Binary Authorization attestors to create. The first attestor specified in this list will be used as the build-attestor during the CI phase. | `list(string)` | n/a | yes | +| primary\_location | Region used for key-ring | `string` | n/a | yes | +| project\_id | Project ID for CICD Pipeline Project | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| binauth\_attestor\_ids | IDs of Attestors | +| binauth\_attestor\_names | Names of Attestors | +| binauth\_attestor\_project\_id | Project ID where attestors get created | + + diff --git a/modules/attestor/metadata.display.yaml b/modules/attestor/metadata.display.yaml new file mode 100644 index 00000000..a2e97302 --- /dev/null +++ b/modules/attestor/metadata.display.yaml @@ -0,0 +1,39 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: blueprints.cloud.google.com/v1alpha1 +kind: BlueprintMetadata +metadata: + name: terraform-google-secure-cicd-attestor-display + annotations: + config.kubernetes.io/local-config: "true" +spec: + info: + title: Binary Authorization Attestors Module + source: + repo: https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd.git + sourceType: git + dir: /modules/attestor + ui: + input: + variables: + attestor_names_prefix: + name: attestor_names_prefix + title: Attestor Names Prefix + primary_location: + name: primary_location + title: Primary Location + project_id: + name: project_id + title: Project Id diff --git a/modules/attestor/metadata.yaml b/modules/attestor/metadata.yaml new file mode 100644 index 00000000..7f02f9df --- /dev/null +++ b/modules/attestor/metadata.yaml @@ -0,0 +1,140 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: blueprints.cloud.google.com/v1alpha1 +kind: BlueprintMetadata +metadata: + name: terraform-google-secure-cicd-attestor + annotations: + config.kubernetes.io/local-config: "true" +spec: + info: + title: Binary Authorization Attestors Module + source: + repo: https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd.git + sourceType: git + dir: /modules/attestor + actuationTool: + flavor: Terraform + version: ">= 1.3" + description: {} + content: + examples: + - name: standalone_single_project + location: examples/standalone_single_project + interfaces: + variables: + - name: project_id + description: Project ID for CICD Pipeline Project + varType: string + required: true + - name: primary_location + description: Region used for key-ring + varType: string + required: true + - name: attestor_names_prefix + description: A list of Binary Authorization attestors to create. The first attestor specified in this list will be used as the build-attestor during the CI phase. + varType: list(string) + required: true + outputs: + - name: binauth_attestor_ids + description: IDs of Attestors + - name: binauth_attestor_names + description: Names of Attestors + - name: binauth_attestor_project_id + description: Project ID where attestors get created + requirements: + roles: + - level: Project + roles: + - roles/compute.networkAdmin + - roles/container.admin + - roles/binaryauthorization.policyEditor + - roles/resourcemanager.projectIamAdmin + - roles/iam.serviceAccountAdmin + - roles/serviceusage.serviceUsageViewer + - roles/iam.serviceAccountUser + - level: Project + roles: + - roles/artifactregistry.admin + - roles/binaryauthorization.attestorsAdmin + - roles/cloudbuild.builds.builder + - roles/cloudbuild.workerPoolOwner + - roles/cloudbuild.admin + - roles/clouddeploy.admin + - roles/cloudkms.admin + - roles/cloudkms.publicKeyViewer + - roles/containeranalysis.notes.editor + - roles/compute.admin + - roles/compute.networkAdmin + - roles/compute.securityAdmin + - roles/gkehub.editor + - roles/iam.serviceAccountAdmin + - roles/iam.serviceAccountUser + - roles/pubsub.editor + - roles/serviceusage.serviceUsageAdmin + - roles/source.admin + - roles/storage.admin + - roles/resourcemanager.projectIamAdmin + - roles/viewer + - roles/secretmanager.admin + - roles/servicedirectory.admin + - roles/dns.admin + services: + - accesscontextmanager.googleapis.com + - aiplatform.googleapis.com + - anthos.googleapis.com + - anthosconfigmanagement.googleapis.com + - anthospolicycontroller.googleapis.com + - apikeys.googleapis.com + - artifactregistry.googleapis.com + - binaryauthorization.googleapis.com + - certificatemanager.googleapis.com + - cloudbilling.googleapis.com + - cloudbuild.googleapis.com + - clouddeploy.googleapis.com + - cloudfunctions.googleapis.com + - cloudkms.googleapis.com + - cloudresourcemanager.googleapis.com + - cloudtrace.googleapis.com + - compute.googleapis.com + - container.googleapis.com + - containeranalysis.googleapis.com + - containerscanning.googleapis.com + - gkehub.googleapis.com + - iam.googleapis.com + - iap.googleapis.com + - mesh.googleapis.com + - modelarmor.googleapis.com + - monitoring.googleapis.com + - multiclusteringress.googleapis.com + - multiclusterservicediscovery.googleapis.com + - networkmanagement.googleapis.com + - networkservices.googleapis.com + - notebooks.googleapis.com + - orgpolicy.googleapis.com + - secretmanager.googleapis.com + - servicedirectory.googleapis.com + - servicemanagement.googleapis.com + - servicenetworking.googleapis.com + - serviceusage.googleapis.com + - sourcerepo.googleapis.com + - sqladmin.googleapis.com + - storage.googleapis.com + - trafficdirector.googleapis.com + providerVersions: + - source: hashicorp/google + version: "> 4, < 8" + - source: hashicorp/random + version: ">= 3.7.2" diff --git a/modules/attestor/versions.tf b/modules/attestor/versions.tf index 0523f03c..0185ed5b 100644 --- a/modules/attestor/versions.tf +++ b/modules/attestor/versions.tf @@ -15,6 +15,7 @@ */ terraform { + required_version = ">= 1.3" required_providers { google = { source = "hashicorp/google" diff --git a/modules/cloudbuild-private-pool/main.tf b/modules/cloudbuild-private-pool/main.tf index bd3c0515..c8ec34e9 100644 --- a/modules/cloudbuild-private-pool/main.tf +++ b/modules/cloudbuild-private-pool/main.tf @@ -14,7 +14,6 @@ * limitations under the License. */ -# Networking config resource "google_project_service" "servicenetworking" { project = var.network_project_id service = "servicenetworking.googleapis.com" @@ -79,7 +78,6 @@ resource "google_compute_network_peering_routes_config" "service_networking_peer ] } -# Cloud Build Worker Pool resource "google_cloudbuild_worker_pool" "pool" { name = var.worker_pool_name project = var.project_id diff --git a/modules/cloudbuild-private-pool/metadata.display.yaml b/modules/cloudbuild-private-pool/metadata.display.yaml index 0f6bc817..24bd9e2f 100644 --- a/modules/cloudbuild-private-pool/metadata.display.yaml +++ b/modules/cloudbuild-private-pool/metadata.display.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ spec: source: repo: https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd.git sourceType: git - dir: cloudbuild-private-pool + dir: /modules/cloudbuild-private-pool ui: input: variables: @@ -50,6 +50,9 @@ spec: worker_address: name: worker_address title: Worker Address + worker_address_prefix_length: + name: worker_address_prefix_length + title: Worker Address Prefix Length worker_pool_name: name: worker_pool_name title: Worker Pool Name diff --git a/modules/cloudbuild-private-pool/metadata.yaml b/modules/cloudbuild-private-pool/metadata.yaml index bcc883d3..8e505a29 100644 --- a/modules/cloudbuild-private-pool/metadata.yaml +++ b/modules/cloudbuild-private-pool/metadata.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,121 +24,153 @@ spec: source: repo: https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd.git sourceType: git - dir: cloudbuild-private-pool - version: 1.1.1 + dir: /modules/cloudbuild-private-pool actuationTool: flavor: Terraform - version: '>= 0.13.0' + version: ~> 1.3 description: {} content: examples: - - name: app_cicd - location: examples/app_cicd - - name: cloudbuild_private_pool - location: examples/cloudbuild_private_pool - - name: private_cluster_cicd - location: examples/private_cluster_cicd - - name: standalone_single_project - location: examples/standalone_single_project + - name: standalone_single_project + location: examples/standalone_single_project interfaces: variables: - - name: create_cloudbuild_network - description: 'Whether to create a VPC for the Cloud Build Worker Pool. Set to false if providing an existing VPC name in ''private_pool_vpc_name'' ' - varType: bool - required: true - - name: labels - description: A set of key/value label pairs to assign to the resources deployed by this blueprint. - varType: map(string) - defaultValue: {} - - name: location - description: Region for Cloud Build worker pool - varType: string - defaultValue: us-central1 - - name: machine_type - description: Machine type for Cloud Build worker pool - varType: string - defaultValue: e2-medium - - name: network_project_id - description: Project ID for Cloud Build network. - varType: string - required: true - - name: private_pool_vpc_name - description: Set the name of the private pool VPC - varType: string - defaultValue: cloudbuild-vpc - - name: project_id - description: Project ID for Cloud Build Private Worker Pool - varType: string - required: true - - name: worker_address - description: 'Choose an address range for the Cloud Build Private Pool workers. example: 10.37.0.0. Do not include a prefix, as it must be /16' - varType: string - defaultValue: 10.37.0.0 - - name: worker_pool_name - description: Name of Cloud Build Worker Pool - varType: string - defaultValue: cloudbuild-private-worker-pool - - name: worker_pool_no_external_ip - description: Whether to disable external IP on the Cloud Build Worker Pool - varType: bool - defaultValue: false - - name: worker_range_name - description: Name of Cloud Build Worker IP address range - varType: string - defaultValue: worker-pool-range + - name: project_id + description: Project ID for Cloud Build Private Worker Pool + varType: string + required: true + - name: network_project_id + description: Project ID for Cloud Build network. + varType: string + required: true + - name: create_cloudbuild_network + description: "Whether to create a VPC for the Cloud Build Worker Pool. Set to false if providing an existing VPC name in 'private_pool_vpc_name' " + varType: bool + required: true + - name: private_pool_vpc_name + description: Set the name of the private pool VPC + varType: string + defaultValue: cloudbuild-vpc + - name: worker_address + description: "Choose an address range for the Cloud Build Private Pool workers. example: 10.37.0.0. Do not include a prefix length." + varType: string + defaultValue: 10.37.0.0 + - name: worker_address_prefix_length + description: Prefix length, such as 24 for /24 or 16 for /16. Must be 24 or lower. + varType: string + defaultValue: "16" + - name: worker_pool_name + description: Name of Cloud Build Worker Pool + varType: string + defaultValue: cloudbuild-private-worker-pool + - name: worker_range_name + description: Name of Cloud Build Worker IP address range + varType: string + defaultValue: worker-pool-range + - name: worker_pool_no_external_ip + description: Whether to disable external IP on the Cloud Build Worker Pool + varType: bool + defaultValue: false + - name: location + description: Region for Cloud Build worker pool + varType: string + defaultValue: us-central1 + - name: machine_type + description: Machine type for Cloud Build worker pool + varType: string + defaultValue: e2-medium + - name: labels + description: A set of key/value label pairs to assign to the resources deployed by this blueprint. + varType: map(string) + defaultValue: {} outputs: - - name: workerpool_id - description: Cloud Build worker pool ID - - name: workerpool_network - description: Self Link for Cloud Build workerpool VPC network - - name: workerpool_range - description: IP Address range for Cloud Build worker pool + - name: workerpool_id + description: Cloud Build worker pool ID + - name: workerpool_network + description: Self Link for Cloud Build workerpool VPC network + - name: workerpool_range + description: IP Address range for Cloud Build worker pool requirements: roles: - - level: Project - roles: - - roles/artifactregistry.admin - - roles/binaryauthorization.attestorsAdmin - - roles/cloudbuild.builds.builder - - roles/cloudbuild.workerPoolOwner - - roles/clouddeploy.admin - - roles/cloudkms.admin - - roles/cloudkms.publicKeyViewer - - roles/containeranalysis.notes.editor - - roles/compute.networkAdmin - - roles/gkehub.editor - - roles/iam.serviceAccountAdmin - - roles/iam.serviceAccountUser - - roles/pubsub.editor - - roles/serviceusage.serviceUsageAdmin - - roles/source.admin - - roles/storage.admin - - roles/resourcemanager.projectIamAdmin - - roles/viewer - - level: Project - roles: - - roles/compute.networkAdmin - - roles/container.admin - - roles/binaryauthorization.policyEditor - - roles/resourcemanager.projectIamAdmin - - roles/iam.serviceAccountAdmin - - roles/serviceusage.serviceUsageViewer - - roles/iam.serviceAccountUser + - level: Project + roles: + - roles/compute.networkAdmin + - roles/container.admin + - roles/binaryauthorization.policyEditor + - roles/resourcemanager.projectIamAdmin + - roles/iam.serviceAccountAdmin + - roles/serviceusage.serviceUsageViewer + - roles/iam.serviceAccountUser + - level: Project + roles: + - roles/artifactregistry.admin + - roles/binaryauthorization.attestorsAdmin + - roles/cloudbuild.builds.builder + - roles/cloudbuild.workerPoolOwner + - roles/cloudbuild.admin + - roles/clouddeploy.admin + - roles/cloudkms.admin + - roles/cloudkms.publicKeyViewer + - roles/containeranalysis.notes.editor + - roles/compute.admin + - roles/compute.networkAdmin + - roles/compute.securityAdmin + - roles/gkehub.editor + - roles/iam.serviceAccountAdmin + - roles/iam.serviceAccountUser + - roles/pubsub.editor + - roles/serviceusage.serviceUsageAdmin + - roles/source.admin + - roles/storage.admin + - roles/resourcemanager.projectIamAdmin + - roles/viewer + - roles/secretmanager.admin + - roles/servicedirectory.admin + - roles/dns.admin services: - - cloudresourcemanager.googleapis.com - - cloudbilling.googleapis.com - - clouddeploy.googleapis.com - - storage-api.googleapis.com - - serviceusage.googleapis.com - - cloudbuild.googleapis.com - - containerregistry.googleapis.com - - iamcredentials.googleapis.com - - secretmanager.googleapis.com - - sourcerepo.googleapis.com - - artifactregistry.googleapis.com - - containeranalysis.googleapis.com - - cloudkms.googleapis.com - - binaryauthorization.googleapis.com - - containerscanning.googleapis.com - - servicenetworking.googleapis.com - - pubsub.googleapis.com + - accesscontextmanager.googleapis.com + - aiplatform.googleapis.com + - anthos.googleapis.com + - anthosconfigmanagement.googleapis.com + - anthospolicycontroller.googleapis.com + - apikeys.googleapis.com + - artifactregistry.googleapis.com + - binaryauthorization.googleapis.com + - certificatemanager.googleapis.com + - cloudbilling.googleapis.com + - cloudbuild.googleapis.com + - clouddeploy.googleapis.com + - cloudfunctions.googleapis.com + - cloudkms.googleapis.com + - cloudresourcemanager.googleapis.com + - cloudtrace.googleapis.com + - compute.googleapis.com + - container.googleapis.com + - containeranalysis.googleapis.com + - containerscanning.googleapis.com + - gkehub.googleapis.com + - iam.googleapis.com + - iap.googleapis.com + - mesh.googleapis.com + - modelarmor.googleapis.com + - monitoring.googleapis.com + - multiclusteringress.googleapis.com + - multiclusterservicediscovery.googleapis.com + - networkmanagement.googleapis.com + - networkservices.googleapis.com + - notebooks.googleapis.com + - orgpolicy.googleapis.com + - secretmanager.googleapis.com + - servicedirectory.googleapis.com + - servicemanagement.googleapis.com + - servicenetworking.googleapis.com + - serviceusage.googleapis.com + - sourcerepo.googleapis.com + - sqladmin.googleapis.com + - storage.googleapis.com + - trafficdirector.googleapis.com + providerVersions: + - source: hashicorp/google + version: ">= 4.3.0, < 8" + - source: hashicorp/google-beta + version: ">= 4.3.0, < 8" diff --git a/modules/cloudbuild-private-pool/versions.tf b/modules/cloudbuild-private-pool/versions.tf index 4f1e1771..3962530d 100644 --- a/modules/cloudbuild-private-pool/versions.tf +++ b/modules/cloudbuild-private-pool/versions.tf @@ -15,15 +15,22 @@ */ terraform { - required_version = ">= 0.13.0" + required_version = "~> 1.3" required_providers { google = { source = "hashicorp/google" - version = ">= 4.3.0 < 8" # google_cloudbuild_worker_pool in GA requires >= 4.3.0 + version = ">= 4.3.0, < 8" } - google_beta = { + google-beta = { source = "hashicorp/google-beta" - version = ">= 4.3.0 < 8" + version = ">= 4.3.0, < 8" } } + + provider_meta "google" { + module_name = "blueprints/terraform/terraform-google-secure-cicd:cloudbuild-private-pool/v1.2.2" + } + provider_meta "google-beta" { + module_name = "blueprints/terraform/terraform-google-secure-cicd:cloudbuild-private-pool/v1.2.2" + } } diff --git a/modules/secure-cd/README.md b/modules/secure-cd/README.md index 6627380d..7bd44ab4 100644 --- a/modules/secure-cd/README.md +++ b/modules/secure-cd/README.md @@ -1,56 +1,133 @@ # Secure CD Module -This module creates a number of Google Cloud Build triggers to facilitate deployment of container images to GKE clusters. -To securely deploy container images, this pipeline focuses on implementing the "Securing deployed artifacts" and "Securing artifact promotions" sections of the [Shifting left on security report](https://cloud.google.com/solutions/shifting-left-on-security). This module implements security best practices by automating the deployment and promotion of updated container images across multiple GKE clusters, and "failing fast" upon security violation. Users configure a GKE cluster's required attesttions and at which stages a container will receive that attestation based on succesful deployment. The [`cloudbuild-cd.yaml`](../../build/cloudbuild-cd.yaml) contains user-customizable post-deployment security checks to prevent promotion upon discovery of a vulnerability. +This module creates a secure Continuous Deployment (CD) pipeline using Google Cloud Deploy, Cloud Build, and Binary Authorization. It facilitates the automated, secure rollout of containerized applications to various target environments (GKE, Anthos, or Cloud Run) while enforcing strict security gates. -This module creates: -* A Cloud Deploy [Pipeline and Targets](https://cloud.google.com/deploy/docs/create-pipeline-targets) for each deployment environment. -* A `clouddeploy-operations` [Pub/Sub topic](https://cloud.google.com/deploy/docs/integrating) to integrate with post-deployment processes. -* [Cloud Build Triggers](https://cloud.google.com/build/docs/automating-builds/create-manage-triggers), to execute post-deployment checks, triggered by messages to the `clouddeploy-operations` topic. -* A [Binary Authorization Policy](https://cloud.google.com/binary-authorization/docs) in each project with a GKE cluster, specifying which attestations are required to run containers in each cluster +To ensure only trusted artifacts are deployed, this module configures Binary Authorization policies on the target clusters, requiring specific attestations (e.g., build, vulnerability scan, QA approval) before a deployment is allowed to proceed. + +## Features + +This module provisions the following resources: + +* **Cloud Deploy Pipeline & Targets**: Creates a delivery pipeline and deployment targets (GKE, Anthos, or Cloud Run) based on your environment definitions. +* **Cloud Build Triggers**: Sets up Pub/Sub-driven Cloud Build triggers that execute the actual deployment manifests (e.g., `cloudbuild-cd.yaml`) when Cloud Deploy initiates a rollout. +* **Source Code Repositories**: Supports native Cloud Source Repositories (CSR) or 2nd-gen Cloud Build repository connections (GitHub/GitLab) for storing your CD configuration files. +* **Binary Authorization Policies**: Enforces `REQUIRE_ATTESTATION` rules on the target clusters, ensuring images are signed by the required attestors before deployment. +* **Pub/Sub Notifications**: Creates a Pub/Sub topic to handle Cloud Deploy operational notifications. +* **IAM & Security**: Provisions a dedicated Cloud Deploy execution Service Account and configures all necessary cross-service IAM bindings (e.g., allowing Cloud Build to impersonate the execution SA, granting GKE developer roles, and allowing the BinAuthz service agent to verify attestations). + +## Prerequisites + +### APIs + +Ensure the following APIs are enabled in your GCP project: + +* `clouddeploy.googleapis.com` +* `cloudbuild.googleapis.com` +* `binaryauthorization.googleapis.com` +* `pubsub.googleapis.com` +* `sourcerepo.googleapis.com` (If using CSR) +* `secretmanager.googleapis.com` (If using GitHub/GitLab auth) + +### IAM Roles + +The identity executing this Terraform module needs the following minimum roles (or equivalent permissions) on the target project: + +* `roles/binaryauthorization.policyEditor` (To configure cluster admission rules) +* `roles/cloudbuild.builds.editor` (To create deployment triggers) +* `roles/cloudbuild.connectionAdmin` (To create 2nd-gen repository connections) +* `roles/clouddeploy.admin` (To create the delivery pipeline and targets) +* `roles/iam.serviceAccountAdmin` (To create the Cloud Deploy execution service account) +* `roles/pubsub.admin` (To create the Cloud Deploy operations topic) +* `roles/resourcemanager.projectIamAdmin` (To grant necessary roles to the execution and build service accounts) +* `roles/source.admin` (To create the CSR repository, if applicable) ## Usage -Basic usage of this module is as follows: + +### Basic Usage (Cloud Source Repositories & GKE) + ```hcl module "cd_pipeline" { - source = "GoogleCloudPlatform/terraform-google-secure-cicd//secure-cd" - - project_id = var.project_id - primary_location = "us-central1" - gar_repo_name = - cloudbuild_cd_repo = "cloudbuild-cd-config-pc" - deploy_branch_clusters = { - dev = { - cluster = "dev-cluster", - project_id = "gke-proj-dev", - location = "us-central1", - required_attestations = ["projects/${var.project_id}/attestors/build-attestor"] - env_attestation = "projects/${var.project_id}/attestors/security-attestor" + source = "GoogleCloudPlatform/terraform-google-secure-cicd//secure-cd" + + project_id = "my-gcp-project-id" + primary_location = "us-central1" + repository_type = "CSR" + csr_cloudbuild_cd_repo = "my-cd-config-repo" + gar_repo_name = "my-app-image-repo" + app_deploy_trigger_yaml = "cloudbuild-cd.yaml" + cache_bucket_name = "my-build-cache-bucket" + clouddeploy_pipeline_name = "my-app-delivery-pipeline" + cloudbuild_service_account = "build-sa@my-gcp-project-id.iam.gserviceaccount.com" + + deploy_branch_clusters = { + "dev" = { + cluster = "dev-cluster" + anthos_membership = "" + project_id = "my-gcp-project-id" + location = "us-central1" + required_attestations = ["projects/my-gcp-project-id/attestors/build-attestor"] + env_attestation = "projects/my-gcp-project-id/attestors/security-attestor" next_env = "qa" + target_type = "gke" }, - qa = { - cluster = "qa-cluster", - project_id = "gke-proj-prod", - location = "us-central1", - required_attestations = ["projects/${var.project_id}/attestors/security-attestor", "projects/${var.project_id}/attestors/build-attestor"] - env_attestation = "projects/${var.project_id}/attestors/quality-attestor" - next_env = "prod" - }, - prod = { - cluster = "prod-cluster", - project_id = "gke-proj-prod", - location = "us-central1", - required_attestations = ["projects/${var.project_id}/attestors/quality-attestor", "projects/${var.project_id}/attestors/security-attestor", "projects/${var.project_id}/attestors/build-attestor"] + "qa" = { + cluster = "qa-cluster" + anthos_membership = "" + project_id = "my-gcp-project-id" + location = "us-central1" + required_attestations = [ + "projects/my-gcp-project-id/attestors/build-attestor", + "projects/my-gcp-project-id/attestors/security-attestor" + ] + env_attestation = "projects/my-gcp-project-id/attestors/qa-attestor" + next_env = "" + target_type = "gke" + } + } +} +``` + +### Usage with GitLab (2nd Gen Repositories) + +```hcl +module "cd_pipeline" { + source = "GoogleCloudPlatform/terraform-google-secure-cicd//secure-cd" + + project_id = "my-gcp-project-id" + primary_location = "us-central1" + repository_type = "GITLAB" + gar_repo_name = "my-app-image-repo" + app_deploy_trigger_yaml = "cloudbuild-cd.yaml" + cache_bucket_name = "my-build-cache-bucket" + clouddeploy_pipeline_name = "my-app-delivery-pipeline" + cloudbuild_service_account = "build-sa@my-gcp-project-id.iam.gserviceaccount.com" + + cd_repository = { + repository_name = "my-gitlab-cd-repo" + repository_url = "https://gitlab.com/my-org/my-cd-configs.git" + } + + gitlab_auth = { + read_authorizer_credential_secret_id = "projects/123/secrets/gitlab-read-token" + authorizer_credential_secret_id = "projects/123/secrets/gitlab-authorizer-token" + webhook_secret_id = "projects/123/secrets/gitlab-webhook-secret" + secret_project_id = "my-secrets-project" + } + + deploy_branch_clusters = { + "prod" = { + cluster = "prod-cluster" + anthos_membership = "" + project_id = "my-gcp-project-id" + location = "us-central1" + required_attestations = ["projects/my-gcp-project-id/attestors/qa-attestor"] env_attestation = "" next_env = "" - }, + target_type = "gke" + } } - app_deploy_trigger_yaml = "cloudbuild-cd.yaml" - cache_bucket_name = } ``` -### Build Configuration -The template [`cloudbuild-cd.yaml`](../../build/cloudbuild-cd.yaml) build configuration specifies the post-deployment checks to run on a Cloud Deploy target upon successful deployment, triggered by Pub/Sub messages from Cloud Deploy. By default, this runs an OWASP ZAProxy scan of any exposed services. Then, the process will automatically promote the given Release to the next environment in Cloud Deploy. Push the configuration file to the root of the `main` branch of the `cloudbuild_cd_repo` to properly configure the automation. ## Inputs @@ -60,15 +137,19 @@ The template [`cloudbuild-cd.yaml`](../../build/cloudbuild-cd.yaml) build config | additional\_substitutions | Parameters to be substituted in the build specification. All keys should begin with an underscore. | `map(string)` | `{}` | no | | app\_deploy\_trigger\_yaml | Name of application cloudbuild yaml file for deployment | `string` | n/a | yes | | cache\_bucket\_name | cloud build artifact bucket name | `string` | n/a | yes | -| cloudbuild\_cd\_repo | Name of repo that stores the Cloud Build CD phase configs - for post-deployment checks | `string` | n/a | yes | +| cd\_repository | The CD repository to configure. The key is a short name for the service. |
object({
repository_name = string
repository_url = string
})
| `null` | no | | cloudbuild\_private\_pool | Cloud Build private pool self-link | `string` | `""` | no | | cloudbuild\_service\_account | Cloud Build SA email address | `string` | n/a | yes | | clouddeploy\_pipeline\_name | Cloud Deploy pipeline name | `string` | n/a | yes | +| csr\_cloudbuild\_cd\_repo | Name of the CSR repo that stores the Cloud Build CD phase configs - for post-deployment checks | `string` | `null` | no | | deploy\_branch\_clusters | mapping of branch names to cluster deployments. target\_type can be one of `gke`, `anthos_cluster`, or `run`. See [clouddeploy\_target Terraform docs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/clouddeploy_target) for more details |
map(object({
cluster = string
anthos_membership = string
project_id = string
location = string
required_attestations = list(string)
env_attestation = string
next_env = string
target_type = string
}))
| `{}` | no | | gar\_repo\_name | Docker artifact registry repo to store app build images | `string` | n/a | yes | +| github\_auth | Authentication configuration for GitHub. Required only if repo\_type is 'GITHUBv2'. |
object({
secret_id = string
app_id_secret_id = string
secret_project_id = string
})
| `null` | no | +| gitlab\_auth | Authentication configuration for GitLab. Required only if repo\_type is 'GITLABv2'. |
object({
read_authorizer_credential_secret_id = string
authorizer_credential_secret_id = string
webhook_secret_id = string
enterprise_host_uri = optional(string)
enterprise_service_directory = optional(string)
enterprise_ca_certificate = optional(string)
secret_project_id = string
})
| `null` | no | | labels | A set of key/value label pairs to assign to the resources deployed by this blueprint. | `map(string)` | `{}` | no | | primary\_location | Region used for key-ring | `string` | n/a | yes | | project\_id | Project ID for CICD Pipeline Project | `string` | n/a | yes | +| repository\_type | The type of the repository. Must be one of 'GITHUB', 'GITLAB', or 'CSR'. | `string` | n/a | yes | ## Outputs diff --git a/modules/secure-cd/build.tf b/modules/secure-cd/build.tf index 0a021281..6928b22b 100644 --- a/modules/secure-cd/build.tf +++ b/modules/secure-cd/build.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,147 @@ # Set up Cloud Deploy notifications # (https://cloud.google.com/deploy/docs/subscribe-deploy-notifications) + resource "google_pubsub_topic" "clouddeploy_topic" { name = local.clouddeploy_pubsub_topic_name project = var.project_id labels = var.labels } -# Trigger post-deploy checks on successful Cloud Deploy rollout -resource "google_cloudbuild_trigger" "deploy_trigger" { - # This for_each loop remains unchanged from your original code. - for_each = { +resource "google_cloudbuild_trigger" "deploy_trigger_gitlab" { + for_each = var.repository_type == "GITLAB" ? { for env, config in var.deploy_branch_clusters : env => config if config.next_env != "" + } : {} + + project = var.project_id + location = var.primary_location + name = "deploy-trigger-gitlab-${each.key}" + service_account = "projects/${var.project_id}/serviceAccounts/${var.cloudbuild_service_account}" + + pubsub_config { + topic = google_pubsub_topic.clouddeploy_topic.id + } + + substitutions = merge( + { + _GAR_REPOSITORY = var.gar_repo_name + _DEFAULT_REGION = each.value.location + _CLUSTER_NAME = each.value.cluster + _ANTHOS_MEMBERSHIP = each.value.anthos_membership + _TARGET_TYPE = each.value.target_type + _CLUSTER_PROJECT = each.value.project_id + _CLOUDBUILD_FILENAME = var.app_deploy_trigger_yaml + _CACHE_BUCKET_NAME = var.cache_bucket_name + _NEXT_ENV = each.value.next_env + _ATTESTOR_NAME = each.value.env_attestation + _CLOUDBUILD_PRIVATE_POOL = var.cloudbuild_private_pool + _CLOUDDEPLOY_PIPELINE_NAME = var.clouddeploy_pipeline_name + _GITLAB_CA_CERT = var.gitlab_auth != null ? var.gitlab_auth.enterprise_ca_certificate : "" + _GITLAB_HOST_URI = var.gitlab_auth != null ? var.gitlab_auth.enterprise_host_uri : "" + _GITLAB_PAT_SECRET_ID = var.gitlab_auth != null ? var.gitlab_auth.authorizer_credential_secret_id : "" + _GITLAB_SECRET_PROJECT = var.gitlab_auth != null ? var.gitlab_auth.secret_project_id : "" + _CD_REPO_URL = var.cd_repository.repository_url + _ACTION_TYPE = "$(body.message.attributes.Action)" + _RESOURCE_TYPE = "$(body.message.attributes.ResourceType)" + _DELIVERY_PIPELINE_ID = "$(body.message.attributes.DeliveryPipelineId)" + _TARGET_ID = "$(body.message.attributes.TargetId)" + _RELEASE_ID = "$(body.message.attributes.ReleaseId)" + }, + var.additional_substitutions + ) + + build { + timeout = "3600s" + tags = ["secure-cicd-cd", "gitlab-parent-trigger"] + logs_bucket = "gs://$_CACHE_BUCKET_NAME/build_logs" + + options { + substitution_option = "ALLOW_LOOSE" + worker_pool = var.cloudbuild_private_pool + } + + available_secrets { + secret_manager { + env = "_GITLAB_TOKEN" + version_name = "$_GITLAB_PAT_SECRET_ID/versions/latest" + } + } + + step { + name = "gcr.io/cloud-builders/gcloud" + id = "manual-fetch-source" + entrypoint = "bash" + secret_env = ["_GITLAB_TOKEN"] + args = [ + "-c", + <<-EOT + if [ -n "$_GITLAB_CA_CERT" ]; then + echo "Configuring git with GitLab CA certificate..." + echo "$_GITLAB_CA_CERT" > /tmp/gitlab-ca.crt + _GITLAB_HOST=$$(echo "$_GITLAB_HOST_URI" | sed 's|https://||g') + git config --global "http.https://$$_GITLAB_HOST/.sslCAInfo" /tmp/gitlab-ca.crt + echo "GitLab CA certificate configured." + fi + + _GITLAB_HOST=$$(echo "$_GITLAB_HOST_URI" | sed 's|https://||g') + _REPO_PATH=$$(echo "$_CD_REPO_URL" | sed "s|https://$$_GITLAB_HOST/||g") + + _AUTH_URL="https://oauth2:$$_GITLAB_TOKEN@$$_GITLAB_HOST/$$_REPO_PATH" + + echo "Cloning repository..." + git clone "$$_AUTH_URL" /workspace + cd /workspace + git checkout main + + echo "Submitting child build using $_CLOUDBUILD_FILENAME..." + + _SUBS="_GAR_REPOSITORY=$_GAR_REPOSITORY" + _SUBS+=",_DEFAULT_REGION=$_DEFAULT_REGION" + _SUBS+=",_CLUSTER_NAME=$_CLUSTER_NAME" + _SUBS+=",_ANTHOS_MEMBERSHIP=$_ANTHOS_MEMBERSHIP" + _SUBS+=",_TARGET_TYPE=$_TARGET_TYPE" + _SUBS+=",_CLUSTER_PROJECT=$_CLUSTER_PROJECT" + _SUBS+=",_CLOUDBUILD_FILENAME=$_CLOUDBUILD_FILENAME" + _SUBS+=",_CACHE_BUCKET_NAME=$_CACHE_BUCKET_NAME" + _SUBS+=",_NEXT_ENV=$_NEXT_ENV" + _SUBS+=",_ATTESTOR_NAME=$_ATTESTOR_NAME" + _SUBS+=",_CLOUDBUILD_PRIVATE_POOL=$_CLOUDBUILD_PRIVATE_POOL" + _SUBS+=",_CLOUDDEPLOY_PIPELINE_NAME=$_CLOUDDEPLOY_PIPELINE_NAME" + _SUBS+=",_ACTION_TYPE=$_ACTION_TYPE" + _SUBS+=",_RESOURCE_TYPE=$_RESOURCE_TYPE" + _SUBS+=",_DELIVERY_PIPELINE_ID=$_DELIVERY_PIPELINE_ID" + _SUBS+=",_TARGET_ID=$_TARGET_ID" + _SUBS+=",_RELEASE_ID=$_RELEASE_ID" + + %{for k, v in var.additional_substitutions~} + _SUBS+=",${k}=$$${k}" + %{endfor~} + + gcloud builds submit \ + --service-account="projects/${var.project_id}/serviceAccounts/${var.cloudbuild_service_account}" \ + --config=$_CLOUDBUILD_FILENAME \ + --region=$_DEFAULT_REGION \ + --substitutions="$$_SUBS" \ + --project=${var.project_id} + EOT + ] + } } + filter = "_RESOURCE_TYPE.matches('Rollout') && _ACTION_TYPE.matches('Succeed') && _DELIVERY_PIPELINE_ID.matches('${var.clouddeploy_pipeline_name}') && _TARGET_ID.matches('${google_clouddeploy_target.deploy_target[each.key].name}')" +} + + + + +resource "google_cloudbuild_trigger" "deploy_trigger" { + for_each = var.repository_type != "GITLAB" ? { + for env, config in var.deploy_branch_clusters : env => config + if config.next_env != "" + } : {} + + project = var.project_id location = var.primary_location name = each.value.target_type == "gke" ? "deploy-trigger-${each.value.cluster}" : each.value.target_type == "anthos_cluster" ? "deploy-trigger-${each.value.anthos_membership}" : "deploy-trigger-${each.key}" @@ -37,20 +164,16 @@ resource "google_cloudbuild_trigger" "deploy_trigger" { service_account = "projects/${var.project_id}/serviceAccounts/${var.cloudbuild_service_account}" - # This Pub/Sub trigger configuration remains unchanged. pubsub_config { topic = google_pubsub_topic.clouddeploy_topic.id } - # This block now dynamically points to the correct repository source. source_to_build { - uri = local.cd_repo_source.uri ref = "main" repo_type = local.cd_repo_source.repo_type repository = local.cd_repo_source.repository } - # The substitutions block is complete and unchanged. substitutions = merge( { _GAR_REPOSITORY = var.gar_repo_name @@ -75,6 +198,5 @@ resource "google_cloudbuild_trigger" "deploy_trigger" { var.additional_substitutions ) - # The filter remains complete and unchanged. filter = "_RESOURCE_TYPE.matches('Rollout') && _ACTION_TYPE.matches('Succeed') && _DELIVERY_PIPELINE_ID.matches('${var.clouddeploy_pipeline_name}') && _TARGET_ID.matches('${google_clouddeploy_target.deploy_target[each.key].name}')" } diff --git a/modules/secure-cd/iam.tf b/modules/secure-cd/iam.tf index 125d6645..f7ef8c5b 100644 --- a/modules/secure-cd/iam.tf +++ b/modules/secure-cd/iam.tf @@ -1,5 +1,5 @@ /** - * Copyright 2022 Google LLC + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,8 +29,6 @@ locals { ] } -# Cloud Deploy Execution Service Account -# https://cloud.google.com/deploy/docs/cloud-deploy-service-account#execution_service_account resource "google_service_account" "clouddeploy_execution_sa" { project = var.project_id account_id = "clouddeploy-execution-sa" @@ -45,11 +43,10 @@ resource "google_project_iam_member" "cd_sa_iam" { member = "serviceAccount:${google_service_account.clouddeploy_execution_sa.email}" } -# Cloud Deploy Service Agent resource "google_project_service_identity" "clouddeploy_service_agent" { provider = google-beta - project = var.project_id - service = "clouddeploy.googleapis.com" + project = var.project_id + service = "clouddeploy.googleapis.com" } resource "google_project_iam_member" "clouddeploy_service_agent_role" { @@ -58,14 +55,12 @@ resource "google_project_iam_member" "clouddeploy_service_agent_role" { member = "serviceAccount:${google_project_service_identity.clouddeploy_service_agent.email}" } -# IAM membership for Cloud Build SA to act as Cloud Deploy Execution SA resource "google_service_account_iam_member" "cloudbuild_clouddeploy_impersonation" { service_account_id = google_service_account.clouddeploy_execution_sa.name role = "roles/iam.serviceAccountUser" member = "serviceAccount:${var.cloudbuild_service_account}" } -# IAM membership for Cloud Deploy Execution SA deploy to GKE resource "google_project_iam_member" "clouddeploy_gke_dev" { for_each = var.deploy_branch_clusters project = each.value.project_id @@ -73,7 +68,6 @@ resource "google_project_iam_member" "clouddeploy_gke_dev" { member = "serviceAccount:${google_service_account.clouddeploy_execution_sa.email}" } -# IAM membership for Cloud Build SA to deploy to GKE resource "google_project_iam_member" "cloudbuild_gke_dev" { for_each = var.deploy_branch_clusters project = each.value.project_id @@ -81,9 +75,6 @@ resource "google_project_iam_member" "cloudbuild_gke_dev" { member = "serviceAccount:${var.cloudbuild_service_account}" } -# IAM grants for deploying to GKE via Connect Gateway -# https://cloud.google.com/anthos/multicluster-management/gateway/setup#grant_roles_for_access_through_kubectl -# Cloud Deploy Execution SA deploy to cluster resource "google_project_iam_member" "clouddeploy_gkehub_viewer" { for_each = var.deploy_branch_clusters project = each.value.project_id @@ -97,7 +88,6 @@ resource "google_project_iam_member" "clouddeploy_gkehub_gatewayadmin" { member = "serviceAccount:${google_service_account.clouddeploy_execution_sa.email}" } -# Cloud Build SA to deploy to cluster resource "google_project_iam_member" "cloudbuild_gkehub_viewer" { for_each = var.deploy_branch_clusters project = each.value.project_id @@ -111,7 +101,6 @@ resource "google_project_iam_member" "cloudbuild_gkehub_gatewayadmin" { member = "serviceAccount:${var.cloudbuild_service_account}" } -# IAM membership for Binary Authorization service agents in GKE projects on attestors resource "google_project_service_identity" "binauth_service_agent" { provider = google-beta for_each = var.deploy_branch_clusters @@ -127,3 +116,18 @@ resource "google_binary_authorization_attestor_iam_member" "binauthz_verifier" { role = "roles/binaryauthorization.attestorsVerifier" member = "serviceAccount:${google_project_service_identity.binauth_service_agent[each.value.env].email}" } + +resource "google_service_account_iam_member" "sa_impersonate_self" { + service_account_id = "projects/${var.project_id}/serviceAccounts/${var.cloudbuild_service_account}" + role = "roles/iam.serviceAccountUser" + member = "serviceAccount:${var.cloudbuild_service_account}" +} + +resource "google_secret_manager_secret_iam_member" "gitlab_pat_accessor" { + count = var.repository_type == "GITLAB" && var.gitlab_auth != null ? 1 : 0 + + secret_id = var.gitlab_auth.authorizer_credential_secret_id + + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${var.cloudbuild_service_account}" +} diff --git a/modules/secure-cd/main.tf b/modules/secure-cd/main.tf index 5fdfdffb..6b681dc9 100644 --- a/modules/secure-cd/main.tf +++ b/modules/secure-cd/main.tf @@ -1,5 +1,5 @@ /** - * Copyright 2021 Google LLC + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -119,7 +119,6 @@ resource "google_clouddeploy_delivery_pipeline" "pipeline" { } } -# Binary Authorization Policy resource "google_binary_authorization_policy" "deployment_policy" { for_each = local.binary_authorization_map project = each.key diff --git a/modules/secure-cd/metadata.display.yaml b/modules/secure-cd/metadata.display.yaml index 0acbac23..867f63ce 100644 --- a/modules/secure-cd/metadata.display.yaml +++ b/modules/secure-cd/metadata.display.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ spec: source: repo: https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd.git sourceType: git - dir: secure-cd + dir: /modules/secure-cd ui: input: variables: @@ -35,6 +35,9 @@ spec: cache_bucket_name: name: cache_bucket_name title: Cache Bucket Name + cd_repository: + name: cd_repository + title: Cd Repository cloudbuild_cd_repo: name: cloudbuild_cd_repo title: Cloudbuild Cd Repo @@ -47,12 +50,21 @@ spec: clouddeploy_pipeline_name: name: clouddeploy_pipeline_name title: Clouddeploy Pipeline Name + csr_cloudbuild_cd_repo: + name: csr_cloudbuild_cd_repo + title: Csr Cloudbuild Cd Repo deploy_branch_clusters: name: deploy_branch_clusters title: Deploy Branch Clusters gar_repo_name: name: gar_repo_name title: Gar Repo Name + github_auth: + name: github_auth + title: Github Auth + gitlab_auth: + name: gitlab_auth + title: Gitlab Auth labels: name: labels title: Labels @@ -62,3 +74,6 @@ spec: project_id: name: project_id title: Project Id + repository_type: + name: repository_type + title: Repository Type diff --git a/modules/secure-cd/metadata.yaml b/modules/secure-cd/metadata.yaml index 46a207db..500ac59b 100644 --- a/modules/secure-cd/metadata.yaml +++ b/modules/secure-cd/metadata.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,137 +24,195 @@ spec: source: repo: https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd.git sourceType: git - dir: secure-cd - version: 1.1.1 + dir: /modules/secure-cd actuationTool: flavor: Terraform - version: '>= 1.0' + version: ~> 1.3 description: {} content: examples: - - name: app_cicd - location: examples/app_cicd - - name: cloudbuild_private_pool - location: examples/cloudbuild_private_pool - - name: private_cluster_cicd - location: examples/private_cluster_cicd - - name: standalone_single_project - location: examples/standalone_single_project + - name: standalone_single_project + location: examples/standalone_single_project interfaces: variables: - - name: additional_substitutions - description: Parameters to be substituted in the build specification. All keys should begin with an underscore. - varType: map(string) - defaultValue: {} - - name: app_deploy_trigger_yaml - description: Name of application cloudbuild yaml file for deployment - varType: string - required: true - - name: cache_bucket_name - description: cloud build artifact bucket name - varType: string - required: true - - name: cloudbuild_cd_repo - description: Name of repo that stores the Cloud Build CD phase configs - for post-deployment checks - varType: string - required: true - - name: cloudbuild_private_pool - description: Cloud Build private pool self-link - varType: string - defaultValue: "" - - name: cloudbuild_service_account - description: Cloud Build SA email address - varType: string - required: true - - name: clouddeploy_pipeline_name - description: Cloud Deploy pipeline name - varType: string - required: true - - name: deploy_branch_clusters - description: mapping of branch names to cluster deployments. target_type can be one of `gke`, `anthos_cluster`, or `run`. See [clouddeploy_target Terraform docs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/clouddeploy_target) for more details - varType: |- - map(object({ - cluster = string - anthos_membership = string - project_id = string - location = string - required_attestations = list(string) - env_attestation = string - next_env = string - target_type = string - })) - defaultValue: {} - - name: gar_repo_name - description: Docker artifact registry repo to store app build images - varType: string - required: true - - name: labels - description: A set of key/value label pairs to assign to the resources deployed by this blueprint. - varType: map(string) - defaultValue: {} - - name: primary_location - description: Region used for key-ring - varType: string - required: true - - name: project_id - description: Project ID for CICD Pipeline Project - varType: string - required: true + - name: project_id + description: Project ID for CICD Pipeline Project + varType: string + required: true + - name: primary_location + description: Region used for key-ring + varType: string + required: true + - name: csr_cloudbuild_cd_repo + description: Name of the CSR repo that stores the Cloud Build CD phase configs - for post-deployment checks + varType: string + - name: gar_repo_name + description: Docker artifact registry repo to store app build images + varType: string + required: true + - name: app_deploy_trigger_yaml + description: Name of application cloudbuild yaml file for deployment + varType: string + required: true + - name: deploy_branch_clusters + description: mapping of branch names to cluster deployments. target_type can be one of `gke`, `anthos_cluster`, or `run`. See [clouddeploy_target Terraform docs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/clouddeploy_target) for more details + varType: |- + map(object({ + cluster = string + anthos_membership = string + project_id = string + location = string + required_attestations = list(string) + env_attestation = string + next_env = string + target_type = string + })) + defaultValue: {} + - name: cache_bucket_name + description: cloud build artifact bucket name + varType: string + required: true + - name: additional_substitutions + description: Parameters to be substituted in the build specification. All keys should begin with an underscore. + varType: map(string) + defaultValue: {} + - name: cloudbuild_private_pool + description: Cloud Build private pool self-link + varType: string + defaultValue: "" + - name: clouddeploy_pipeline_name + description: Cloud Deploy pipeline name + varType: string + required: true + - name: labels + description: A set of key/value label pairs to assign to the resources deployed by this blueprint. + varType: map(string) + defaultValue: {} + - name: cloudbuild_service_account + description: Cloud Build SA email address + varType: string + required: true + - name: repository_type + description: The type of the repository. Must be one of 'GITHUB', 'GITLAB', or 'CSR'. + varType: string + required: true + - name: cd_repository + description: The CD repository to configure. The key is a short name for the service. + varType: |- + object({ + repository_name = string + repository_url = string + }) + - name: github_auth + description: Authentication configuration for GitHub. Required only if repo_type is 'GITHUBv2'. + varType: |- + object({ + secret_id = string + app_id_secret_id = string + secret_project_id = string + }) + - name: gitlab_auth + description: Authentication configuration for GitLab. Required only if repo_type is 'GITLABv2'. + varType: |- + object({ + read_authorizer_credential_secret_id = string + authorizer_credential_secret_id = string + webhook_secret_id = string + enterprise_host_uri = optional(string) + enterprise_service_directory = optional(string) + enterprise_ca_certificate = optional(string) + secret_project_id = string + }) outputs: - - name: binauthz_policy_required_attestations - description: Binary Authorization policy required attestation in GKE projects - - name: clouddeploy_delivery_pipeline_id - description: ID of the Cloud Deploy delivery pipeline - - name: clouddeploy_target_id - description: ID(s) of Cloud Deploy targets - - name: deploy_trigger_names - description: Names of CD Cloud Build triggers + - name: binauthz_policy_required_attestations + description: Binary Authorization policy required attestation in GKE projects + - name: clouddeploy_delivery_pipeline_id + description: ID of the Cloud Deploy delivery pipeline + - name: clouddeploy_target_id + description: ID(s) of Cloud Deploy targets + - name: deploy_trigger_names + description: Names of CD Cloud Build triggers requirements: roles: - - level: Project - roles: - - roles/compute.networkAdmin - - roles/container.admin - - roles/binaryauthorization.policyEditor - - roles/resourcemanager.projectIamAdmin - - roles/iam.serviceAccountAdmin - - roles/serviceusage.serviceUsageViewer - - roles/iam.serviceAccountUser - - level: Project - roles: - - roles/artifactregistry.admin - - roles/binaryauthorization.attestorsAdmin - - roles/cloudbuild.builds.builder - - roles/cloudbuild.workerPoolOwner - - roles/clouddeploy.admin - - roles/cloudkms.admin - - roles/cloudkms.publicKeyViewer - - roles/containeranalysis.notes.editor - - roles/compute.networkAdmin - - roles/gkehub.editor - - roles/iam.serviceAccountAdmin - - roles/iam.serviceAccountUser - - roles/pubsub.editor - - roles/serviceusage.serviceUsageAdmin - - roles/source.admin - - roles/storage.admin - - roles/resourcemanager.projectIamAdmin - - roles/viewer + - level: Project + roles: + - roles/compute.networkAdmin + - roles/container.admin + - roles/binaryauthorization.policyEditor + - roles/resourcemanager.projectIamAdmin + - roles/iam.serviceAccountAdmin + - roles/serviceusage.serviceUsageViewer + - roles/iam.serviceAccountUser + - level: Project + roles: + - roles/artifactregistry.admin + - roles/binaryauthorization.attestorsAdmin + - roles/cloudbuild.builds.builder + - roles/cloudbuild.workerPoolOwner + - roles/cloudbuild.admin + - roles/clouddeploy.admin + - roles/cloudkms.admin + - roles/cloudkms.publicKeyViewer + - roles/containeranalysis.notes.editor + - roles/compute.admin + - roles/compute.networkAdmin + - roles/compute.securityAdmin + - roles/gkehub.editor + - roles/iam.serviceAccountAdmin + - roles/iam.serviceAccountUser + - roles/pubsub.editor + - roles/serviceusage.serviceUsageAdmin + - roles/source.admin + - roles/storage.admin + - roles/resourcemanager.projectIamAdmin + - roles/viewer + - roles/secretmanager.admin + - roles/servicedirectory.admin + - roles/dns.admin services: - - cloudresourcemanager.googleapis.com - - cloudbilling.googleapis.com - - clouddeploy.googleapis.com - - storage-api.googleapis.com - - serviceusage.googleapis.com - - cloudbuild.googleapis.com - - containerregistry.googleapis.com - - iamcredentials.googleapis.com - - secretmanager.googleapis.com - - sourcerepo.googleapis.com - - artifactregistry.googleapis.com - - containeranalysis.googleapis.com - - cloudkms.googleapis.com - - binaryauthorization.googleapis.com - - containerscanning.googleapis.com - - servicenetworking.googleapis.com - - pubsub.googleapis.com + - accesscontextmanager.googleapis.com + - aiplatform.googleapis.com + - anthos.googleapis.com + - anthosconfigmanagement.googleapis.com + - anthospolicycontroller.googleapis.com + - apikeys.googleapis.com + - artifactregistry.googleapis.com + - binaryauthorization.googleapis.com + - certificatemanager.googleapis.com + - cloudbilling.googleapis.com + - cloudbuild.googleapis.com + - clouddeploy.googleapis.com + - cloudfunctions.googleapis.com + - cloudkms.googleapis.com + - cloudresourcemanager.googleapis.com + - cloudtrace.googleapis.com + - compute.googleapis.com + - container.googleapis.com + - containeranalysis.googleapis.com + - containerscanning.googleapis.com + - gkehub.googleapis.com + - iam.googleapis.com + - iap.googleapis.com + - mesh.googleapis.com + - modelarmor.googleapis.com + - monitoring.googleapis.com + - multiclusteringress.googleapis.com + - multiclusterservicediscovery.googleapis.com + - networkmanagement.googleapis.com + - networkservices.googleapis.com + - notebooks.googleapis.com + - orgpolicy.googleapis.com + - secretmanager.googleapis.com + - servicedirectory.googleapis.com + - servicemanagement.googleapis.com + - servicenetworking.googleapis.com + - serviceusage.googleapis.com + - sourcerepo.googleapis.com + - sqladmin.googleapis.com + - storage.googleapis.com + - trafficdirector.googleapis.com + providerVersions: + - source: hashicorp/google + version: "> 4, < 8" + - source: hashicorp/google-beta + version: "> 4, < 8" diff --git a/modules/secure-cd/outputs.tf b/modules/secure-cd/outputs.tf index d7af95ff..c8bc38cf 100644 --- a/modules/secure-cd/outputs.tf +++ b/modules/secure-cd/outputs.tf @@ -1,5 +1,5 @@ /** - * Copyright 2021 Google LLC + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/modules/secure-cd/variables.tf b/modules/secure-cd/variables.tf index f9f57edc..73bc6587 100644 --- a/modules/secure-cd/variables.tf +++ b/modules/secure-cd/variables.tf @@ -1,5 +1,5 @@ /** - * Copyright 2021 Google LLC + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/modules/secure-cd/versions.tf b/modules/secure-cd/versions.tf index 8e38689f..dea539f8 100644 --- a/modules/secure-cd/versions.tf +++ b/modules/secure-cd/versions.tf @@ -1,5 +1,5 @@ /** - * Copyright 2021 Google LLC + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ terraform { + required_version = "~> 1.3" required_providers { google = { source = "hashicorp/google" @@ -25,4 +26,11 @@ terraform { version = "> 4, < 8" } } + + provider_meta "google" { + module_name = "blueprints/terraform/terraform-google-secure-cicd:secure-cd/v1.2.2" + } + provider_meta "google-beta" { + module_name = "blueprints/terraform/terraform-google-secure-cicd:secure-cd/v1.2.2" + } } diff --git a/modules/secure-ci/README.md b/modules/secure-ci/README.md index 425d80a2..66bb199a 100644 --- a/modules/secure-ci/README.md +++ b/modules/secure-ci/README.md @@ -1,34 +1,97 @@ # Secure CI Module -This module creates a number of Google Cloud Source Repositories and a Cloud Build Trigger to facilitate a software build process with security checks. -To securely build container images, this pipeline focuses on implementing the "Securing artifacts before deployment" section of the [Shifting left on security report](https://cloud.google.com/solutions/shifting-left-on-security). The modules implements security best practices such as: using artifact repositories to store immutable container images, running container analysis scans to test container structure and check for CVEs, and signing approved images with Binary Authorization attestors to enable secure deployment. +This module creates a secure Continuous Integration (CI) pipeline using Google Cloud Build and Artifact Registry. It facilitates a software build process with built-in security checks, supporting native Cloud Source Repositories (CSR) as well as 2nd-gen Cloud Build connections for GitHub and GitLab. -This module creates: -* Cloud Source Repositories for: application source code, template Kubernetes manifests, and hydrated Kubernetes manifests -* Cloud Build Trigger to execute the integration pipeline upon pushing code changes to the application source code repository -* Storage Bucket to store build artifacts -* Artifact Registry repository for built container images -* Cloud KMS keyring to support attestors -* Binary Authorization attestors +To securely build container images, this pipeline focuses on implementing the "Securing artifacts before deployment" section of the [Shifting left on security report](https://cloud.google.com/solutions/shifting-left-on-security). The module implements security best practices such as: using Artifact Registry to store immutable container images, running container analysis scans to test container structure and check for CVEs, and preparing images for secure deployment. + +## Features + +This module provisions the following resources: + +* **Source Code Repositories**: Supports native Cloud Source Repositories (CSR) or 2nd-gen Cloud Build repository connections for GitHub and GitLab. +* **Cloud Build Triggers**: Executes the integration pipeline upon pushing code changes to the application source code repository. +* **Artifact Registry**: A Docker repository for storing built container images. +* **Storage Bucket**: A GCS bucket to store build artifacts and cache, with optional Customer-Managed Encryption Key (CMEK) support. +* **Custom Builder Image**: Automatically builds and pushes a custom Skaffold builder image to Artifact Registry for use in the pipeline. +* **IAM & Security**: Provisions a dedicated least-privilege Service Account for Cloud Build execution and supports Cloud Build Private Pools. + +## Prerequisites + +### APIs + +Ensure the following APIs are enabled in your GCP project: + +* `cloudbuild.googleapis.com` +* `artifactregistry.googleapis.com` +* `sourcerepo.googleapis.com` (If using CSR) +* `containerscanning.googleapis.com` +* `secretmanager.googleapis.com` (If using GitHub/GitLab auth) + +### IAM Roles + +The identity executing this Terraform module needs the following minimum roles (or equivalent permissions) on the target project: + +* `roles/artifactregistry.admin` (To create the Artifact Registry repository) +* `roles/cloudbuild.builds.editor` (To create triggers and submit the custom builder image build) +* `roles/cloudbuild.connectionAdmin` (To create 2nd-gen repository connections) +* `roles/iam.serviceAccountAdmin` (To create the `build-sa` service account) +* `roles/iam.serviceAccountUser` (To act as the `build-sa` when submitting the custom builder image) +* `roles/resourcemanager.projectIamAdmin` (To grant roles to the `build-sa` service account) +* `roles/secretmanager.secretAccessor` (To allow Cloud Build to access Git authentication secrets) +* `roles/source.admin` (To create the CSR repository, if applicable) +* `roles/storage.admin` (To create the cache bucket) +* `roles/serviceusage.serviceUsageAdmin` (To enable the Container Scanning API) ## Usage -Basic usage of this module is as follows: + +### Basic Usage (Cloud Source Repositories) + +```hcl +module "ci_pipeline" { + source = "GoogleCloudPlatform/terraform-google-secure-cicd//secure-ci" + + project_id = "my-gcp-project-id" + primary_location = "us-central1" + repository_type = "CSR" + csr_app_source_repo = "my-app-source" + attestor_names_prefix = ["build", "security", "quality"] + app_build_trigger_yaml = "cloudbuild-ci.yaml" + build_image_config_yaml = "cloudbuild-skaffold-build-image.yaml" + trigger_branch_name = "main" +} +``` + +### Usage with GitHub (2nd Gen Repositories) + ```hcl module "ci_pipeline" { source = "GoogleCloudPlatform/terraform-google-secure-cicd//secure-ci" - project_id = var.project_id + project_id = "my-gcp-project-id" primary_location = "us-central1" + repository_type = "GITHUB" + + ci_repository = { + repository_name = "my-github-repo" + repository_url = "https://github.com/my-org/my-app.git" + } + + github_auth = { + secret_id = "projects/123/secrets/github-pat" + app_id_secret_id = "projects/123/secrets/github-app-id" + secret_project_id = "my-secrets-project" + } + attestor_names_prefix = ["build", "security", "quality"] app_build_trigger_yaml = "cloudbuild-ci.yaml" - runner_build_folder = "../../../examples/app_cicd/cloud-build-builder" build_image_config_yaml = "cloudbuild-skaffold-build-image.yaml" - trigger_branch_name = ".*" + trigger_branch_name = "main" } ``` + ### Build Configuration -The template [`cloudbuild-ci.yaml`](../../build/cloudbuild-ci.yaml) build configuration runs container structure and vulnerability scans, and creates Binary Authorization attestations based on their results. Add the configuration file to the root of the `app_source_repo` to trigger the CI phase. +The template `cloudbuild-ci.yaml` build configuration runs container structure and vulnerability scans, and creates Binary Authorization attestations based on their results. Add the configuration file to the root of your source repository to trigger the CI phase. ## Inputs @@ -37,18 +100,22 @@ The template [`cloudbuild-ci.yaml`](../../build/cloudbuild-ci.yaml) build config |------|-------------|------|---------|:--------:| | additional\_substitutions | Parameters to be substituted in the build specification. All keys should begin with an underscore. | `map(string)` | `{}` | no | | app\_build\_trigger\_yaml | Name of application cloudbuild yaml file | `string` | n/a | yes | -| app\_source\_repo | Name of repo that contains app source code along with cloudbuild yaml | `string` | `"app-source"` | no | | attestor\_names\_prefix | A list of Binary Authorization attestors to create. The first attestor specified in this list will be used as the build-attestor during the CI phase. | `list(string)` | n/a | yes | +| bucket\_kms\_key | KMS Key id to be used to encrypt bucket. | `string` | `null` | no | | build\_image\_config\_yaml | Name of image builder yaml file | `string` | n/a | yes | | cache\_bucket\_name | Name of cloudbuild artifact and cache GCS bucket | `string` | `""` | no | -| cloudbuild\_cd\_repo | Name of repo that stores the Cloud Build CD phase configs - for post-deployment checks | `string` | `"cloudbuild-cd-config"` | no | +| ci\_repository | The CI repository to configure. The key is a short name for the service. |
object({
repository_name = string
repository_url = string
})
| `null` | no | | cloudbuild\_private\_pool | Cloud Build private pool self-link | `string` | `""` | no | | cloudbuild\_service\_account\_roles | IAM roles given to the Cloud Build service account to enable security scanning operations | `list(string)` |
[
"roles/artifactregistry.admin",
"roles/binaryauthorization.attestorsVerifier",
"roles/cloudbuild.builds.builder",
"roles/clouddeploy.developer",
"roles/clouddeploy.releaser",
"roles/cloudkms.cryptoOperator",
"roles/containeranalysis.notes.attacher",
"roles/containeranalysis.notes.occurrences.viewer",
"roles/source.writer",
"roles/storage.admin",
"roles/cloudbuild.workerPoolUser",
"roles/ondemandscanning.admin",
"roles/logging.logWriter"
]
| no | | clouddeploy\_pipeline\_name | Cloud Deploy pipeline name | `string` | `"deploy-pipeline"` | no | -| gar\_repo\_name\_suffix | Docker artifact regitery repo to store app build images | `string` | `"app-image-repo"` | no | +| csr\_app\_source\_repo | Name of repo that contains app source code along with cloudbuild yaml | `string` | `"app-source"` | no | +| gar\_repo\_name\_suffix | Docker artifact registry repo to store app build images | `string` | `"app-image-repo"` | no | +| github\_auth | Authentication configuration for GitHub. Required only if repo\_type is 'GITHUBv2'. |
object({
secret_id = string
app_id_secret_id = string
secret_project_id = string
})
| `null` | no | +| gitlab\_auth | Authentication configuration for GitLab. Required only if repo\_type is 'GITLABv2'. |
object({
read_authorizer_credential_secret_id = string
authorizer_credential_secret_id = string
webhook_secret_id = string
enterprise_host_uri = optional(string)
enterprise_service_directory = optional(string)
enterprise_ca_certificate = optional(string)
secret_project_id = string
})
| `null` | no | | labels | A set of key/value label pairs to assign to the resources deployed by this blueprint. | `map(string)` | `{}` | no | | primary\_location | Region used for key-ring | `string` | n/a | yes | | project\_id | Project ID for CICD Pipeline Project | `string` | n/a | yes | +| repository\_type | The type of the repository. Must be one of 'GITHUB', 'GITLAB', or 'CSR'. | `string` | n/a | yes | | runner\_build\_folder | Path to the source folder for the cloud builds submit command. Leave blank if `skip_provisioners = true` | `string` | `""` | no | | skip\_provisioners | Skip modules that use provisioners/local-exec | `bool` | `false` | no | | trigger\_branch\_name | A regular expression to match one or more branches for the build trigger. | `string` | n/a | yes | @@ -63,9 +130,10 @@ The template [`cloudbuild-ci.yaml`](../../build/cloudbuild-ci.yaml) build config | binauth\_attestor\_names | Names of Attestors | | binauth\_attestor\_project\_id | Project ID where attestors get created | | build\_sa\_email | Cloud Build Service Account email address | -| build\_trigger\_name | The name of the cloud build trigger for the app source repo. | | cache\_bucket\_name | The name of the storage bucket for cloud build. | -| source\_repo\_names | Name of the created CSR repos | -| source\_repo\_urls | URLS of the created CSR repos | +| skaffold\_builder\_image\_tag | The full path to the built Skaffold builder image in Artifact Registry. | +| source\_repo\_name | Name of the created CSR repos | +| source\_repo\_url | URLS of the created CSR repos | +| standalone\_bucket\_kms\_key | n/a | diff --git a/modules/secure-ci/builder_image.tf b/modules/secure-ci/builder_image.tf new file mode 100644 index 00000000..227b2cd8 --- /dev/null +++ b/modules/secure-ci/builder_image.tf @@ -0,0 +1,49 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + skaffold_builder_version = "1.0.0" + skaffold_builder_image_name = "skaffold-builder" + skaffold_builder_image_tag = "${var.primary_location}-docker.pkg.dev/${var.project_id}/${google_artifact_registry_repository.image_repo.name}/${local.skaffold_builder_image_name}:${local.skaffold_builder_version}" +} + +resource "null_resource" "build_skaffold_builder_image" { + triggers = { + skaffold_builder_version = local.skaffold_builder_version + project_id = var.project_id + region = var.primary_location + gar_repo_name = google_artifact_registry_repository.image_repo.name + } + + provisioner "local-exec" { + command = <<-EOT + gcloud builds submit "${path.module}/cloud-build-builder" \ + --config="${path.module}/cloud-build-builder/cloudbuild-skaffold-build-image.yaml" \ + --project="${var.project_id}" \ + --region="${var.primary_location}" \ + --substitutions=_DEFAULT_REGION="${var.primary_location}",_GAR_REPOSITORY="${google_artifact_registry_repository.image_repo.name}" \ + --service-account="${google_service_account.build_sa.id}" \ + --gcs-log-dir="${google_storage_bucket.cache_bucket.url}/cloudbuild-logs" \ + --worker-pool="${var.cloudbuild_private_pool}" + EOT + } + + depends_on = [ + google_artifact_registry_repository.image_repo, + google_service_account.build_sa, + google_storage_bucket.cache_bucket, + ] +} diff --git a/examples/private_cluster_cicd/cloud-build-builder/Dockerfile b/modules/secure-ci/cloud-build-builder/Dockerfile similarity index 86% rename from examples/private_cluster_cicd/cloud-build-builder/Dockerfile rename to modules/secure-ci/cloud-build-builder/Dockerfile index 695a7766..9065220f 100644 --- a/examples/private_cluster_cicd/cloud-build-builder/Dockerfile +++ b/modules/secure-ci/cloud-build-builder/Dockerfile @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,10 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Start by building the application. FROM golang:1.15-buster as kritis-build -# Install kritis WORKDIR /go/src RUN git clone https://github.com/grafeas/kritis RUN mv kritis app @@ -26,16 +24,13 @@ RUN cp out/signer /signer FROM alpine:3.18 -### 1. Get Java via the package manager RUN apk update \ && apk upgrade \ && apk add --no-cache bash curl jq openjdk17-jre git openssh \ && apk add --no-cache --virtual=build-dependencies unzip -### Set JAVA_HOME ENV JAVA_HOME="/usr/lib/jvm/java-17-openjdk" -### 2. Get Python, PIP RUN apk add --no-cache python3 \ && python3 -m ensurepip \ && pip3 install --upgrade pip setuptools \ @@ -44,7 +39,6 @@ if [ ! -e /usr/bin/pip ]; then ln -s pip3 /usr/bin/pip ; fi && \ if [[ ! -e /usr/bin/python ]]; then ln -sf /usr/bin/python3 /usr/bin/python; fi && \ rm -r /root/.cache -#### 3. Install gcloud ENV CLOUD_SDK_VERSION="423.0.0" ENV CLOUDSDK_INSTALL_DIR /usr/local/gcloud/ RUN wget "https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-${CLOUD_SDK_VERSION}-linux-x86_64.tar.gz" \ @@ -52,18 +46,14 @@ RUN wget "https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-clou && rm "google-cloud-sdk-${CLOUD_SDK_VERSION}-linux-x86_64.tar.gz" ENV PATH $PATH:/usr/local/google-cloud-sdk/bin -#### Install skaffold and other components via gcloud RUN gcloud components install skaffold kubectl kustomize kpt nomos beta -##### 4. Add the kritis signer to the commands COPY --from=kritis-build /signer . -#### 4. Install YQ for parsing RUN curl -Lo yq https://github.com/mikefarah/yq/releases/download/v4.6.1/yq_linux_amd64 RUN chmod +x yq RUN mv ./yq /usr/local/bin -# Print out java version RUN echo -e "JAVA_VERSION: $(${JAVA_HOME}/bin/java --version)" RUN echo -e "PYTHON: $(python --version)\nPIP: $(pip --version)" diff --git a/examples/private_cluster_cicd/cloud-build-builder/cloudbuild-skaffold-build-image.yaml b/modules/secure-ci/cloud-build-builder/cloudbuild-skaffold-build-image.yaml similarity index 83% rename from examples/private_cluster_cicd/cloud-build-builder/cloudbuild-skaffold-build-image.yaml rename to modules/secure-ci/cloud-build-builder/cloudbuild-skaffold-build-image.yaml index 6f4fa3d3..3031773e 100644 --- a/examples/private_cluster_cicd/cloud-build-builder/cloudbuild-skaffold-build-image.yaml +++ b/modules/secure-ci/cloud-build-builder/cloudbuild-skaffold-build-image.yaml @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,14 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -options: - # Use higher CPU machines so the caching and build steps are faster. - machineType: 'E2_HIGHCPU_32' steps: - name: 'gcr.io/cloud-builders/docker' args: [ 'build', '-t', '$_DEFAULT_REGION-docker.pkg.dev/$PROJECT_ID/$_GAR_REPOSITORY/skaffold-builder', '.' ] - name: 'gcr.io/cloud-builders/docker' args: ['push', '$_DEFAULT_REGION-docker.pkg.dev/$PROJECT_ID/$_GAR_REPOSITORY/skaffold-builder'] substitutions: - _DEFAULT_REGION: 'us-east1' # default value + _DEFAULT_REGION: 'us-central1' # default value _GAR_REPOSITORY: '$PROJECT_ID-app-image-repo' # default value diff --git a/modules/secure-ci/main.tf b/modules/secure-ci/main.tf index 5a456595..61d9dbfd 100644 --- a/modules/secure-ci/main.tf +++ b/modules/secure-ci/main.tf @@ -1,5 +1,5 @@ /** - * Copyright 2021 Google LLC + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,12 @@ * limitations under the License. */ +resource "random_string" "suffix" { + length = 4 + special = false + upper = false +} + resource "google_sourcerepo_repository" "csr_ci_repository" { count = local.use_csr ? 1 : 0 @@ -49,7 +55,7 @@ module "cloudbuild_repositories" { resource "google_storage_bucket" "cache_bucket" { project = var.project_id - name = local.cache_bucket_name + name = "${local.cache_bucket_name}-${random_string.suffix.id}" location = var.primary_location uniform_bucket_level_access = true force_destroy = true @@ -57,6 +63,13 @@ resource "google_storage_bucket" "cache_bucket" { enabled = true } labels = var.labels + + dynamic "encryption" { + for_each = var.bucket_kms_key != null ? [1] : [] + content { + default_kms_key_name = var.bucket_kms_key + } + } } resource "google_service_account" "build_sa" { diff --git a/modules/secure-ci/metadata.display.yaml b/modules/secure-ci/metadata.display.yaml index b5b517c6..0d62e542 100644 --- a/modules/secure-ci/metadata.display.yaml +++ b/modules/secure-ci/metadata.display.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ spec: source: repo: https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd.git sourceType: git - dir: secure-ci + dir: /modules/secure-ci ui: input: variables: @@ -38,12 +38,18 @@ spec: attestor_names_prefix: name: attestor_names_prefix title: Attestor Names Prefix + bucket_kms_key: + name: bucket_kms_key + title: Bucket Kms Key build_image_config_yaml: name: build_image_config_yaml title: Build Image Config Yaml cache_bucket_name: name: cache_bucket_name title: Cache Bucket Name + ci_repository: + name: ci_repository + title: Ci Repository cloudbuild_cd_repo: name: cloudbuild_cd_repo title: Cloudbuild Cd Repo @@ -56,9 +62,18 @@ spec: clouddeploy_pipeline_name: name: clouddeploy_pipeline_name title: Clouddeploy Pipeline Name + csr_app_source_repo: + name: csr_app_source_repo + title: Csr App Source Repo gar_repo_name_suffix: name: gar_repo_name_suffix title: Gar Repo Name Suffix + github_auth: + name: github_auth + title: Github Auth + gitlab_auth: + name: gitlab_auth + title: Gitlab Auth labels: name: labels title: Labels @@ -68,6 +83,9 @@ spec: project_id: name: project_id title: Project Id + repository_type: + name: repository_type + title: Repository Type runner_build_folder: name: runner_build_folder title: Runner Build Folder diff --git a/modules/secure-ci/metadata.yaml b/modules/secure-ci/metadata.yaml index e6a6717d..85f69c8c 100644 --- a/modules/secure-ci/metadata.yaml +++ b/modules/secure-ci/metadata.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,174 +24,234 @@ spec: source: repo: https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd.git sourceType: git - dir: secure-ci - version: 1.1.1 + dir: /modules/secure-ci actuationTool: flavor: Terraform - version: '>= 0.13.0' description: {} content: examples: - - name: app_cicd - location: examples/app_cicd - - name: cloudbuild_private_pool - location: examples/cloudbuild_private_pool - - name: private_cluster_cicd - location: examples/private_cluster_cicd - - name: standalone_single_project - location: examples/standalone_single_project + - name: standalone_single_project + location: examples/standalone_single_project interfaces: variables: - - name: additional_substitutions - description: Parameters to be substituted in the build specification. All keys should begin with an underscore. - varType: map(string) - defaultValue: {} - - name: app_build_trigger_yaml - description: Name of application cloudbuild yaml file - varType: string - required: true - - name: app_source_repo - description: Name of repo that contains app source code along with cloudbuild yaml - varType: string - defaultValue: app-source - - name: attestor_names_prefix - description: A list of Binary Authorization attestors to create. The first attestor specified in this list will be used as the build-attestor during the CI phase. - varType: list(string) - required: true - - name: build_image_config_yaml - description: Name of image builder yaml file - varType: string - required: true - - name: cache_bucket_name - description: Name of cloudbuild artifact and cache GCS bucket - varType: string - defaultValue: "" - - name: cloudbuild_cd_repo - description: Name of repo that stores the Cloud Build CD phase configs - for post-deployment checks - varType: string - defaultValue: cloudbuild-cd-config - - name: cloudbuild_private_pool - description: Cloud Build private pool self-link - varType: string - defaultValue: "" - - name: cloudbuild_service_account_roles - description: IAM roles given to the Cloud Build service account to enable security scanning operations - varType: list(string) - defaultValue: - - roles/artifactregistry.admin - - roles/binaryauthorization.attestorsVerifier - - roles/cloudbuild.builds.builder - - roles/clouddeploy.developer - - roles/clouddeploy.releaser - - roles/cloudkms.cryptoOperator - - roles/containeranalysis.notes.attacher - - roles/containeranalysis.notes.occurrences.viewer - - roles/source.writer - - roles/storage.admin - - roles/cloudbuild.workerPoolUser - - roles/ondemandscanning.admin - - roles/logging.logWriter - - name: clouddeploy_pipeline_name - description: Cloud Deploy pipeline name - varType: string - defaultValue: deploy-pipeline - - name: gar_repo_name_suffix - description: Docker artifact regitery repo to store app build images - varType: string - defaultValue: app-image-repo - - name: labels - description: A set of key/value label pairs to assign to the resources deployed by this blueprint. - varType: map(string) - defaultValue: {} - - name: primary_location - description: Region used for key-ring - varType: string - required: true - - name: project_id - description: Project ID for CICD Pipeline Project - varType: string - required: true - - name: runner_build_folder - description: Path to the source folder for the cloud builds submit command. Leave blank if `skip_provisioners = true` - varType: string - defaultValue: "" - - name: skip_provisioners - description: Skip modules that use provisioners/local-exec - varType: bool - defaultValue: false - - name: trigger_branch_name - description: A regular expression to match one or more branches for the build trigger. - varType: string - required: true - - name: use_tf_google_credentials_env_var - description: Optional GOOGLE_CREDENTIALS environment variable to be activated. - varType: bool - defaultValue: false + - name: project_id + description: Project ID for CICD Pipeline Project + varType: string + required: true + - name: primary_location + description: Region used for key-ring + varType: string + required: true + - name: attestor_names_prefix + description: A list of Binary Authorization attestors to create. The first attestor specified in this list will be used as the build-attestor during the CI phase. + varType: list(string) + required: true + - name: app_build_trigger_yaml + description: Name of application cloudbuild yaml file + varType: string + required: true + - name: runner_build_folder + description: Path to the source folder for the cloud builds submit command. Leave blank if `skip_provisioners = true` + varType: string + defaultValue: "" + - name: build_image_config_yaml + description: Name of image builder yaml file + varType: string + required: true + - name: csr_app_source_repo + description: Name of repo that contains app source code along with cloudbuild yaml + varType: string + defaultValue: app-source + - name: cache_bucket_name + description: Name of cloudbuild artifact and cache GCS bucket + varType: string + defaultValue: "" + - name: gar_repo_name_suffix + description: Docker artifact registry repo to store app build images + varType: string + defaultValue: app-image-repo + - name: use_tf_google_credentials_env_var + description: Optional GOOGLE_CREDENTIALS environment variable to be activated. + varType: bool + defaultValue: false + - name: trigger_branch_name + description: A regular expression to match one or more branches for the build trigger. + varType: string + required: true + - name: cloudbuild_service_account_roles + description: IAM roles given to the Cloud Build service account to enable security scanning operations + varType: list(string) + defaultValue: + - roles/artifactregistry.admin + - roles/binaryauthorization.attestorsVerifier + - roles/cloudbuild.builds.builder + - roles/clouddeploy.developer + - roles/clouddeploy.releaser + - roles/cloudkms.cryptoOperator + - roles/containeranalysis.notes.attacher + - roles/containeranalysis.notes.occurrences.viewer + - roles/source.writer + - roles/storage.admin + - roles/cloudbuild.workerPoolUser + - roles/ondemandscanning.admin + - roles/logging.logWriter + - name: additional_substitutions + description: Parameters to be substituted in the build specification. All keys should begin with an underscore. + varType: map(string) + defaultValue: {} + - name: cloudbuild_private_pool + description: Cloud Build private pool self-link + varType: string + defaultValue: "" + - name: clouddeploy_pipeline_name + description: Cloud Deploy pipeline name + varType: string + defaultValue: deploy-pipeline + - name: skip_provisioners + description: Skip modules that use provisioners/local-exec + varType: bool + defaultValue: false + - name: labels + description: A set of key/value label pairs to assign to the resources deployed by this blueprint. + varType: map(string) + defaultValue: {} + - name: bucket_kms_key + description: KMS Key id to be used to encrypt bucket. + varType: string + - name: repository_type + description: The type of the repository. Must be one of 'GITHUB', 'GITLAB', or 'CSR'. + varType: string + required: true + - name: ci_repository + description: The CI repository to configure. The key is a short name for the service. + varType: |- + object({ + repository_name = string + repository_url = string + }) + - name: github_auth + description: Authentication configuration for GitHub. Required only if repo_type is 'GITHUBv2'. + varType: |- + object({ + secret_id = string + app_id_secret_id = string + secret_project_id = string + }) + - name: gitlab_auth + description: Authentication configuration for GitLab. Required only if repo_type is 'GITLABv2'. + varType: |- + object({ + read_authorizer_credential_secret_id = string + authorizer_credential_secret_id = string + webhook_secret_id = string + enterprise_host_uri = optional(string) + enterprise_service_directory = optional(string) + enterprise_ca_certificate = optional(string) + secret_project_id = string + }) outputs: - - name: app_artifact_repo - description: GAR Repo created to store runner images - - name: binauth_attestor_ids - description: IDs of Attestors - - name: binauth_attestor_names - description: Names of Attestors - - name: binauth_attestor_project_id - description: Project ID where attestors get created - - name: build_sa_email - description: Cloud Build Service Account email address - - name: build_trigger_name - description: The name of the cloud build trigger for the app source repo. - - name: cache_bucket_name - description: The name of the storage bucket for cloud build. - - name: source_repo_names - description: Name of the created CSR repos - - name: source_repo_urls - description: URLS of the created CSR repos + - name: app_artifact_repo + description: GAR Repo created to store runner images + - name: binauth_attestor_ids + description: IDs of Attestors + - name: binauth_attestor_names + description: Names of Attestors + - name: binauth_attestor_project_id + description: Project ID where attestors get created + - name: build_sa_email + description: Cloud Build Service Account email address + - name: cache_bucket_name + description: The name of the storage bucket for cloud build. + - name: skaffold_builder_image_tag + description: The full path to the built Skaffold builder image in Artifact Registry. + - name: source_repo_name + description: Name of the created CSR repos + - name: source_repo_url + description: URLS of the created CSR repos + - name: standalone_bucket_kms_key requirements: roles: - - level: Project - roles: - - roles/artifactregistry.admin - - roles/binaryauthorization.attestorsAdmin - - roles/cloudbuild.builds.builder - - roles/cloudbuild.workerPoolOwner - - roles/clouddeploy.admin - - roles/cloudkms.admin - - roles/cloudkms.publicKeyViewer - - roles/containeranalysis.notes.editor - - roles/compute.networkAdmin - - roles/gkehub.editor - - roles/iam.serviceAccountAdmin - - roles/iam.serviceAccountUser - - roles/pubsub.editor - - roles/serviceusage.serviceUsageAdmin - - roles/source.admin - - roles/storage.admin - - roles/resourcemanager.projectIamAdmin - - roles/viewer - - level: Project - roles: - - roles/compute.networkAdmin - - roles/container.admin - - roles/binaryauthorization.policyEditor - - roles/resourcemanager.projectIamAdmin - - roles/iam.serviceAccountAdmin - - roles/serviceusage.serviceUsageViewer - - roles/iam.serviceAccountUser + - level: Project + roles: + - roles/compute.networkAdmin + - roles/container.admin + - roles/binaryauthorization.policyEditor + - roles/resourcemanager.projectIamAdmin + - roles/iam.serviceAccountAdmin + - roles/serviceusage.serviceUsageViewer + - roles/iam.serviceAccountUser + - level: Project + roles: + - roles/artifactregistry.admin + - roles/binaryauthorization.attestorsAdmin + - roles/cloudbuild.builds.builder + - roles/cloudbuild.workerPoolOwner + - roles/cloudbuild.admin + - roles/clouddeploy.admin + - roles/cloudkms.admin + - roles/cloudkms.publicKeyViewer + - roles/containeranalysis.notes.editor + - roles/compute.admin + - roles/compute.networkAdmin + - roles/compute.securityAdmin + - roles/gkehub.editor + - roles/iam.serviceAccountAdmin + - roles/iam.serviceAccountUser + - roles/pubsub.editor + - roles/serviceusage.serviceUsageAdmin + - roles/source.admin + - roles/storage.admin + - roles/resourcemanager.projectIamAdmin + - roles/viewer + - roles/secretmanager.admin + - roles/servicedirectory.admin + - roles/dns.admin services: - - cloudresourcemanager.googleapis.com - - cloudbilling.googleapis.com - - clouddeploy.googleapis.com - - storage-api.googleapis.com - - serviceusage.googleapis.com - - cloudbuild.googleapis.com - - containerregistry.googleapis.com - - iamcredentials.googleapis.com - - secretmanager.googleapis.com - - sourcerepo.googleapis.com - - artifactregistry.googleapis.com - - containeranalysis.googleapis.com - - cloudkms.googleapis.com - - binaryauthorization.googleapis.com - - containerscanning.googleapis.com - - servicenetworking.googleapis.com - - pubsub.googleapis.com + - accesscontextmanager.googleapis.com + - aiplatform.googleapis.com + - anthos.googleapis.com + - anthosconfigmanagement.googleapis.com + - anthospolicycontroller.googleapis.com + - apikeys.googleapis.com + - artifactregistry.googleapis.com + - binaryauthorization.googleapis.com + - certificatemanager.googleapis.com + - cloudbilling.googleapis.com + - cloudbuild.googleapis.com + - clouddeploy.googleapis.com + - cloudfunctions.googleapis.com + - cloudkms.googleapis.com + - cloudresourcemanager.googleapis.com + - cloudtrace.googleapis.com + - compute.googleapis.com + - container.googleapis.com + - containeranalysis.googleapis.com + - containerscanning.googleapis.com + - gkehub.googleapis.com + - iam.googleapis.com + - iap.googleapis.com + - mesh.googleapis.com + - modelarmor.googleapis.com + - monitoring.googleapis.com + - multiclusteringress.googleapis.com + - multiclusterservicediscovery.googleapis.com + - networkmanagement.googleapis.com + - networkservices.googleapis.com + - notebooks.googleapis.com + - orgpolicy.googleapis.com + - secretmanager.googleapis.com + - servicedirectory.googleapis.com + - servicemanagement.googleapis.com + - servicenetworking.googleapis.com + - serviceusage.googleapis.com + - sourcerepo.googleapis.com + - sqladmin.googleapis.com + - storage.googleapis.com + - trafficdirector.googleapis.com + providerVersions: + - source: hashicorp/google + version: "> 4, < 8" + - source: hashicorp/google-beta + version: "> 4, < 8" + - source: hashicorp/random + version: ">= 3.7.2" diff --git a/modules/secure-ci/outputs.tf b/modules/secure-ci/outputs.tf index 7b2ae5cc..32cde59e 100644 --- a/modules/secure-ci/outputs.tf +++ b/modules/secure-ci/outputs.tf @@ -26,9 +26,7 @@ output "binauth_attestor_names" { output "binauth_attestor_ids" { description = "IDs of Attestors" - # For each entry in our locals map, create a map entry - # with the key being the short name and the value being the full ID. - value = { for key, attestor in local.attestors : key => attestor.id } + value = { for key, attestor in local.attestors : key => attestor.id } } output "binauth_attestor_project_id" { @@ -55,3 +53,12 @@ output "build_sa_email" { description = "Cloud Build Service Account email address" value = google_service_account.build_sa.email } + +output "standalone_bucket_kms_key" { + value = var.bucket_kms_key +} + +output "skaffold_builder_image_tag" { + description = "The full path to the built Skaffold builder image in Artifact Registry." + value = local.skaffold_builder_image_tag +} diff --git a/modules/secure-ci/versions.tf b/modules/secure-ci/versions.tf index d32c46c6..c736cab3 100644 --- a/modules/secure-ci/versions.tf +++ b/modules/secure-ci/versions.tf @@ -29,4 +29,11 @@ terraform { version = "> 4, < 8" } } + + provider_meta "google" { + module_name = "blueprints/terraform/terraform-google-secure-cicd:secure-ci/v1.2.2" + } + provider_meta "google-beta" { + module_name = "blueprints/terraform/terraform-google-secure-cicd:secure-ci/v1.2.2" + } } diff --git a/modules/workerpool-gke-ha-vpn/main.tf b/modules/workerpool-gke-ha-vpn/main.tf index f73da4df..903a07c0 100644 --- a/modules/workerpool-gke-ha-vpn/main.tf +++ b/modules/workerpool-gke-ha-vpn/main.tf @@ -14,7 +14,6 @@ * limitations under the License. */ -# HA VPN module "vpn_ha_1" { source = "terraform-google-modules/vpn/google//modules/vpn_ha" version = "~> 2.3.0, != 2.3.2" @@ -39,11 +38,15 @@ module "vpn_ha_1" { asn = var.gateway_2_asn } bgp_peer_options = null + bgp_session_name = "${module.vpn_ha_1.name}-tunnel-0" bgp_session_range = "${cidrhost(var.bgp_range_1, 1)}/30" # 169.254.1.1/30 ike_version = 2 vpn_gateway_interface = 0 peer_external_gateway_interface = null + peer_external_gateway_self_link = module.vpn_ha_2.self_link shared_secret = "" + peer_external_gateway_self_link = null + bgp_session_name = null } remote-1 = { bgp_peer = { @@ -51,11 +54,15 @@ module "vpn_ha_1" { asn = var.gateway_2_asn } bgp_peer_options = null + bgp_session_name = "${module.vpn_ha_1.name}-tunnel-1" bgp_session_range = "${cidrhost(var.bgp_range_2, 1)}/30" # 169.254.2.1/30 ike_version = 2 vpn_gateway_interface = 1 peer_external_gateway_interface = null + peer_external_gateway_self_link = module.vpn_ha_2.self_link shared_secret = "" + peer_external_gateway_self_link = null + bgp_session_name = null } } } @@ -82,11 +89,15 @@ module "vpn_ha_2" { asn = var.gateway_1_asn } bgp_peer_options = null + bgp_session_name = "${module.vpn_ha_2.name}-tunnel-0" bgp_session_range = "${cidrhost(var.bgp_range_1, 2)}/30" # 169.254.1.2/30 ike_version = 2 vpn_gateway_interface = 0 peer_external_gateway_interface = null + peer_external_gateway_self_link = module.vpn_ha_1.self_link shared_secret = module.vpn_ha_1.random_secret + peer_external_gateway_self_link = null + bgp_session_name = null } remote-1 = { bgp_peer = { @@ -94,11 +105,15 @@ module "vpn_ha_2" { asn = var.gateway_1_asn } bgp_peer_options = null + bgp_session_name = "${module.vpn_ha_2.name}-tunnel-1" bgp_session_range = "${cidrhost(var.bgp_range_2, 2)}/30" # 169.254.2.2/30 ike_version = 2 vpn_gateway_interface = 1 peer_external_gateway_interface = null + peer_external_gateway_self_link = module.vpn_ha_1.self_link shared_secret = module.vpn_ha_1.random_secret + peer_external_gateway_self_link = null + bgp_session_name = null } } } diff --git a/modules/workerpool-gke-ha-vpn/metadata.display.yaml b/modules/workerpool-gke-ha-vpn/metadata.display.yaml index bd49d4ce..a0b0b6b4 100644 --- a/modules/workerpool-gke-ha-vpn/metadata.display.yaml +++ b/modules/workerpool-gke-ha-vpn/metadata.display.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ spec: source: repo: https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd.git sourceType: git - dir: workerpool-gke-ha-vpn + dir: /modules/workerpool-gke-ha-vpn ui: input: variables: diff --git a/modules/workerpool-gke-ha-vpn/metadata.yaml b/modules/workerpool-gke-ha-vpn/metadata.yaml index d27891c5..6414903b 100644 --- a/modules/workerpool-gke-ha-vpn/metadata.yaml +++ b/modules/workerpool-gke-ha-vpn/metadata.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,139 +24,166 @@ spec: source: repo: https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd.git sourceType: git - dir: workerpool-gke-ha-vpn - version: 1.1.1 + dir: /modules/workerpool-gke-ha-vpn + version: 1.2.1 actuationTool: flavor: Terraform - version: '>= 0.13.0' + version: ~> 1.3 description: {} content: examples: - - name: app_cicd - location: examples/app_cicd - - name: cloudbuild_private_pool - location: examples/cloudbuild_private_pool - - name: private_cluster_cicd - location: examples/private_cluster_cicd - - name: standalone_single_project - location: examples/standalone_single_project + - name: standalone_single_project + location: examples/standalone_single_project interfaces: variables: - - name: bgp_range_1 - description: BGP range for HA VPN tunnel 1 - varType: string - defaultValue: 169.254.1.0/30 - - name: bgp_range_2 - description: BGP range for HA VPN tunnel 1 - varType: string - defaultValue: 169.254.2.0/30 - - name: gateway_1_asn - description: 'ASN for HA VPN gateway #1. You can use any private ASN (64512 through 65534, 4200000000 through 4294967294) that you are not using elsewhere in your network' - varType: number - defaultValue: 65001 - - name: gateway_2_asn - description: 'ASN for HA VPN gateway #2. You can use any private ASN (64512 through 65534, 4200000000 through 4294967294) that you are not using elsewhere in your network' - varType: number - defaultValue: 65002 - - name: gke_control_plane_cidrs - description: map of GKE control plane CIDRs to name - varType: map(string) - required: true - - name: gke_location - description: Region of GKE subnet & cluster - varType: string - required: true - - name: gke_network - description: Name of GKE VPC - varType: string - required: true - - name: gke_project - description: Project ID of GKE VPC and cluster - varType: string - required: true - - name: labels - description: A set of key/value label pairs to assign to the resources deployed by this blueprint. - varType: map(string) - defaultValue: {} - - name: location - description: Region for Cloud Build worker pool - varType: string - defaultValue: us-central1 - - name: project_id - description: Project ID for Cloud Build - varType: string - required: true - - name: vpn_router_name_prefix - description: Prefix for HA VPN router names - varType: string - defaultValue: "" - - name: workerpool_network - description: Self link for Cloud Build VPC - varType: string - required: true - - name: workerpool_range - description: Address range of Cloud Build Workerpool - varType: string - required: true + - name: vpn_router_name_prefix + description: Prefix for HA VPN router names + varType: string + defaultValue: "" + - name: location + description: Region for Cloud Build worker pool + varType: string + defaultValue: us-central1 + - name: project_id + description: Project ID for Cloud Build + varType: string + required: true + - name: gke_project + description: Project ID of GKE VPC and cluster + varType: string + required: true + - name: gke_network + description: Name of GKE VPC + varType: string + required: true + - name: gke_location + description: Region of GKE subnet & cluster + varType: string + required: true + - name: gke_control_plane_cidrs + description: map of GKE control plane CIDRs to name + varType: map(string) + required: true + - name: workerpool_network + description: Self link for Cloud Build VPC + varType: string + required: true + - name: workerpool_range + description: Address range of Cloud Build Workerpool + varType: string + required: true + - name: gateway_1_asn + description: "ASN for HA VPN gateway #1. You can use any private ASN (64512 through 65534, 4200000000 through 4294967294) that you are not using elsewhere in your network" + varType: number + defaultValue: 65001 + - name: gateway_2_asn + description: "ASN for HA VPN gateway #2. You can use any private ASN (64512 through 65534, 4200000000 through 4294967294) that you are not using elsewhere in your network" + varType: number + defaultValue: 65002 + - name: bgp_range_1 + description: BGP range for HA VPN tunnel 1 + varType: string + defaultValue: 169.254.1.0/30 + - name: bgp_range_2 + description: BGP range for HA VPN tunnel 1 + varType: string + defaultValue: 169.254.2.0/30 + - name: labels + description: A set of key/value label pairs to assign to the resources deployed by this blueprint. + varType: map(string) + defaultValue: {} outputs: - - name: vpn_gateway_cloudbuild - description: Name of HA VPN gateway on Cloud Build VPC - - name: vpn_gateway_gke - description: Name of HA VPN gateway on GKE VPC - - name: vpn_router_cloudbuild_names - description: Names of HA VPN router on Cloud Build VPC - - name: vpn_router_gke_names - description: Names of HA VPN router on GKE VPC - - name: vpn_tunnel_cloudbuild_names - description: Names of HA VPN tunnels on Cloud Build VPC - - name: vpn_tunnel_gke_names - description: Names of HA VPN tunnels on GKE VPC + - name: vpn_gateway_cloudbuild + description: Name of HA VPN gateway on Cloud Build VPC + - name: vpn_gateway_gke + description: Name of HA VPN gateway on GKE VPC + - name: vpn_router_cloudbuild_names + description: Names of HA VPN router on Cloud Build VPC + - name: vpn_router_gke_names + description: Names of HA VPN router on GKE VPC + - name: vpn_tunnel_cloudbuild_names + description: Names of HA VPN tunnels on Cloud Build VPC + - name: vpn_tunnel_gke_names + description: Names of HA VPN tunnels on GKE VPC requirements: roles: - - level: Project - roles: - - roles/artifactregistry.admin - - roles/binaryauthorization.attestorsAdmin - - roles/cloudbuild.builds.builder - - roles/cloudbuild.workerPoolOwner - - roles/clouddeploy.admin - - roles/cloudkms.admin - - roles/cloudkms.publicKeyViewer - - roles/containeranalysis.notes.editor - - roles/compute.networkAdmin - - roles/gkehub.editor - - roles/iam.serviceAccountAdmin - - roles/iam.serviceAccountUser - - roles/pubsub.editor - - roles/serviceusage.serviceUsageAdmin - - roles/source.admin - - roles/storage.admin - - roles/resourcemanager.projectIamAdmin - - roles/viewer - - level: Project - roles: - - roles/compute.networkAdmin - - roles/container.admin - - roles/binaryauthorization.policyEditor - - roles/resourcemanager.projectIamAdmin - - roles/iam.serviceAccountAdmin - - roles/serviceusage.serviceUsageViewer - - roles/iam.serviceAccountUser + - level: Project + roles: + - roles/compute.networkAdmin + - roles/container.admin + - roles/binaryauthorization.policyEditor + - roles/resourcemanager.projectIamAdmin + - roles/iam.serviceAccountAdmin + - roles/serviceusage.serviceUsageViewer + - roles/iam.serviceAccountUser + - level: Project + roles: + - roles/artifactregistry.admin + - roles/binaryauthorization.attestorsAdmin + - roles/cloudbuild.builds.builder + - roles/cloudbuild.workerPoolOwner + - roles/cloudbuild.admin + - roles/clouddeploy.admin + - roles/cloudkms.admin + - roles/cloudkms.publicKeyViewer + - roles/containeranalysis.notes.editor + - roles/compute.admin + - roles/compute.networkAdmin + - roles/compute.securityAdmin + - roles/gkehub.editor + - roles/iam.serviceAccountAdmin + - roles/iam.serviceAccountUser + - roles/pubsub.editor + - roles/serviceusage.serviceUsageAdmin + - roles/source.admin + - roles/storage.admin + - roles/resourcemanager.projectIamAdmin + - roles/viewer + - roles/secretmanager.admin + - roles/servicedirectory.admin + - roles/dns.admin services: - - cloudresourcemanager.googleapis.com - - cloudbilling.googleapis.com - - clouddeploy.googleapis.com - - storage-api.googleapis.com - - serviceusage.googleapis.com - - cloudbuild.googleapis.com - - containerregistry.googleapis.com - - iamcredentials.googleapis.com - - secretmanager.googleapis.com - - sourcerepo.googleapis.com - - artifactregistry.googleapis.com - - containeranalysis.googleapis.com - - cloudkms.googleapis.com - - binaryauthorization.googleapis.com - - containerscanning.googleapis.com - - servicenetworking.googleapis.com - - pubsub.googleapis.com + - accesscontextmanager.googleapis.com + - aiplatform.googleapis.com + - anthos.googleapis.com + - anthosconfigmanagement.googleapis.com + - anthospolicycontroller.googleapis.com + - apikeys.googleapis.com + - artifactregistry.googleapis.com + - binaryauthorization.googleapis.com + - certificatemanager.googleapis.com + - cloudbilling.googleapis.com + - cloudbuild.googleapis.com + - clouddeploy.googleapis.com + - cloudfunctions.googleapis.com + - cloudkms.googleapis.com + - cloudresourcemanager.googleapis.com + - cloudtrace.googleapis.com + - compute.googleapis.com + - container.googleapis.com + - containeranalysis.googleapis.com + - containerscanning.googleapis.com + - gkehub.googleapis.com + - iam.googleapis.com + - iap.googleapis.com + - mesh.googleapis.com + - modelarmor.googleapis.com + - monitoring.googleapis.com + - multiclusteringress.googleapis.com + - multiclusterservicediscovery.googleapis.com + - networkmanagement.googleapis.com + - networkservices.googleapis.com + - notebooks.googleapis.com + - orgpolicy.googleapis.com + - secretmanager.googleapis.com + - servicedirectory.googleapis.com + - servicemanagement.googleapis.com + - servicenetworking.googleapis.com + - serviceusage.googleapis.com + - sourcerepo.googleapis.com + - sqladmin.googleapis.com + - storage.googleapis.com + - trafficdirector.googleapis.com + providerVersions: + - source: hashicorp/google + version: "> 4, < 8" diff --git a/modules/workerpool-gke-ha-vpn/versions.tf b/modules/workerpool-gke-ha-vpn/versions.tf index 6cc55791..de928eec 100644 --- a/modules/workerpool-gke-ha-vpn/versions.tf +++ b/modules/workerpool-gke-ha-vpn/versions.tf @@ -15,7 +15,7 @@ */ terraform { - required_version = ">= 0.13.0" + required_version = "~> 1.3" required_providers { google = { source = "hashicorp/google" @@ -24,6 +24,9 @@ terraform { } provider_meta "google" { - module_name = "blueprints/terraform/terraform-google-secure-cicd:workerpool-gke-ha-vpn/v1.2.1" + module_name = "blueprints/terraform/terraform-google-secure-cicd:workerpool-gke-ha-vpn/v1.2.2" + } + provider_meta "google-beta" { + module_name = "blueprints/terraform/terraform-google-secure-cicd:workerpool-gke-ha-vpn/v1.2.2" } } diff --git a/test/integration/go.mod b/test/integration/go.mod index 8493c96e..3eb8c0ae 100644 --- a/test/integration/go.mod +++ b/test/integration/go.mod @@ -1,78 +1,78 @@ module github.com/terraform-google-modules/terraform-google-secure-cicd/test/integration -go 1.20 +go 1.24.0 + +toolchain go1.24.4 + +require ( + github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test v0.18.0 + github.com/stretchr/testify v1.11.1 +) require ( - cloud.google.com/go/secretmanager v1.10.0 - github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test v0.5.1 - github.com/stretchr/testify v1.8.2 + github.com/gruntwork-io/terratest v0.54.0 + github.com/otiai10/copy v1.14.1 + gitlab.com/gitlab-org/api/client-go v1.46.0 ) -require github.com/gruntwork-io/terratest v0.41.23 +require ( + github.com/alexflint/go-filemutex v1.3.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-querystring v1.2.0 // indirect + github.com/hashicorp/go-getter/v2 v2.2.3 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect + github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f // indirect + github.com/hashicorp/terraform-config-inspect v0.0.0-20250828155816-225c06ed5fd9 // indirect + github.com/mattn/go-shellwords v1.0.12 // indirect + github.com/otiai10/mint v1.6.3 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.39.0 // indirect +) require ( - cloud.google.com/go v0.110.0 // indirect - cloud.google.com/go/compute v1.19.1 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v0.13.0 // indirect - cloud.google.com/go/storage v1.29.0 // indirect github.com/agext/levenshtein v1.2.3 // indirect - github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect - github.com/aws/aws-sdk-go v1.44.209 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-errors/errors v1.4.2 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/go-errors/errors v1.5.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.3 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/google/gnostic v0.6.9 // indirect - github.com/google/go-cmp v0.5.9 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect - github.com/googleapis/gax-go/v2 v2.7.1 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-getter v1.7.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect - github.com/hashicorp/go-version v1.6.0 // indirect - github.com/hashicorp/hcl/v2 v2.16.1 // indirect - github.com/hashicorp/terraform-json v0.15.0 // indirect - github.com/jinzhu/copier v0.3.5 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hcl/v2 v2.22.0 // indirect + github.com/hashicorp/terraform-json v0.27.2 // indirect + github.com/jinzhu/copier v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/compress v1.15.15 // indirect + github.com/klauspost/compress v1.16.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-zglob v0.0.4 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/go-testing-interface v1.14.2-0.20210217184823-a52172cd2f64 // indirect + github.com/mitchellh/go-testing-interface v1.14.2-0.20210821155943-2d9075ca8770 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/tidwall/gjson v1.14.4 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/tidwall/gjson v1.18.0 github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect - github.com/tmccombs/hcl2json v0.4.0 // indirect - github.com/ulikunitz/xz v0.5.11 // indirect - github.com/zclconf/go-cty v1.13.0 // indirect - go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/mod v0.8.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/oauth2 v0.7.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/api v0.114.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/grpc v1.56.3 // indirect - google.golang.org/protobuf v1.30.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + github.com/tmccombs/hcl2json v0.6.4 // indirect + github.com/ulikunitz/xz v0.5.14 // indirect + github.com/zclconf/go-cty v1.16.4 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/kube-openapi v0.0.0-20230224204730-66828de6f33b // indirect - sigs.k8s.io/kustomize/kyaml v0.14.0 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + sigs.k8s.io/kustomize/kyaml v0.21.0 // indirect ) diff --git a/test/integration/go.sum b/test/integration/go.sum index 887ed2b2..96d4554a 100644 --- a/test/integration/go.sum +++ b/test/integration/go.sum @@ -1,445 +1,118 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= -cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= -cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= -cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= -cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= -cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= -cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= -cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= -cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= -cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= -cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= -cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= -cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= -cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= -cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= -cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= -cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= -cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= -cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= -cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= -cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= -cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= -cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= -cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= -cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= -cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= -cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= -cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= -cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= -cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY= -cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= -cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= -cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= -cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= -cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= -cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= -cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= -cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= -cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= -cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= -cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= -cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= -cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= -cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= -cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= -cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= -cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= -cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= -cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= -cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= -cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= -cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= -cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= -cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= -cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= -cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= -cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= -cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= -cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= -cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= -cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= -cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= -cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= -cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k= -cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= -cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= -cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= -cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= -cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= -cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= -cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= -cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= -cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= -cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= -cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= -cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= -cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= -cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= -cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= -cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= -cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= -cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= -cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= -cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= -cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= -cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= -cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= -cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= -cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= -cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= -cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= -cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= -cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= -cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= -cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= -cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= -cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= -cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= -cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= -cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= -cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= -cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= -cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= -cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= -cloud.google.com/go/secretmanager v1.10.0 h1:pu03bha7ukxF8otyPKTFdDz+rr9sE3YauS5PliDXK60= -cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= -cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= -cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= -cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= -cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= -cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= -cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= -cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= -cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= -cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= -cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= -cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= -cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI= -cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= -cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= -cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= -cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= -cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= -cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= -cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= -cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= -cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= -cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= -cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= -cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test v0.5.1 h1:c9pDJXXIj0I11fWBzDp3+lWvsF2rHaBYif3aXLQvBJI= -github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test v0.5.1/go.mod h1:vL5BHqlptYBDjjzF75uOc5YaEZDQ6UzMwSE+wd+g6g0= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test v0.18.0 h1:i7ReU+tZORrSoM9twwSN5rfrDXtuzBTFwUKKuARgdp8= +github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test v0.18.0/go.mod h1:nEy338yUjKkv7tMXBy7ervFU+j1NIh9tUUTAEJgygJ4= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -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/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/aws/aws-sdk-go v1.44.209 h1:wZuiaA4eaqYZmoZXqGgNHqVD7y7kUGFvACDGBgowTps= -github.com/aws/aws-sdk-go v1.44.209/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/alexflint/go-filemutex v1.3.0 h1:LgE+nTUWnQCyRKbpoceKZsPQbs84LivvgwUymZXdOcM= +github.com/alexflint/go-filemutex v1.3.0/go.mod h1:U0+VA/i30mGBlLCrFPGtTe9y6wGQfNAWPBTekHQ+c8A= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/go-errors/errors v1.5.0 h1:/EuijeGOu7ckFxzhkj4CXJ8JaenxK7bKUxpPYqeLHqQ= +github.com/go-errors/errors v1.5.0/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.2.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.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= -github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= -github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= -github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A= -github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/gruntwork-io/terratest v0.41.23 h1:GqwK0Nh6IQze3hka6iwuOd1V3wX7/y/85W25d1zpAnE= -github.com/gruntwork-io/terratest v0.41.23/go.mod h1:O6gajNBjO1wvc7Wl9WtbO+ORcdnhAV2GQiBE71ycwIk= +github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= +github.com/graph-gophers/graphql-go v1.9.0 h1:yu0ucKHLc5qGpRwLYKIWtr9bOoxovkWasuBrPQwlHls= +github.com/graph-gophers/graphql-go v1.9.0/go.mod h1:23olKZ7duEvHlF/2ELEoSZaY1aNPfShjP782SOoNTyM= +github.com/gruntwork-io/terratest v0.54.0 h1:JOVATYDpU0NAPbEkgYUP50BR2m45UGiR4dbs20sKzck= +github.com/gruntwork-io/terratest v0.54.0/go.mod h1:QvwQWZMTJmJB4E0d1Uc18quQm7+X53liKKp+fJSuaKA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-getter v1.7.1 h1:SWiSWN/42qdpR0MdhaOc/bLR48PLuP1ZQtYLRlM69uY= -github.com/hashicorp/go-getter v1.7.1/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= +github.com/hashicorp/go-getter/v2 v2.2.3 h1:6CVzhT0KJQHqd9b0pK3xSP0CM/Cv+bVhk+jcaRJ2pGk= +github.com/hashicorp/go-getter/v2 v2.2.3/go.mod h1:hp5Yy0GMQvwWVUmwLs3ygivz1JSLI323hdIE9J9m7TY= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl/v2 v2.16.1 h1:BwuxEMD/tsYgbhIW7UuI3crjovf3MzuFWiVgiv57iHg= -github.com/hashicorp/hcl/v2 v2.16.1/go.mod h1:JRmR89jycNkrrqnMmvPDMd56n1rQJ2Q6KocSLCMCXng= -github.com/hashicorp/terraform-json v0.15.0 h1:/gIyNtR6SFw6h5yzlbDbACyGvIhKtQi8mTsbkNd79lE= -github.com/hashicorp/terraform-json v0.15.0/go.mod h1:+L1RNzjDU5leLFZkHTFTbJXaoqUC6TqXlFgDoOXrtvk= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= -github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+DbLISwf2B8WXEolNRA8BGCwI9jws= +github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= +github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/hashicorp/terraform-config-inspect v0.0.0-20250828155816-225c06ed5fd9 h1:1MLunZqjVd9j5gc5kjE04VEoieDVdWdgdM6T2fNQvY8= +github.com/hashicorp/terraform-config-inspect v0.0.0-20250828155816-225c06ed5fd9/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= +github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= +github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= -github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= -github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-zglob v0.0.4 h1:LQi2iOm0/fGgu80AioIJ/1j9w9Oh+9DZ39J4VAGzHQM= github.com/mattn/go-zglob v0.0.4/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= -github.com/mitchellh/go-testing-interface v1.14.2-0.20210217184823-a52172cd2f64 h1:+9bM6qWXndPx7+czi9+Jj6zHPioFpfdhwVGOYOgujMY= -github.com/mitchellh/go-testing-interface v1.14.2-0.20210217184823-a52172cd2f64/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-testing-interface v1.14.2-0.20210821155943-2d9075ca8770 h1:drhDO54gdT/a15GBcMRmunZiNcLgPiFIJa23KzmcvcU= +github.com/mitchellh/go-testing-interface v1.14.2-0.20210821155943-2d9075ca8770/go.mod h1:SO/iHr6q2EzbqRApt+8/E9wqebTwQn5y+UlB04bxzo0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= +github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= +github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= +github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= -github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= @@ -447,567 +120,57 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/tmccombs/hcl2json v0.4.0 h1:U4NsBBHsAh7lU0vIC6IwmRYwXWMG6dEWHVu4JTjpj1A= -github.com/tmccombs/hcl2json v0.4.0/go.mod h1:B0ZpBthAKbQur6yZRKrtaqDmYLCvgnwHOBApE0faCpU= -github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= -github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -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/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= -github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= -github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= -github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= -github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= -golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= -google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= -google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= -google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= -google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= -google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= -google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE= -google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= -google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= -google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= -google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= -google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= -google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +github.com/tmccombs/hcl2json v0.6.4 h1:/FWnzS9JCuyZ4MNwrG4vMrFrzRgsWEOVi+1AyYUVLGw= +github.com/tmccombs/hcl2json v0.6.4/go.mod h1:+ppKlIW3H5nsAsZddXPy2iMyvld3SHxyjswOZhavRDk= +github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg= +github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/zclconf/go-cty v1.16.4 h1:QGXaag7/7dCzb+odlGrgr+YmYZFaOCMW6DEpS+UD1eE= +github.com/zclconf/go-cty v1.16.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +gitlab.com/gitlab-org/api/client-go v1.46.0 h1:YxBWFZIFYKcGESCb9fpkwzouo+apyB9pr/XTWzNoL24= +gitlab.com/gitlab-org/api/client-go v1.46.0/go.mod h1:FtgyU6g2HS5+fMhw6nLK96GBEEBx5MzntOiJWfIaiN8= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/kube-openapi v0.0.0-20230224204730-66828de6f33b h1:4dkmFEDQj0ZBLKCxJ0R+qzhvZmEvRdRaaZAE06tR/Lg= -k8s.io/kube-openapi v0.0.0-20230224204730-66828de6f33b/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/kustomize/kyaml v0.14.0 h1:uzH0MzMtYypHW09LbMDk8k/lT/LSsUuCoZIuEGhIBNE= -sigs.k8s.io/kustomize/kyaml v0.14.0/go.mod h1:AN1/IpawKilWD7V+YvQwRGUvuUOOWpjsHu6uHwonSF4= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/kyaml v0.21.0 h1:7mQAf3dUwf0wBerWJd8rXhVcnkk5Tvn/q91cGkaP6HQ= +sigs.k8s.io/kustomize/kyaml v0.21.0/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/test/integration/harness_cluster_network/cluster_network_test.go b/test/integration/harness_cluster_network/cluster_network_test.go new file mode 100644 index 00000000..77c0b0d8 --- /dev/null +++ b/test/integration/harness_cluster_network/cluster_network_test.go @@ -0,0 +1,44 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package harness_cluster_network + +import ( + "testing" + "time" + + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" + "github.com/terraform-google-modules/terraform-google-secure-cicd/test/integration/testutils" +) + +func TestClusterNetwork(t *testing.T) { + setupOutput := tft.NewTFBlueprintTest(t, tft.WithTFDir("../../setup")) + + projectID := setupOutput.GetStringOutput("project_id_standalone") + region := setupOutput.GetStringOutput("primary_location") + + vars := map[string]interface{}{ + "project_id": projectID, + "region": region, + } + clusterNetworkPath := "../../setup/harness/cluster_network" + + clusterNetwork := tft.NewTFBlueprintTest(t, + tft.WithTFDir(clusterNetworkPath), + tft.WithVars(vars), + tft.WithRetryableTerraformErrors(testutils.RetryableTransientErrors, 3, 2*time.Minute), + tft.WithParallelism(100), + ) + clusterNetwork.Test() +} diff --git a/test/integration/harness_gitlab/gitlab_setup_test.go b/test/integration/harness_gitlab/gitlab_setup_test.go new file mode 100644 index 00000000..a015f7fd --- /dev/null +++ b/test/integration/harness_gitlab/gitlab_setup_test.go @@ -0,0 +1,54 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package harness_gitlab + +import ( + "testing" + "time" + + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" + "github.com/terraform-google-modules/terraform-google-secure-cicd/test/integration/testutils" +) + +func TestGitLab(t *testing.T) { + gitLabPath := "../../setup/harness/gitlab" + privateWorkerPoolPath := "../../setup/harness/private_workerpool" + privateWorkerPool := tft.NewTFBlueprintTest(t, + tft.WithTFDir(privateWorkerPoolPath), + ) + + loggingBucketPath := "../../setup/harness/logging_bucket" + loggingBucket := tft.NewTFBlueprintTest(t, + tft.WithTFDir(loggingBucketPath), + ) + + vars := map[string]interface{}{ + "network_name": privateWorkerPool.GetStringOutput("workerpool_network_name"), + "network_id": privateWorkerPool.GetStringOutput("workerpool_network_id"), + "project_id_standalone": privateWorkerPool.GetStringOutput("workerpool_project_id"), + "logging_kms_crypto_id": loggingBucket.GetStringOutput("bucket_kms_key"), + "logging_bucket_name": loggingBucket.GetStringOutput("logging_bucket"), + "attestation_kms_crypto_id": loggingBucket.GetStringOutput("attestation_kms_key"), + } + + gitLab := tft.NewTFBlueprintTest(t, + tft.WithTFDir(gitLabPath), + tft.WithRetryableTerraformErrors(testutils.RetryableTransientErrors, 3, 2*time.Minute), + tft.WithVars(vars), + tft.WithParallelism(100), + ) + gitLab.Test() + +} diff --git a/test/integration/harness_logging_bucket/logging_bucket_test.go b/test/integration/harness_logging_bucket/logging_bucket_test.go new file mode 100644 index 00000000..6247b7d4 --- /dev/null +++ b/test/integration/harness_logging_bucket/logging_bucket_test.go @@ -0,0 +1,34 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package harness_logging_bucket + +import ( + "testing" + "time" + + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" + "github.com/terraform-google-modules/terraform-google-secure-cicd/test/integration/testutils" +) + +func TestLoggingBucket(t *testing.T) { + loggingBucketPath := "../../setup/harness/logging_bucket" + + loggingBucket := tft.NewTFBlueprintTest(t, + tft.WithTFDir(loggingBucketPath), + tft.WithRetryableTerraformErrors(testutils.RetryableTransientErrors, 3, 2*time.Minute), + tft.WithParallelism(100), + ) + loggingBucket.Test() +} diff --git a/test/integration/harness_private_workerpool/private_workerpool_test.go b/test/integration/harness_private_workerpool/private_workerpool_test.go new file mode 100644 index 00000000..109e7711 --- /dev/null +++ b/test/integration/harness_private_workerpool/private_workerpool_test.go @@ -0,0 +1,35 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package herness_private_pool + +import ( + "testing" + "time" + + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" + "github.com/terraform-google-modules/terraform-google-secure-cicd/test/integration/testutils" +) + +func TestPrivateWorkerPool(t *testing.T) { + privateWorkerPoolPath := "../../setup/harness/private_workerpool" + + privateWorkerPool := tft.NewTFBlueprintTest(t, + tft.WithTFDir(privateWorkerPoolPath), + tft.WithRetryableTerraformErrors(testutils.RetryableTransientErrors, 3, 2*time.Minute), + tft.WithParallelism(100), + ) + privateWorkerPool.Test() + +} diff --git a/test/integration/harness_setup_gitlab/bootstrap_gitlab_environment_test.go b/test/integration/harness_setup_gitlab/bootstrap_gitlab_environment_test.go new file mode 100644 index 00000000..d014a19c --- /dev/null +++ b/test/integration/harness_setup_gitlab/bootstrap_gitlab_environment_test.go @@ -0,0 +1,213 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package harness_setup_gitlab + +import ( + "fmt" + "io" + "os" + "strings" + "testing" + "time" + + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" + "github.com/gruntwork-io/terratest/modules/shell" + "github.com/terraform-google-modules/terraform-google-secure-cicd/test/integration/testutils" + gitlab "gitlab.com/gitlab-org/api/client-go" +) + +// connects to a Google Cloud VM instance using SSH and retrieves the logs from the VM's Startup Script service +func readLogsFromVm(t *testing.T, instanceName string, instanceZone string, instanceProject string) (string, error) { + args := []string{"compute", "ssh", instanceName, fmt.Sprintf("--zone=%s", instanceZone), fmt.Sprintf("--project=%s", instanceProject), "-q", "--command=journalctl -u google-startup-scripts.service -n 20"} + gcloudCmd := shell.Command{ + Command: "gcloud", + Args: args, + } + return shell.RunCommandAndGetStdOutE(t, gcloudCmd) +} + +func TestValidateStartupScript(t *testing.T) { + // Retrieve output values from test setup + setup := tft.NewTFBlueprintTest(t, + tft.WithTFDir("../../setup/harness/gitlab"), + ) + instanceName := setup.GetStringOutput("gitlab_instance_name") + instanceZone := setup.GetStringOutput("gitlab_instance_zone") + gitlabSecretProject := setup.GetStringOutput("gitlab_secret_project") + // Periodically read logs from startup script running on the VM instance + for count := 0; count < 100; count++ { + logs, err := readLogsFromVm(t, instanceName, instanceZone, gitlabSecretProject) + if err != nil { + t.Fatal(err) + } + + if strings.Contains(logs, "Finished Google Compute Engine Startup Scripts") { + if strings.Contains(logs, "exit status 1") { + t.Fatal("ERROR: Startup Script finished with invalid exit status.") + } + break + } + time.Sleep(12 * time.Second) + } +} +func TestBootstrapGitlabVM(t *testing.T) { + caCert, err := os.ReadFile("/usr/local/share/ca-certificates/gitlab.crt") + + if err != nil { + t.Fatalf("Failed to read CA certificate: %v", err) + } + + // Retrieve output values from test setup + setup := tft.NewTFBlueprintTest(t, + tft.WithTFDir("../../setup/harness/gitlab"), + ) + + setupOutput := tft.NewTFBlueprintTest(t, tft.WithTFDir("../../setup")) + + clusterNetworkOutput := tft.NewTFBlueprintTest(t, tft.WithTFDir("../../setup/harness/cluster_network")) + + privateWorkerPoolOutput := tft.NewTFBlueprintTest(t, tft.WithTFDir("../../setup/harness/private_workerpool")) + + projectID := setupOutput.GetStringOutput("project_id_standalone") + region := setupOutput.GetStringOutput("primary_location") + + gitlabSecretProject := setup.GetStringOutput("gitlab_secret_project") + external_url := setup.GetStringOutput("gitlab_url") + url := "https://gitlab.example.com" + serviceDirectory := setup.GetStringOutput("gitlab_service_directory") + gitlabSecretProjectNumber := setup.GetStringOutput("gitlab_project_number") + gitlabPersonalTokenSecretName := setup.GetStringOutput("gitlab_pat_secret_name") + gitlabWebhookSecretId := setup.GetStringOutput("webhook_secret_id") + gitlabTokenSecretId := fmt.Sprintf("projects/%s/secrets/%s", gitlabSecretProjectNumber, gitlabPersonalTokenSecretName) + privateWorkerPoolId := privateWorkerPoolOutput.GetStringOutput("workerpool_id") + clusterNetworkName := clusterNetworkOutput.GetStringOutput("network_name") + + token, err := testutils.GetSecretFromSecretManager(t, gitlabPersonalTokenSecretName, gitlabSecretProject) + if err != nil { + t.Fatal(err) + } + git, err := gitlab.NewClient(token, gitlab.WithBaseURL(external_url)) + if err != nil { + t.Fatal(err) + } + repos := []string{ + "secure-ci", + "secure-cd", + } + + for _, repo := range repos { + p := &gitlab.CreateProjectOptions{ + Name: gitlab.Ptr(repo), + Description: gitlab.Ptr("Test Repo"), + InitializeWithReadme: gitlab.Ptr(true), + Visibility: gitlab.Ptr(gitlab.PrivateVisibility), + DefaultBranch: gitlab.Ptr("master"), + } + project, _, err := git.Projects.CreateProject(p) + if err != nil { + t.Error(err) + } else { + t.Log(project.WebURL) + t.Log(project.Name) + } + + } + + root := "../../.." + + // Replace Service Directory + err = testutils.ReplacePatternInTfVars("{PROJECT_ID}", projectID, root) + if err != nil { + t.Fatal(err) + } + + err = testutils.ReplacePatternInTfVars("{REGION}", region, root) + if err != nil { + t.Fatal(err) + } + + // Replace gitlab.com/user with custom self hosted URL using the root namespace + + replacement := fmt.Sprintf("%s/root", url) + err = testutils.ReplacePatternInTfVars("https://gitlab.com/user", replacement, root) + if err != nil { + t.Fatal(err) + } + + // Replace https://gitlab.com with custom self hosted URL + err = testutils.ReplacePatternInTfVars("https://gitlab.com", url, root) + if err != nil { + t.Fatal(err) + } + + // Replace SSL Cert + err = testutils.ReplacePatternInTfVars("REPLACE_WITH_SSL_CERT\n", string(caCert), root) + if err != nil { + t.Fatal(err) + } + + // Replace Service Directory + err = testutils.ReplacePatternInTfVars("REPLACE_WITH_SERVICE_DIRECTORY", serviceDirectory, root) + if err != nil { + t.Fatal(err) + } + + // Replace webhook secret id + err = testutils.ReplacePatternInTfVars("REPLACE_WITH_WEBHOOK_SECRET_ID", gitlabWebhookSecretId, root) + if err != nil { + t.Fatal(err) + } + + // Replace gitlab token secret ids + err = testutils.ReplacePatternInTfVars("REPLACE_WITH_READ_API_SECRET_ID", gitlabTokenSecretId, root) + if err != nil { + t.Fatal(err) + } + + // Replace secret project_id + err = testutils.ReplacePatternInTfVars("REPLACE_WITH_SECRET_PROJECT_ID", gitlabSecretProject, root) + if err != nil { + t.Fatal(err) + } + + err = testutils.ReplacePatternInTfVars("REPLACE_WITH_READ_USER_SECRET_ID", gitlabTokenSecretId, root) + if err != nil { + t.Fatal(err) + } + + err = testutils.ReplacePatternInTfVars("{REPLACE_WITH_WORKER_POOL_ID}", privateWorkerPoolId, root) + if err != nil { + t.Fatal(err) + } + + err = testutils.ReplacePatternInTfVars("{REPLACE_WITH_NETWORK_NAME}", clusterNetworkName, root) + if err != nil { + t.Fatal(err) + } + + filePath := "../../../examples/standalone_single_project/terraform.tfvars" + + file, err := os.Open(filePath) + if err != nil { + t.Fatal(err) + } + + b, err := io.ReadAll(file) + if err != nil { + t.Fatal(err) + } + + t.Log(string(b)) +} diff --git a/test/integration/standalone_single_project/standalone_single_project_test.go b/test/integration/standalone_single_project/standalone_single_project_test.go index 392de1e5..07ee073f 100644 --- a/test/integration/standalone_single_project/standalone_single_project_test.go +++ b/test/integration/standalone_single_project/standalone_single_project_test.go @@ -1,33 +1,32 @@ -/** - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package standalone_single_project import ( "fmt" + "log" + "os" + "os/exec" + "path/filepath" "strings" "testing" - // "time" // Uncomment if using RetryableErrors or Polling - - "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/gcloud" + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/git" "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" - - // "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/utils" // Uncomment if using Polling "github.com/stretchr/testify/assert" + "github.com/terraform-google-modules/terraform-google-secure-cicd/test/integration/testutils" "github.com/tidwall/gjson" ) @@ -47,126 +46,132 @@ func assertHasRole(assert *assert.Assertions, bindings []gjson.Result, role stri assert.True(found, fmt.Sprintf("Role '%s' should be granted to '%s' on %s", role, expectedMember, resourceName)) } -func TestStandaloneSingleProjectExample(t *testing.T) { +func runCmd(t *testing.T, dir, name string, args ...string) { + cmd := exec.Command(name, args...) + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + t.Logf("Running command in %s: %s %v", dir, name, args) + if err := cmd.Run(); err != nil { + t.Fatalf("Command failed in %s: %s %v, error: %v", dir, name, args, err) + } +} - // 1. Initialize the Setup Environment - setupOutput := tft.NewTFBlueprintTest(t, tft.WithTFDir("../../setup")) - projectID := setupOutput.GetStringOutput("project_id") - // If you added outputs to the setup module (like network details), grab them here. +func setupGitOperations(t *testing.T, bpFolder, wsFolder, ciRepoName string, cdRepoName string) { + t.Log("Starting Git Operations Setup...") - // 2. Define Variables for the Main Module - vars := map[string]interface{}{ - "project_id": projectID, - "region": "us-central1", // Or pull from setup if dynamic - // Add other necessary variables that your standalone_single_project requires + gitLabPath := "../../setup/harness/gitlab" + gitLab := tft.NewTFBlueprintTest(t, tft.WithTFDir(gitLabPath)) + gitUrl := gitLab.GetStringOutput("gitlab_url") + gitlabPersonalTokenSecretName := gitLab.GetStringOutput("gitlab_pat_secret_name") + gitlabSecretProject := gitLab.GetStringOutput("gitlab_secret_project") + + token, err := testutils.GetSecretFromSecretManager(t, gitlabPersonalTokenSecretName, gitlabSecretProject) + if err != nil { + t.Fatal(err) } - // 3. Initialize the Main Blueprint Test - standaloneSingleProjT := tft.NewTFBlueprintTest(t, - tft.WithVars(vars), - tft.WithTFDir("../../../examples/standalone_single_project"), - // tft.WithRetryableTerraformErrors(testutils.RetryableTransientErrors, 3, 2*time.Minute), // Highly recommended if you copy the testutils package - ) + hostNameWithPath := strings.Split(gitUrl, "https://")[1] + ciRepoUrl := fmt.Sprintf("https://oauth2:%s@%s/root/%s", token, hostNameWithPath, ciRepoName) + cdRepoUrl := fmt.Sprintf("https://oauth2:%s@%s/root/%s", token, hostNameWithPath, cdRepoName) - // 4. Define the Custom Verifier - standaloneSingleProjT.DefineVerify(func(assert *assert.Assertions) { - // Perform default verification ensuring Terraform reports no additional changes - // standaloneSingleProjT.DefaultVerify(assert) - - // Fetch Outputs - region := standaloneSingleProjT.GetStringOutput("region") - garRepoName := standaloneSingleProjT.GetStringOutput("gar_repo") - pipelineFullID := standaloneSingleProjT.GetStringOutput("clouddeploy_pipeline_id") - - // Parse the pipeline name from the full ID string - pipelineParts := strings.Split(pipelineFullID, "/") - pipelineName := pipelineParts[len(pipelineParts)-1] - - buildSAEmail := fmt.Sprintf("serviceAccount:%s", standaloneSingleProjT.GetStringOutput("build_sa_email")) - deploySAEmail := fmt.Sprintf("serviceAccount:%s", standaloneSingleProjT.GetStringOutput("deploy_sa_email")) - - // Handle the comma-separated cluster names - clusterNamesString := standaloneSingleProjT.GetStringOutput("cluster_names") - clusterNames := strings.Split(clusterNamesString, ",") - assert.NotEmpty(clusterNamesString, "cluster_names output should not be empty") - - // ==================================================================== - // A. Artifact Registry Verification - // ==================================================================== - garCommand := fmt.Sprintf("artifacts repositories describe %s --location=%s --project=%s --format=json", garRepoName, region, projectID) - garResult := gcloud.Run(t, garCommand) - - assert.Equal("DOCKER", garResult.Get("format").String(), "Artifact Registry format should be DOCKER") - - // ==================================================================== - // B. Cloud Deploy Pipeline Verification - // ==================================================================== - deployCommand := fmt.Sprintf("deploy delivery-pipelines describe %s --region=%s --project=%s --format=json", pipelineName, region, projectID) - deployResult := gcloud.Run(t, deployCommand) - - assert.False(deployResult.Get("suspended").Bool(), "Cloud Deploy pipeline should be active (not suspended)") - - stages := deployResult.Get("serialPipeline.stages").Array() - assert.NotEmpty(stages, "Cloud Deploy pipeline should have at least one stage configured") - - // ==================================================================== - // C. GKE Clusters Security Posture Verification - // ==================================================================== - for _, clusterName := range clusterNames { - if clusterName == "" { - continue - } + // Configure Git + runCmd(t, wsFolder, "git", "config", "--global", "user.email", "test-user@example.com") + runCmd(t, wsFolder, "git", "config", "--global", "user.name", "Test User") - clusterCmd := fmt.Sprintf("container clusters describe %s --region=%s --project=%s --format=json", clusterName, region, projectID) - clusterResult := gcloud.Run(t, clusterCmd) + cdRepoLocalPath := filepath.Join(wsFolder, cdRepoName) - // Cluster Status - assert.Equal("RUNNING", clusterResult.Get("status").String(), fmt.Sprintf("Cluster %s should be RUNNING", clusterName)) + cdGit := git.NewCmdConfig(t, git.WithDir(wsFolder)) + cdGitRun := func(args ...string) { + _, err := cdGit.RunCmdE(args...) + if err != nil { + t.Fatalf("CD Git command failed in %s: %v", wsFolder, err) + } + } - // Network Security: Ensure private nodes - isPrivate := clusterResult.Get("privateClusterConfig.enablePrivateNodes").Bool() - assert.True(isPrivate, fmt.Sprintf("Cluster %s should have private nodes enabled", clusterName)) + cdGitRun("clone", cdRepoUrl, cdRepoLocalPath) + cdGit = git.NewCmdConfig(t, git.WithDir(cdRepoLocalPath)) + cdGitRun = func(args ...string) { + _, err := cdGit.RunCmdE(args...) + if err != nil { + t.Fatalf("CD Git command failed in %s: %v", cdRepoLocalPath, err) + } + } + cdGitRun("checkout", "-b", "main") + + cfdYamlDest := filepath.Join(cdRepoLocalPath, "cloudbuild-cd.yaml") + runCmd(t, bpFolder, "cp", "/workspace/build/cloudbuild-cd.yaml", cfdYamlDest) - // Workload Identity Posture - expectedWorkloadPool := fmt.Sprintf("%s.svc.id.goog", projectID) - assert.Equal(expectedWorkloadPool, clusterResult.Get("workloadIdentityConfig.workloadPool").String(), fmt.Sprintf("Cluster %s workloadPool should be configured securely", clusterName)) + cdGitRun("add", ".") + cdGit.CommitWithMsg("initial commit", []string{"--allow-empty"}) + cdGitRun("push", "origin", "main", "--force") - // Binary Authorization Posture - binAuthzMode := clusterResult.Get("binaryAuthorization.evaluationMode").String() - assert.Equal("PROJECT_SINGLETON_POLICY_ENFORCE", binAuthzMode, fmt.Sprintf("Binary Authorization should be enforced on cluster %s", clusterName)) + appRepoLocalPath := filepath.Join(wsFolder, "bank-of-anthos") - // Shielded Nodes Posture - shieldedNodes := clusterResult.Get("shieldedNodes.enableSecureBoot").Bool() - assert.True(shieldedNodes, fmt.Sprintf("Shielded Nodes Secure Boot should be enabled on cluster %s", clusterName)) + appGit := git.NewCmdConfig(t, git.WithDir(wsFolder)) + appGitRun := func(args ...string) { + _, err := appGit.RunCmdE(args...) + if err != nil { + t.Fatalf("CI Git command failed in %s: %v", wsFolder, err) } + } - // ==================================================================== - // D. IAM & Principle of Least Privilege Verification - // ==================================================================== + appGitRun("clone", "--branch", "v0.5.11", "https://github.com/GoogleCloudPlatform/bank-of-anthos.git", appRepoLocalPath) + appGit = git.NewCmdConfig(t, git.WithDir(appRepoLocalPath)) + appGitRun = func(args ...string) { + _, err := appGit.RunCmdE(args...) + if err != nil { + t.Fatalf("Git command failed in %s: %v", appRepoLocalPath, err) + } + } + appGitRun("checkout", "-b", "main") + + ciYamlDest := filepath.Join(appRepoLocalPath, "cloudbuild-ci.yaml") + runCmd(t, bpFolder, "cp", "/workspace/build/cloudbuild-ci.yaml", ciYamlDest) - // 1. Fetch Project-level IAM policy - projectIamCmd := fmt.Sprintf("projects get-iam-policy %s --format=json", projectID) - projectIam := gcloud.Run(t, projectIamCmd) - projectBindings := projectIam.Get("bindings").Array() + policiesDest := filepath.Join(appRepoLocalPath, "policies") + runCmd(t, bpFolder, "cp", "-R", "/workspace/build/policies", policiesDest) - // 2. Fetch GAR-level IAM policy - garIamCmd := fmt.Sprintf("artifacts repositories get-iam-policy %s --location=%s --project=%s --format=json", garRepoName, region, projectID) - garIamResult := gcloud.Run(t, garIamCmd) - garBindings := garIamResult.Get("bindings").Array() + appGitRun("remote", "add", "gitlab", ciRepoUrl) + appGitRun("add", ".") + appGit.CommitWithMsg("initial commit", []string{"--allow-empty"}) + appGitRun("push", "gitlab", "main", "--force") + t.Log("Finished Git Operations Setup.") +} - // Assert CI Pipeline (Build SA) Permissions - assertHasRole(assert, garBindings, "roles/artifactregistry.writer", buildSAEmail, "Artifact Registry") - // (Assume KMS is bound at project level for this example, adjust if bound to key) - assertHasRole(assert, projectBindings, "roles/cloudkms.signerVerifier", buildSAEmail, "Project (KMS Signer)") +func TestStandaloneSingleProjectExample(t *testing.T) { - // Assert CD Pipeline (Deploy SA) Permissions - assertHasRole(assert, projectBindings, "roles/clouddeploy.jobRunner", deploySAEmail, "Project (Cloud Deploy Runner)") - assertHasRole(assert, projectBindings, "roles/container.developer", deploySAEmail, "Project (GKE Developer)") + setupOutput := tft.NewTFBlueprintTest(t, tft.WithTFDir("../../setup")) + projectID := setupOutput.GetStringOutput("project_id_standalone") + region := setupOutput.GetStringOutput("primary_location") - }) + blueprintFolder := ("../../../../") - // Optional: Define custom teardown if necessary (e.g., deleting leftover Cloud Build artifacts or GAR images) - // standaloneSingleProjT.DefineTeardown(func(assert *assert.Assertions) { ... }) + workspaceFolder, err := os.MkdirTemp("/tmp", "workspace-*") + if err != nil { + t.Fatalf("Failed to create temporary workspace: %v", err) + } + log.Printf("Created workspace folder: %s", workspaceFolder) + defer os.RemoveAll(workspaceFolder) + + vars := map[string]interface{}{ + "project_id": projectID, + "region": region, + } + + standaloneSingleProjT := tft.NewTFBlueprintTest(t, + tft.WithVars(vars), + tft.WithTFDir("../../../examples/standalone_single_project"), + ) + + standaloneSingleProjT.DefineVerify(func(assert *assert.Assertions) { + + ciRepoName := standaloneSingleProjT.GetStringOutput("ci_repo_name") + cdRepoName := standaloneSingleProjT.GetStringOutput("cd_repo_name") + setupGitOperations(t, blueprintFolder, workspaceFolder, ciRepoName, cdRepoName) + + standaloneSingleProjT.DefaultVerify(assert) + }) // 5. Execute the Test standaloneSingleProjT.Test() diff --git a/test/integration/testutils/retry.go b/test/integration/testutils/retry.go new file mode 100644 index 00000000..6ea7211b --- /dev/null +++ b/test/integration/testutils/retry.go @@ -0,0 +1,72 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +var ( + RetryableTransientErrors = map[string]string{ + // Error 409: unable to queue the operation + ".*Error 409.*unable to queue the operation.*": "Unable to queue operation.", + + // Error code 409 for concurrent policy changes. + ".*Error 409.*There were concurrent policy changes.*": "Concurrent policy changes.", + + // Error 403: Compute Engine API has not been used in project {} before or it is disabled. + ".*Error 403.*Compute Engine API has not been used in project.*": "Compute Engine API not enabled", + + // Error 403: Kubernetes Engine API has not been used in project {} before or it is disabled. + ".*Error 403.*Kubernetes Engine API is not enabled for this project*": "Kubernetes Engine API not enabled", + + // google_gke_hub_feature - Error: Error waiting to create Feature: Error waiting for Creating Feature: Error code 13, message: an internal error has occurred + ".*Error waiting for Creating Feature: Error code 13, message: an internal error has occurred*.": "Error creating feature", + + ".*Error waiting for Creating Connection: Error code 9, message: Failed to verify authorizer_credential.*": "servicedirectory.networks.access propagation time", + + // Request had invalid authentication credentials.* + ".*Request had invalid authentication credentials.*": "Request had invalid authentication credentials.", + + // generic::permission_denied: Request is prohibited by organization's policy. + ".*Request is prohibited by organization's policy.*": "VPC-SC propagation.", + + ".*does not match the eTag of the current version.*": "VPC-SC eTag consistency.", + + ".*Error code 3, message: Request contains an invalid argument.*": "Invalid Argument on Artifact Registry Creation", + + ".*another operation is in progress on this scope.*": "another operation is in progress on this scope", + + ".*Error when reading or editing ServicePerimeterResource.*": "Propagation issues on Service Perimeter.", + + ".*Error when reading or editing AccessLevelCondition.*": "Propagation issues on Access Level.", + + ".*Error 400.*Invalid Directional Policies set in Perimeter.*": "Propagation issues on Service Perimeter.", + + ".*Error 400.*@.*.iam.gserviceaccount.com.*is invalid or non-existent.*": "Service Agent propagation.", + + ".*Error 400.*@.*.iam.gserviceaccount.com.*does not exist.*": "Service Agent propagation.", + + ".*Error 400: Identity Pool does not exist (.*).*": "Identity pool propagation.", + + ".*dial tcp: lookup.*.sslip.io on.*: server misbehaving.*": "VM Gitlab issues.", + + ".*Error 400.*The subnetwork resource.*projects/.*/regions/.*/subnetworks/.*is already being used by.*, resourceInUseByAnotherResource": "Destroy resources usage propagation issues", + + ".*The network resource.*projects/.*/global/networks/.*is already being used by.*": "Destroy resources usage propagation issues", + + ".*Error waiting for Disabling Shared VPC Resource: The resource.*.is still linked to shared VPC host.*": "Destroy resources usage propagation issues", + + ".*Error 400.*Invalid value for field.*resource.networkInterfaces[0].subnetwork.*:.*projects/.*/regions/.*/subnetworks/.*. The referenced subnetwork resource cannot be found.*": "Network propagation", + + ".*Error: Error creating FeatureMembership: Resource already exists - apply blocked by lifecycle params.*": "Duplicated membership request", + } +) diff --git a/test/integration/testutils/utils.go b/test/integration/testutils/utils.go index fd097ae3..e1b7bea8 100644 --- a/test/integration/testutils/utils.go +++ b/test/integration/testutils/utils.go @@ -1,3 +1,17 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package testutils import ( @@ -25,7 +39,6 @@ func GetSecretFromSecretManager(t *testing.T, secretName string, secretProject s return shell.RunCommandAndGetStdOutE(t, gcloudCmd) } -// ReplacePatternInTfVars will walk directories searching for terraform.tfvars and replace the pattern with the replacement func ReplacePatternInTfVars(pattern string, replacement string, root string) error { err := filepath.WalkDir(root, func(path string, d fs.DirEntry, fnErr error) error { if fnErr != nil { @@ -40,7 +53,6 @@ func ReplacePatternInTfVars(pattern string, replacement string, root string) err return err } -// ReplacePatternInFile will walk directories searching for fileName and replace the pattern with the replacement func ReplacePatternInFile(pattern string, replacement string, root string, fileName string) error { err := filepath.WalkDir(root, func(path string, d fs.DirEntry, fnErr error) error { if fnErr != nil { @@ -55,7 +67,6 @@ func ReplacePatternInFile(pattern string, replacement string, root string, fileN return err } -// replaceInFile will replace oldPattern in filePath with newPattern func replaceInFile(filePath, oldPattern, newPattern string) error { fileInfo, err := os.Lstat(filePath) if err != nil { diff --git a/test/setup/harness/cluster_network/main.tf b/test/setup/harness/cluster_network/main.tf new file mode 100644 index 00000000..bed07dd8 --- /dev/null +++ b/test/setup/harness/cluster_network/main.tf @@ -0,0 +1,47 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + subnets = { + for env in var.envs : env => { + subnet_name = "${var.app_name}-subnet-${env}" + subnet_ip = "10.${var.ip_increment[env]}.0.0/17" + subnet_region = var.region + subnet_private_access = "true" + } + } + secondary_ranges = { + for env in var.envs : local.subnets[env].subnet_name => [ + { + range_name = "${local.subnets[env].subnet_name}-gke-pods" + ip_cidr_range = "10.1${var.ip_increment[env]}.0.0/16" + }, + { + range_name = "${local.subnets[env].subnet_name}-gke-services" + ip_cidr_range = "10.10${var.ip_increment[env]}.0.0/20" + }, + ] + } +} + +module "vpc" { + source = "../../modules/cluster_network" + + project_id = var.project_id + vpc_name = "${var.app_name}-vpc" + subnets = values(local.subnets) + secondary_ranges = local.secondary_ranges +} diff --git a/test/setup/harness/cluster_network/outputs.tf b/test/setup/harness/cluster_network/outputs.tf new file mode 100644 index 00000000..3e9661b8 --- /dev/null +++ b/test/setup/harness/cluster_network/outputs.tf @@ -0,0 +1,34 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_self_links" { + description = "The self-links of the created subnets." + value = module.vpc.subnets_self_links +} + +output "network_name" { + description = "The name of the created network." + value = module.vpc.network_name +} +output "network_id" { + description = "The ID of the created network." + value = module.vpc.network_id +} + +output "project_id" { + description = "The project ID of the network." + value = module.vpc.project_id +} diff --git a/test/setup/harness/cluster_network/variables.tf b/test/setup/harness/cluster_network/variables.tf new file mode 100644 index 00000000..16d809d8 --- /dev/null +++ b/test/setup/harness/cluster_network/variables.tf @@ -0,0 +1,48 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "app_name" { + description = "The name of the application." + type = string + default = "ci-cd" +} + +variable "envs" { + description = "The environments to create resources for." + type = list(string) + default = ["dev", "qa", "prod"] +} + +variable "ip_increment" { + description = "A map of environments to IP increments." + type = map(number) + default = { + dev = 1, + qa = 2, + prod = 3 + } +} + +variable "region" { + description = "The region to create resources in." + type = string + default = null +} + +variable "project_id" { + description = "The project ID to deploy to." + type = string +} diff --git a/test/setup/harness/gitlab/gitlab_vm.tf b/test/setup/harness/gitlab/gitlab_vm.tf index 62248be1..fba2377e 100644 --- a/test/setup/harness/gitlab/gitlab_vm.tf +++ b/test/setup/harness/gitlab/gitlab_vm.tf @@ -1,34 +1,129 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { - gitlab_network_url = "https://www.googleapis.com/compute/v1/projects/${var.project_id}/global/networks/${var.network_name}" + gitlab_network_id_without_location = replace(var.network_id, "locations/", "") + gitlab_network_url = "https://www.googleapis.com/compute/v1/projects/${var.project_id_standalone}/global/networks/${var.network_name}" + gitlab_vm_ip_range = "10.2.2.0/24" +} + +data "google_project" "gitlab_project" { + project_id = var.project_id_standalone +} + +data "google_storage_project_service_account" "gitlab_gcs_account" { + project = var.project_id_standalone +} + +resource "google_project_iam_member" "allow_gitlab_bucket_download" { + project = var.project_id_standalone + role = "roles/storage.objectUser" + member = "serviceAccount:${var.cloud_build_sa}" +} + +resource "google_project_iam_member" "allow_gitlab_iam_policy_edit" { + project = var.project_id_standalone + role = "roles/resourcemanager.projectIamAdmin" + member = "serviceAccount:${var.cloud_build_sa}" } resource "google_service_account" "gitlab_vm" { account_id = "gitlab-vm-sa" - project = var.project_id + project = var.project_id_standalone display_name = "Custom SA for VM Instance" } resource "google_project_iam_member" "secret_manager_admin_vm_instance" { - project = var.project_id + project = var.project_id_standalone role = "roles/secretmanager.admin" member = google_service_account.gitlab_vm.member } -resource "google_compute_subnetwork" "gitlab_subnet" { - project = var.project_id - name = "gitlab-vm-subnet" - ip_cidr_range = "10.2.2.0/24" - region = "us-central1" - network = var.network_id +resource "google_service_account_iam_member" "impersonate" { + service_account_id = google_service_account.gitlab_vm.id + role = "roles/iam.serviceAccountUser" + member = "serviceAccount:${var.cloud_build_sa}" +} + +resource "google_project_iam_member" "int_test_gitlab_permissions" { + for_each = toset([ + "roles/compute.instanceAdmin", + "roles/secretmanager.admin" + ]) + project = var.project_id_standalone + role = each.value + member = "serviceAccount:${var.cloud_build_sa}" +} + +resource "google_kms_crypto_key_iam_member" "logging_crypto_encrypt_key" { + for_each = { "gcs_account" : data.google_storage_project_service_account.gitlab_gcs_account.member, "gitlab_sa" : google_service_account.gitlab_vm.member } + crypto_key_id = var.logging_kms_crypto_id + role = "roles/cloudkms.cryptoKeyEncrypter" + member = each.value +} + +resource "google_kms_crypto_key_iam_member" "attestation_crypto_encrypt_key" { + for_each = { "gcs_account" : data.google_storage_project_service_account.gitlab_gcs_account.member, "gitlab_sa" : google_service_account.gitlab_vm.member } + crypto_key_id = var.attestation_kms_crypto_id + role = "roles/cloudkms.cryptoKeyEncrypter" + member = each.value +} + +resource "google_kms_crypto_key_iam_member" "logging_crypto_decrypt_key" { + for_each = { "gcs_account" : data.google_storage_project_service_account.gitlab_gcs_account.member, "gitlab_sa" : google_service_account.gitlab_vm.member } + crypto_key_id = var.logging_kms_crypto_id + role = "roles/cloudkms.cryptoKeyDecrypter" + member = each.value +} + +resource "google_kms_crypto_key_iam_member" "attestation_crypto_decrypt_key" { + for_each = { "gcs_account" : data.google_storage_project_service_account.gitlab_gcs_account.member, "gitlab_sa" : google_service_account.gitlab_vm.member } + crypto_key_id = var.attestation_kms_crypto_id + role = "roles/cloudkms.cryptoKeyDecrypter" + member = each.value +} + +resource "google_storage_bucket_iam_member" "logging_storage_admin" { + for_each = { "gcs_account" : data.google_storage_project_service_account.gitlab_gcs_account.member, "gitlab_sa" : google_service_account.gitlab_vm.member } + bucket = var.logging_bucket_name + role = "roles/storage.admin" + member = each.value +} + +resource "time_sleep" "waits_iam_propagation" { + create_duration = "2m" + depends_on = [ + google_storage_bucket_iam_member.logging_storage_admin, + google_kms_crypto_key_iam_member.attestation_crypto_encrypt_key, + google_kms_crypto_key_iam_member.attestation_crypto_decrypt_key, + google_kms_crypto_key_iam_member.logging_crypto_encrypt_key, + google_kms_crypto_key_iam_member.logging_crypto_decrypt_key, + google_project_iam_member.secret_manager_admin_vm_instance, + google_project_iam_member.int_test_gitlab_permissions, + ] } resource "google_compute_instance" "default" { name = "gitlab" - project = var.project_id + project = var.project_id_standalone machine_type = "n2-standard-4" zone = "us-central1-a" - tags = ["git-vm"] + tags = ["git-vm", "direct-gateway-access"] boot_disk { initialize_params { @@ -38,9 +133,11 @@ resource "google_compute_instance" "default" { network_interface { network = var.network_name - access_config {} # Ephemeral public IP + access_config { + // Ephemeral public IP + } subnetwork = google_compute_subnetwork.gitlab_subnet.name - subnetwork_project = var.project_id + subnetwork_project = var.project_id_standalone } metadata_startup_script = file("./../../scripts/gitlab_self_hosted.sh") @@ -49,53 +146,129 @@ resource "google_compute_instance" "default" { email = google_service_account.gitlab_vm.email scopes = ["cloud-platform"] } + depends_on = [time_sleep.waits_iam_propagation] +} + +resource "google_compute_subnetwork" "gitlab_subnet" { + project = var.project_id_standalone + name = "gitlab-vm-subnet" + ip_cidr_range = "10.2.2.0/24" + + region = "us-central1" + network = var.network_id } resource "google_secret_manager_secret" "gitlab_webhook" { - project = var.project_id + project = var.project_id_standalone secret_id = "gitlab-webhook" replication { auto {} } } -resource "random_uuid" "random_webhook_secret" {} +resource "random_uuid" "random_webhook_secret" { +} resource "google_secret_manager_secret_version" "gitlab_webhook" { secret = google_secret_manager_secret.gitlab_webhook.id secret_data = random_uuid.random_webhook_secret.result } -# Firewall Rules +// ================================ +// FIREWALL RULES +// ================================ + resource "google_compute_firewall" "allow_iap_ssh" { name = "allow-iap-ssh" network = var.network_name - project = var.project_id + project = var.project_id_standalone + allow { ports = [22] protocol = "tcp" } + + source_ranges = ["35.235.240.0/20"] +} + +resource "google_compute_firewall" "allow_service_networking" { + name = "allow-service-networking" + network = var.network_name + project = var.project_id_standalone + + allow { + protocol = "all" + } + source_ranges = ["35.199.192.0/19"] } -resource "google_compute_firewall" "allow_http_https" { - name = "allow-http-https" +resource "google_compute_firewall" "allow_http" { + name = "allow-http" + network = var.network_name + project = var.project_id_standalone + + allow { + protocol = "tcp" + ports = ["80"] + } + + source_ranges = ["0.0.0.0/0"] + target_tags = ["git-vm"] +} + +resource "google_compute_firewall" "allow_https" { + name = "allow-https" network = var.network_name - project = var.project_id + project = var.project_id_standalone + allow { protocol = "tcp" - ports = ["80", "443"] + ports = ["443"] } + source_ranges = ["0.0.0.0/0"] target_tags = ["git-vm"] } -# Service Directory & DNS for Private Access +// ======================================================= +// GITLAB WORKER POOL AND PRIVATE DNS CONFIG +// ======================================================= +module "ssl_cert" { + source = "terraform-google-modules/cloud-storage/google//modules/simple_bucket" + version = "~> 10.0" + + name = "${var.project_id_standalone}-ssl-cert" + project_id = var.project_id_standalone + location = "us-central1" + log_bucket = var.logging_bucket_name + log_object_prefix = "ssl-cert" + force_destroy = true + + versioning = true + encryption = { default_kms_key_name = var.logging_kms_crypto_id } + + # Module does not support values not know before apply (member and role are used to create the index in for_each) + # https://github.com/terraform-google-modules/terraform-google-cloud-storage/blob/v10.0.2/modules/simple_bucket/main.tf#L122 + # iam_members = [{ + # role = "roles/storage.admin" + # member = "${google_service_account.gitlab_vm.member}" + # }] + + depends_on = [time_sleep.waits_iam_propagation] +} + +resource "google_storage_bucket_iam_member" "ssl_storage_admin" { + bucket = module.ssl_cert.name + role = "roles/storage.admin" + member = google_service_account.gitlab_vm.member +} + resource "google_service_directory_namespace" "gitlab" { provider = google-beta namespace_id = "gitlab-namespace" location = "us-central1" - project = var.project_id + project = var.project_id_standalone } resource "google_service_directory_service" "gitlab" { @@ -108,24 +281,28 @@ resource "google_service_directory_endpoint" "gitlab" { provider = google-beta endpoint_id = "endpoint" service = google_service_directory_service.gitlab.id - network = var.network_id - address = google_compute_instance.default.network_interface[0].network_ip - port = 443 + + network = var.network_id + address = google_compute_instance.default.network_interface[0].network_ip + port = 443 } resource "google_dns_managed_zone" "sd_zone" { - provider = google-beta + provider = google-beta + name = "peering-zone" dns_name = "example.com." - description = "Private DNS Service Directory zone for Gitlab Instance" - project = var.project_id - visibility = "private" + description = "Example private DNS Service Directory zone for Gitlab Instance" + project = var.project_id_standalone + + visibility = "private" service_directory_config { namespace { namespace_url = google_service_directory_namespace.gitlab.id } } + private_visibility_config { networks { network_url = local.gitlab_network_url @@ -133,32 +310,39 @@ resource "google_dns_managed_zone" "sd_zone" { } } -module "ssl_cert" { - source = "terraform-google-modules/cloud-storage/google//modules/simple_bucket" - version = "~> 10.0" - - name = "${var.project_id}-ssl-cert" - project_id = var.project_id - location = "us-central1" - log_bucket = var.logging_bucket_name - log_object_prefix = "ssl-cert" - force_destroy = true +resource "google_project_iam_member" "sd_viewer" { + project = var.project_id_standalone + role = "roles/servicedirectory.viewer" + member = "serviceAccount:service-${var.project_number_standalone}@gcp-sa-cloudbuild.iam.gserviceaccount.com" +} - versioning = true - encryption = { default_kms_key_name = var.logging_kms_crypto_id } +resource "google_project_iam_member" "access_network" { + project = var.project_id_standalone + role = "roles/servicedirectory.pscAuthorizedService" + member = "serviceAccount:service-${var.project_number_standalone}@gcp-sa-cloudbuild.iam.gserviceaccount.com" +} - # Module does not support values not know before apply (member and role are used to create the index in for_each) - # https://github.com/terraform-google-modules/terraform-google-cloud-storage/blob/v10.0.2/modules/simple_bucket/main.tf#L122 - # iam_members = [{ - # role = "roles/storage.admin" - # member = "${google_service_account.gitlab_vm.member}" - # }] +resource "google_project_iam_member" "cb_agent_pool_user" { + project = var.project_id_standalone + role = "roles/cloudbuild.workerPoolUser" + member = "serviceAccount:service-${var.project_number_standalone}@gcp-sa-cloudbuild.iam.gserviceaccount.com" +} - depends_on = [time_sleep.waits_iam_propagation] +resource "google_project_iam_member" "cb_sa_pool_user" { + project = var.project_id_standalone + role = "roles/cloudbuild.workerPoolUser" + member = "serviceAccount:${var.project_number_standalone}@cloudbuild.gserviceaccount.com" } -resource "google_storage_bucket_iam_member" "ssl_storage_admin" { - bucket = module.ssl_cert.name - role = "roles/storage.admin" - member = google_service_account.gitlab_vm.member +resource "google_service_networking_peered_dns_domain" "name" { + project = var.project_id_standalone + name = "example-com" + network = var.network_name + dns_suffix = "example.com." + + depends_on = [ + google_dns_managed_zone.sd_zone, + ] } + + diff --git a/test/setup/harness/gitlab/outputs.tf b/test/setup/harness/gitlab/outputs.tf index 46d1cf70..471c0975 100644 --- a/test/setup/harness/gitlab/outputs.tf +++ b/test/setup/harness/gitlab/outputs.tf @@ -1,4 +1,20 @@ -output "gitlab_webhook_secret_id" { +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "webhook_secret_id" { value = google_secret_manager_secret.gitlab_webhook.id } @@ -7,15 +23,23 @@ output "gitlab_pat_secret_name" { } output "gitlab_project_id" { - value = var.project_id + value = var.project_id_standalone +} + +output "gitlab_project_number" { + value = var.project_number_standalone } output "gitlab_url" { value = "https://${google_compute_instance.default.network_interface[0].access_config[0].nat_ip}.sslip.io" } +output "gitlab_internal_ip" { + value = google_compute_instance.default.network_interface[0].network_ip +} + output "gitlab_secret_project" { - value = var.project_id + value = var.project_id_standalone } output "gitlab_instance_zone" { @@ -29,3 +53,7 @@ output "gitlab_instance_name" { output "gitlab_service_directory" { value = google_service_directory_service.gitlab.id } + +output "gitlab_ssl_bucket_name" { + value = module.ssl_cert.name +} diff --git a/test/setup/harness/gitlab/variables.tf b/test/setup/harness/gitlab/variables.tf index 90c945cd..b6bab93e 100644 --- a/test/setup/harness/gitlab/variables.tf +++ b/test/setup/harness/gitlab/variables.tf @@ -1,4 +1,20 @@ -variable "project_id" { +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_standalone" { description = "The project id where Gitlab will run." type = string } @@ -18,7 +34,28 @@ variable "cloud_build_sa" { type = string } -variable "seed_project_number" { +variable "project_number_standalone" { description = "The seed project number." type = string -} \ No newline at end of file +} + +variable "bucket_kms_crypto_id" { + description = "KMS key id used to encrypt buckets." + type = string + default = null +} + +variable "attestation_kms_crypto_id" { + description = "KMS key id used for image attestation." + type = string +} + +variable "logging_bucket_name" { + description = "The name of the logging bucket." + type = string +} + +variable "logging_kms_crypto_id" { + description = "The KMS key for the logging bucket." + type = string +} diff --git a/test/setup/harness/gitlab/versions.tf b/test/setup/harness/gitlab/versions.tf index afd3cd67..1a5d8c9e 100644 --- a/test/setup/harness/gitlab/versions.tf +++ b/test/setup/harness/gitlab/versions.tf @@ -1,3 +1,19 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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" required_providers { diff --git a/test/setup/harness/logging_bucket/main.tf b/test/setup/harness/logging_bucket/main.tf new file mode 100644 index 00000000..9bdd40c8 --- /dev/null +++ b/test/setup/harness/logging_bucket/main.tf @@ -0,0 +1,94 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_string" "prefix" { + length = 6 + special = false + upper = false +} + +module "logging_bucket" { + source = "terraform-google-modules/cloud-storage/google//modules/simple_bucket" + version = "~> 10.0" + + name = "bkt-logging-${random_string.prefix.result}" + project_id = var.project_id_standalone + location = var.region + force_destroy = true + + versioning = true + encryption = { default_kms_key_name = module.kms.keys["bucket"] } +} + +resource "google_storage_bucket_iam_member" "logging_storage_admin" { + for_each = { "admin_ci" : "serviceAccount:${var.sa_email}" } + bucket = module.logging_bucket.name + role = "roles/storage.admin" + member = each.value +} + +data "google_storage_project_service_account" "ci_gcs_account" { + project = var.project_id_standalone +} + +module "kms" { + source = "terraform-google-modules/kms/google" + version = "~> 4.0" + + project_id = var.project_id_standalone + location = var.region + keyring = "kms-bucket-encryption" + keys = ["bucket"] + set_owners_for = ["bucket"] + owners = [ + "serviceAccount:${var.sa_email}", + ] + set_encrypters_for = ["bucket"] + encrypters = [ + "${data.google_storage_project_service_account.ci_gcs_account.member},${"serviceAccount:${var.sa_email}"},serviceAccount:${var.cloud_build_sa}", + ] + set_decrypters_for = ["bucket"] + decrypters = [ + "${data.google_storage_project_service_account.ci_gcs_account.member},${"serviceAccount:${var.sa_email}"},serviceAccount:${var.cloud_build_sa}", + ] + prevent_destroy = false +} + +module "kms_attestor" { + source = "terraform-google-modules/kms/google" + version = "~> 4.0" + + project_id = var.project_id_standalone + location = var.region + keyring = "kms-attestation-sign" + keys = ["attestation"] + set_owners_for = ["attestation"] + purpose = "ASYMMETRIC_SIGN" + key_algorithm = "RSA_SIGN_PKCS1_4096_SHA512" + key_rotation_period = null + owners = [ + "serviceAccount:${var.sa_email}", + ] + set_encrypters_for = ["attestation"] + encrypters = [ + "${data.google_storage_project_service_account.ci_gcs_account.member},${"serviceAccount:${var.sa_email}"},serviceAccount:${var.cloud_build_sa}", + ] + set_decrypters_for = ["attestation"] + decrypters = [ + "${data.google_storage_project_service_account.ci_gcs_account.member},${"serviceAccount:${var.sa_email}"},serviceAccount:${var.cloud_build_sa}", + ] + prevent_destroy = false +} diff --git a/test/setup/harness/logging_bucket/outputs.tf b/test/setup/harness/logging_bucket/outputs.tf new file mode 100644 index 00000000..e1bfce1b --- /dev/null +++ b/test/setup/harness/logging_bucket/outputs.tf @@ -0,0 +1,31 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "project_id_standalone" { + value = var.project_id_standalone +} + +output "logging_bucket" { + value = module.logging_bucket.name +} + +output "bucket_kms_key" { + value = module.kms.keys["bucket"] +} + +output "attestation_kms_key" { + value = module.kms_attestor.keys["attestation"] +} diff --git a/test/setup/harness/logging_bucket/variables.tf b/test/setup/harness/logging_bucket/variables.tf new file mode 100644 index 00000000..bc43cd71 --- /dev/null +++ b/test/setup/harness/logging_bucket/variables.tf @@ -0,0 +1,36 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_standalone" { + description = "The seed project." + type = string +} + +variable "sa_email" { + description = "The ci service account email created by setup to run the tests." + type = string +} + +variable "cloud_build_sa" { + description = "Cloud Build Service Account email to be granted Encrypt/Decrypt role." + type = string +} + +variable "region" { + description = "Region where KMS and Logging bucket will be deployed." + type = string + default = "us-central1" +} diff --git a/test/setup/harness/logging_bucket/versions.tf b/test/setup/harness/logging_bucket/versions.tf new file mode 100644 index 00000000..7fc9df44 --- /dev/null +++ b/test/setup/harness/logging_bucket/versions.tf @@ -0,0 +1,37 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 6.6 , < 7" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 6.6, < 7" + } + time = { + source = "hashicorp/time" + version = ">= 0.12.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.7.2" + } + } +} diff --git a/test/setup/harness/private_workerpool/outputs.tf b/test/setup/harness/private_workerpool/outputs.tf new file mode 100644 index 00000000..16280d30 --- /dev/null +++ b/test/setup/harness/private_workerpool/outputs.tf @@ -0,0 +1,43 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "workerpool_project_id" { + value = module.private_workerpool.workerpool_project_id +} + +output "workerpool_project_number" { + value = module.private_workerpool.workerpool_project_number +} + +output "workerpool_network_name" { + value = module.private_workerpool.workerpool_network_name +} + +output "workerpool_network_id" { + value = module.private_workerpool.workerpool_network_id +} + +output "workerpool_id" { + value = module.private_workerpool.workerpool_id +} + +output "workerpool_network_self_link" { + value = module.private_workerpool.workerpool_network_self_link +} + +output "workerpool_subnets_self_links" { + value = module.private_workerpool.workerpool_subnets_self_links +} diff --git a/test/setup/harness/private_workerpool/private_workerpool.tf b/test/setup/harness/private_workerpool/private_workerpool.tf new file mode 100644 index 00000000..6ebae003 --- /dev/null +++ b/test/setup/harness/private_workerpool/private_workerpool.tf @@ -0,0 +1,27 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "private_workerpool" { + source = "../../modules/private_workerpool" + network_name = "priv-workerpool" + workerpool_machine_type = var.workerpool_machine_type + workpool_region = var.workpool_region + project_id_standalone = var.project_id_standalone + org_id = var.org_id + billing_account = var.billing_account + folder_id = var.folder_id + project_number_standalone = var.project_number_standalone +} diff --git a/test/setup/harness/private_workerpool/variables.tf b/test/setup/harness/private_workerpool/variables.tf new file mode 100644 index 00000000..6572e423 --- /dev/null +++ b/test/setup/harness/private_workerpool/variables.tf @@ -0,0 +1,52 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "folder_id" { + description = "The folder to deploy in" + type = string +} + +variable "project_id_standalone" { + description = "The seed project id" + type = string +} + +variable "project_number_standalone" { + description = "The seed project number" + type = string +} + +variable "org_id" { + description = "The numeric organization id" + type = string +} + +variable "billing_account" { + description = "The billing account id associated with the project, e.g. XXXXXX-YYYYYY-ZZZZZZ" + type = string +} + +variable "workpool_region" { + description = "The region to deploy in" + type = string + default = "us-central1" +} + +variable "workerpool_machine_type" { + description = "The project to deploy in" + type = string + default = "e2-standard-4" +} diff --git a/test/setup/harness/private_workerpool/versions.tf b/test/setup/harness/private_workerpool/versions.tf new file mode 100644 index 00000000..7fc9df44 --- /dev/null +++ b/test/setup/harness/private_workerpool/versions.tf @@ -0,0 +1,37 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 6.6 , < 7" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 6.6, < 7" + } + time = { + source = "hashicorp/time" + version = ">= 0.12.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.7.2" + } + } +} diff --git a/test/setup/iam.tf b/test/setup/iam.tf index a8beda20..3e174603 100644 --- a/test/setup/iam.tf +++ b/test/setup/iam.tf @@ -20,11 +20,14 @@ locals { "roles/binaryauthorization.attestorsAdmin", "roles/cloudbuild.builds.builder", "roles/cloudbuild.workerPoolOwner", + "roles/cloudbuild.admin", "roles/clouddeploy.admin", "roles/cloudkms.admin", "roles/cloudkms.publicKeyViewer", "roles/containeranalysis.notes.editor", + "roles/compute.admin", "roles/compute.networkAdmin", + "roles/compute.securityAdmin", "roles/gkehub.editor", "roles/iam.serviceAccountAdmin", "roles/iam.serviceAccountUser", @@ -33,7 +36,10 @@ locals { "roles/source.admin", "roles/storage.admin", "roles/resourcemanager.projectIamAdmin", - "roles/viewer" + "roles/viewer", + "roles/secretmanager.admin", + "roles/servicedirectory.admin", + "roles/dns.admin" ] gke_int_required_roles = [ "roles/compute.networkAdmin", @@ -44,48 +50,18 @@ locals { "roles/serviceusage.serviceUsageViewer", "roles/iam.serviceAccountUser" ] - gke_proj_role_mapping = flatten([ - for env in local.envs : [ - for role in local.gke_int_required_roles : { - project = module.gke_project[env].project_id - role = role - env = env - } - ] - ]) } resource "google_service_account" "int_test" { - project = module.project.project_id + project = module.project_standalone.project_id account_id = "ci-account" display_name = "ci-account" } -# SA permissions on CI/CD (main) project -resource "google_project_iam_member" "int_test" { - for_each = toset(local.int_required_roles) - - project = module.project.project_id - role = each.value - member = "serviceAccount:${google_service_account.int_test.email}" -} - -# SA permissions on GKE projects -resource "google_project_iam_member" "gke_int_test" { - for_each = { - for mapping in local.gke_proj_role_mapping : "${mapping.env}.${mapping.role}" => mapping - } - - project = each.value.project - role = each.value.role - member = "serviceAccount:${google_service_account.int_test.email}" -} - resource "google_service_account_key" "int_test" { service_account_id = google_service_account.int_test.id } -# SA permissions on standalone single project example resource "google_project_iam_member" "int_test_singleproj" { for_each = toset(local.int_required_roles) diff --git a/test/setup/main.tf b/test/setup/main.tf index dff793fc..a0e26ed2 100644 --- a/test/setup/main.tf +++ b/test/setup/main.tf @@ -14,52 +14,6 @@ * limitations under the License. */ -module "project" { - source = "terraform-google-modules/project-factory/google" - version = "~> 14.0" - - name = "ci-secure-cicd" - random_project_id = "true" - org_id = var.org_id - folder_id = var.folder_id - billing_account = var.billing_account - - activate_apis = [ - "cloudresourcemanager.googleapis.com", - "cloudbilling.googleapis.com", - "clouddeploy.googleapis.com", - "storage-api.googleapis.com", - "serviceusage.googleapis.com", - "cloudbuild.googleapis.com", - "containerregistry.googleapis.com", - "iamcredentials.googleapis.com", - "secretmanager.googleapis.com", - "sourcerepo.googleapis.com", - "artifactregistry.googleapis.com", - "containeranalysis.googleapis.com", - "cloudkms.googleapis.com", - "binaryauthorization.googleapis.com", - "containerscanning.googleapis.com", - "servicenetworking.googleapis.com", - "pubsub.googleapis.com", - ] - activate_api_identities = [ - { - api = "cloudbuild.googleapis.com" - roles = [ - "roles/storage.admin", - "roles/artifactregistry.admin", - "roles/cloudbuild.builds.builder", - "roles/binaryauthorization.attestorsVerifier", - "roles/cloudkms.cryptoOperator", - "roles/containeranalysis.notes.attacher", - "roles/containeranalysis.notes.occurrences.viewer", - "roles/source.writer", - ] - }, - ] -} - locals { envs = ["dev", "qa", "prod"] primary_location = "us-central1" @@ -70,186 +24,6 @@ locals { } } -# GKE Projects -module "gke_project" { - for_each = toset(local.envs) - source = "terraform-google-modules/project-factory/google" - version = "~> 14.0" - - name = "secure-cicd-gke-${each.value}" - random_project_id = "true" - org_id = var.org_id - folder_id = var.folder_id - billing_account = var.billing_account - default_service_account = "keep" - - activate_apis = [ - "cloudresourcemanager.googleapis.com", - "cloudbilling.googleapis.com", - "storage-api.googleapis.com", - "serviceusage.googleapis.com", - "containerregistry.googleapis.com", - "iamcredentials.googleapis.com", - "secretmanager.googleapis.com", - "artifactregistry.googleapis.com", - "containeranalysis.googleapis.com", - "cloudkms.googleapis.com", - "binaryauthorization.googleapis.com", - "containerscanning.googleapis.com", - "container.googleapis.com", - "cloudtrace.googleapis.com", - "monitoring.googleapis.com", - "logging.googleapis.com" - ] -} - -###### Public Clusters ##### -# VPCs -module "vpc" { - for_each = toset(local.envs) - source = "terraform-google-modules/network/google" - version = "~> 7.0" - - project_id = module.gke_project[each.value].project_id - network_name = "gke-vpc-${each.key}" - routing_mode = "REGIONAL" - - subnets = [ - { - subnet_name = "gke-subnet" - subnet_ip = "10.0.0.0/17" - subnet_region = local.primary_location - }, - ] - secondary_ranges = { - gke-subnet = [ - { - range_name = "us-central1-01-gke-01-pods" - ip_cidr_range = "192.168.0.0/18" - }, - { - range_name = "us-central1-01-gke-01-services" - ip_cidr_range = "192.168.64.0/18" - }, - ] - } -} - -module "gke_cluster" { - for_each = toset(local.envs) - source = "terraform-google-modules/kubernetes-engine/google" - version = "~> 25.0" - - - project_id = module.gke_project[each.value].project_id - name = "${each.key}-cluster" - regional = true - region = local.primary_location - zones = ["us-central1-a", "us-central1-b", "us-central1-f"] - network = module.vpc[each.key].network_name - subnetwork = module.vpc[each.key].subnets_names[0] - ip_range_pods = "us-central1-01-gke-01-pods" - ip_range_services = "us-central1-01-gke-01-services" - create_service_account = true - enable_binary_authorization = true - skip_provisioners = false - - release_channel = "REGULAR" - kubernetes_version = "latest" - - depends_on = [ - module.vpc - ] -} - - -###### Private Clusters ###### -# Private Cluster VPCs -module "vpc_private_cluster" { - for_each = toset(local.envs) - source = "terraform-google-modules/network/google" - version = "~> 7.0" - - project_id = module.gke_project[each.value].project_id - network_name = "gke-private-vpc-${each.value}" - routing_mode = "REGIONAL" - - subnets = [ - { - subnet_name = "gke-subnet-private" - subnet_ip = "10.0.0.0/17" - subnet_region = local.primary_location - }, - ] - secondary_ranges = { - gke-subnet-private = [ - { - range_name = "us-central1-01-gke-01-pods" - ip_cidr_range = "192.168.0.0/18" - }, - { - range_name = "us-central1-01-gke-01-services" - ip_cidr_range = "192.168.64.0/18" - }, - ] - } -} - -resource "google_compute_network_peering_routes_config" "gke_peering_routes_config" { - for_each = toset(local.envs) - - project = module.gke_project[each.value].project_id - peering = module.gke_private_cluster[each.value].peering_name - network = module.vpc_private_cluster[each.value].network_name - - import_custom_routes = true - export_custom_routes = true -} - -module "gke_private_cluster" { - for_each = toset(local.envs) - source = "terraform-google-modules/kubernetes-engine/google//modules/private-cluster" - version = "~> 25.0" - - project_id = module.gke_project[each.value].project_id - name = "${each.value}-private-cluster" - regional = true - region = local.primary_location - zones = ["us-central1-a", "us-central1-b", "us-central1-f"] - network = module.vpc_private_cluster[each.value].network_name - subnetwork = module.vpc_private_cluster[each.value].subnets_names[0] - ip_range_pods = "us-central1-01-gke-01-pods" - ip_range_services = "us-central1-01-gke-01-services" - horizontal_pod_autoscaling = true - create_service_account = true - enable_binary_authorization = true - - release_channel = "REGULAR" - kubernetes_version = "latest" - - enable_private_endpoint = true - enable_private_nodes = true - master_ipv4_cidr_block = "172.16.${local.ip_increment[each.value]}.0/28" - - enable_vertical_pod_autoscaling = true - - master_authorized_networks = [ - { - cidr_block = module.vpc_private_cluster[each.value].subnets_ips[0] - display_name = "VPC" - }, - { - cidr_block = "10.39.0.0/16" - display_name = "CLOUDBUILD" - } - ] - - depends_on = [ - module.vpc_private_cluster - ] -} - -# Single Project example module "project_standalone" { source = "terraform-google-modules/project-factory/google" version = "~> 14.0" @@ -262,28 +36,47 @@ module "project_standalone" { default_service_account = "keep" activate_apis = [ - "cloudresourcemanager.googleapis.com", + "accesscontextmanager.googleapis.com", + "aiplatform.googleapis.com", + "anthos.googleapis.com", + "anthosconfigmanagement.googleapis.com", + "anthospolicycontroller.googleapis.com", + "apikeys.googleapis.com", + "artifactregistry.googleapis.com", + "binaryauthorization.googleapis.com", + "certificatemanager.googleapis.com", "cloudbilling.googleapis.com", - "clouddeploy.googleapis.com", - "storage-api.googleapis.com", - "serviceusage.googleapis.com", "cloudbuild.googleapis.com", - "iamcredentials.googleapis.com", - "sourcerepo.googleapis.com", - "artifactregistry.googleapis.com", - "containeranalysis.googleapis.com", + "clouddeploy.googleapis.com", + "cloudfunctions.googleapis.com", "cloudkms.googleapis.com", - "binaryauthorization.googleapis.com", - "containerscanning.googleapis.com", - "servicenetworking.googleapis.com", - "pubsub.googleapis.com", - "container.googleapis.com", + "cloudresourcemanager.googleapis.com", "cloudtrace.googleapis.com", - "monitoring.googleapis.com", - "logging.googleapis.com", "compute.googleapis.com", + "container.googleapis.com", + "containeranalysis.googleapis.com", + "containerscanning.googleapis.com", "gkehub.googleapis.com", - "connectgateway.googleapis.com", + "iam.googleapis.com", + "iap.googleapis.com", + "mesh.googleapis.com", + "modelarmor.googleapis.com", + "monitoring.googleapis.com", + "multiclusteringress.googleapis.com", + "multiclusterservicediscovery.googleapis.com", + "networkmanagement.googleapis.com", + "networkservices.googleapis.com", + "notebooks.googleapis.com", + "orgpolicy.googleapis.com", + "secretmanager.googleapis.com", + "servicedirectory.googleapis.com", + "servicemanagement.googleapis.com", + "servicenetworking.googleapis.com", + "serviceusage.googleapis.com", + "sourcerepo.googleapis.com", + "sqladmin.googleapis.com", + "storage.googleapis.com", + "trafficdirector.googleapis.com", ] activate_api_identities = [ { @@ -301,3 +94,32 @@ module "project_standalone" { }, ] } + +resource "random_string" "kms_suffix" { + length = 4 + special = false + upper = false +} + +resource "google_kms_key_ring" "standalone_keyring" { + name = "standalone-keyring-${random_string.kms_suffix.result}" + location = local.primary_location + project = module.project_standalone.project_id + depends_on = [module.project_standalone] +} + +resource "google_kms_crypto_key" "standalone_bucket_key" { + name = "standalone-bucket-key" + key_ring = google_kms_key_ring.standalone_keyring.id + rotation_period = "7776000s" +} + +data "google_storage_project_service_account" "standalone_gcs_account" { + project = module.project_standalone.project_id +} + +resource "google_kms_crypto_key_iam_member" "standalone_gcs_sa_kms" { + crypto_key_id = google_kms_crypto_key.standalone_bucket_key.id + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + member = "serviceAccount:${data.google_storage_project_service_account.standalone_gcs_account.email_address}" +} diff --git a/test/setup/modules/cluster_network/main.tf b/test/setup/modules/cluster_network/main.tf new file mode 100644 index 00000000..5e813d84 --- /dev/null +++ b/test/setup/modules/cluster_network/main.tf @@ -0,0 +1,63 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_vpc" { + source = "terraform-google-modules/network/google" + version = "~> 13.0" + + project_id = var.project_id + network_name = "vpc-${var.vpc_name}" + shared_vpc_host = var.shared_vpc_host + + ingress_rules = var.ingress_rules + egress_rules = var.egress_rules + + subnets = var.subnets + secondary_ranges = var.secondary_ranges +} + +module "cluster_private_service_connect" { + count = var.shared_vpc_host ? 1 : 0 + source = "terraform-google-modules/network/google//modules/private-service-connect" + version = "~> 10.0" + project_id = module.cluster_vpc.project_id + network_self_link = module.cluster_vpc.network_self_link + private_service_connect_ip = "10.3.0.5" + forwarding_rule_target = "vpc-sc" +} + +resource "google_compute_router" "nat_router" { + for_each = var.shared_vpc_host ? { "create" : true } : {} + name = "nat-router-us-central-1" + region = "us-central1" + network = module.cluster_vpc.network_self_link + project = module.cluster_vpc.project_id +} + +resource "google_compute_router_nat" "cloud_nat" { + for_each = google_compute_router.nat_router + name = "cloud-nat" + router = each.value.name + region = each.value.region + project = module.cluster_vpc.project_id + nat_ip_allocate_option = "AUTO_ONLY" + source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES" + + log_config { + enable = true + filter = "ERRORS_ONLY" + } +} diff --git a/test/setup/modules/cluster_network/outputs.tf b/test/setup/modules/cluster_network/outputs.tf new file mode 100644 index 00000000..e184b8ba --- /dev/null +++ b/test/setup/modules/cluster_network/outputs.tf @@ -0,0 +1,45 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "project_id" { + description = "Network project id." + value = module.cluster_vpc.project_id +} + +output "subnets_self_links" { + description = "Subnets self-links." + value = module.cluster_vpc.subnets_self_links +} + +output "subnets_names" { + description = "Subnets self-links." + value = module.cluster_vpc.subnets_names +} + +output "network_self_link" { + description = "Network self-link." + value = module.cluster_vpc.network_self_link +} + +output "network_name" { + description = "Network name." + value = module.cluster_vpc.network_name +} + +output "network_id" { + description = "Network name." + value = module.cluster_vpc.network_id +} diff --git a/test/setup/modules/cluster_network/variables.tf b/test/setup/modules/cluster_network/variables.tf new file mode 100644 index 00000000..60aec400 --- /dev/null +++ b/test/setup/modules/cluster_network/variables.tf @@ -0,0 +1,54 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "vpc_name" { + description = "The VPC name to be concat with `vpc-` prefix." + type = string +} + +variable "project_id" { + description = "The project to deploy in" + type = string +} + +variable "shared_vpc_host" { + description = "Makes this project a Shared VPC host if 'true' (default 'false')" + type = bool + default = false +} + +variable "subnets" { + description = "Sub-networks to be created." + type = any +} + +variable "secondary_ranges" { + description = "Secondary ranges that will be used in some of the subnets." + type = any + default = {} +} + +variable "ingress_rules" { + description = "List of ingress rules. This will be ignored if variable 'rules' is non-empty." + type = any + default = [] +} + +variable "egress_rules" { + description = "List of egress rules. This will be ignored if variable 'rules' is non-empty" + type = any + default = [] +} diff --git a/test/setup/modules/cluster_network/versions.tf b/test/setup/modules/cluster_network/versions.tf new file mode 100644 index 00000000..7fc9df44 --- /dev/null +++ b/test/setup/modules/cluster_network/versions.tf @@ -0,0 +1,37 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 6.6 , < 7" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 6.6, < 7" + } + time = { + source = "hashicorp/time" + version = ">= 0.12.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.7.2" + } + } +} diff --git a/test/setup/modules/private_workerpool/main.tf b/test/setup/modules/private_workerpool/main.tf new file mode 100644 index 00000000..2e00face --- /dev/null +++ b/test/setup/modules/private_workerpool/main.tf @@ -0,0 +1,62 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_global_address" "worker_range" { + project = var.project_id_standalone + name = "worker-pool-range" + purpose = "VPC_PEERING" + address_type = "INTERNAL" + address = "10.3.3.0" + prefix_length = 24 + network = module.vpc.network_name + +} + +resource "google_service_networking_connection" "gitlab_worker_pool_conn" { + network = module.vpc.network_id + service = "servicenetworking.googleapis.com" + reserved_peering_ranges = [google_compute_global_address.worker_range.name] + depends_on = [google_project_service.servicenetworking] +} + +resource "google_project_service" "servicenetworking" { + project = var.project_id_standalone + service = "servicenetworking.googleapis.com" + disable_on_destroy = false +} + +resource "google_cloudbuild_worker_pool" "pool" { + name = "cb-pool" + project = var.project_id_standalone + location = var.workpool_region + worker_config { + disk_size_gb = 100 + machine_type = var.workerpool_machine_type + no_external_ip = true + } + network_config { + peered_network = module.vpc.network_id + peered_network_ip_range = "/24" + } + + depends_on = [google_service_networking_connection.gitlab_worker_pool_conn] +} + +resource "time_sleep" "wait_service_network_peering" { + depends_on = [google_service_networking_connection.gitlab_worker_pool_conn] + + create_duration = "30s" +} diff --git a/test/setup/modules/private_workerpool/nat.tf b/test/setup/modules/private_workerpool/nat.tf new file mode 100644 index 00000000..29bb7fed --- /dev/null +++ b/test/setup/modules/private_workerpool/nat.tf @@ -0,0 +1,150 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_network_peering_routes_config" "peering_routes" { + project = var.project_id_standalone + peering = google_service_networking_connection.gitlab_worker_pool_conn.peering + network = module.vpc.network_name + import_custom_routes = true + export_custom_routes = true + // explicitly allow the peering for public ip address + import_subnet_routes_with_public_ip = true + export_subnet_routes_with_public_ip = true + +} + +module "firewall_rules" { + source = "terraform-google-modules/network/google//modules/firewall-rules" + version = "~> 9.0" + project_id = var.project_id_standalone + network_name = module.vpc.network_name + + rules = [{ + name = "allow-pool-to-nat" + direction = "INGRESS" + priority = 1000 + source_tags = null + source_service_accounts = null + target_tags = ["nat-gateway"] + target_service_accounts = null + + ranges = ["${google_compute_global_address.worker_range.address}/${google_compute_global_address.worker_range.prefix_length}"] + + allow = [{ + protocol = "all" + ports = null + }] + + log_config = { + metadata = "INCLUDE_ALL_METADATA" + } + }, + { + name = "allow-icmp" + description = "Allow ICMP from anywhere" + direction = "INGRESS" + priority = 65534 + source_ranges = ["0.0.0.0/0"] + log_config = { + metadata = "INCLUDE_ALL_METADATA" + } + allow = [{ + protocol = "icmp" + }] + } + ] +} + +resource "google_compute_address" "cloud_build_nat" { + project = var.project_id_standalone + address_type = "EXTERNAL" + name = "cloud-build-nat" + network_tier = "PREMIUM" + region = "us-central1" +} + +resource "google_compute_instance" "vm-proxy" { + project = var.project_id_standalone + name = "cloud-build-nat-vm" + machine_type = "e2-medium" + zone = "us-central1-a" + + tags = ["direct-gateway-access", "nat-gateway"] + + boot_disk { + initialize_params { + image = "debian-cloud/debian-12" + } + } + + network_interface { + network = module.vpc.network_name + subnetwork = module.vpc.subnets_names[0] + subnetwork_project = var.project_id_standalone + + access_config { + nat_ip = google_compute_address.cloud_build_nat.address + } + } + + can_ip_forward = true + + metadata_startup_script = "sysctl -w net.ipv4.ip_forward=1 && iptables -t nat -A POSTROUTING -o $(ip addr show scope global | head -1 | awk -F: '{print $2}') -j MASQUERADE" + + service_account { + scopes = ["cloud-platform"] + } +} + +resource "google_compute_route" "through-nat" { + name = "through-nat-range1" + project = var.project_id_standalone + dest_range = "0.0.0.0/1" + network = module.vpc.network_name + next_hop_instance = google_compute_instance.vm-proxy.id + priority = 10 +} + +resource "google_compute_route" "through-nat2" { + name = "through-nat-range2" + project = var.project_id_standalone + dest_range = "128.0.0.0/1" + network = module.vpc.network_name + next_hop_instance = google_compute_instance.vm-proxy.id + priority = 10 +} + +resource "google_compute_route" "direct-to-gateway" { + name = "direct-to-gateway-range1" + project = var.project_id_standalone + dest_range = "0.0.0.0/1" + network = module.vpc.network_name + next_hop_gateway = "default-internet-gateway" + tags = ["direct-gateway-access"] + priority = 5 +} + +resource "google_compute_route" "direct-to-gateway2" { + name = "direct-to-gateway-range2" + project = var.project_id_standalone + dest_range = "128.0.0.0/1" + network = module.vpc.network_name + next_hop_gateway = "default-internet-gateway" + tags = ["direct-gateway-access"] + priority = 5 +} diff --git a/test/setup/modules/private_workerpool/network.tf b/test/setup/modules/private_workerpool/network.tf new file mode 100644 index 00000000..39fc9788 --- /dev/null +++ b/test/setup/modules/private_workerpool/network.tf @@ -0,0 +1,55 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { + nat_proxy_vm_ip_range = "10.1.1.0/24" +} + +module "vpc" { + source = "terraform-google-modules/network/google" + version = "~> 10.0" + + project_id = var.project_id_standalone + network_name = "eab-vpc-workerpool" + shared_vpc_host = false + delete_default_internet_gateway_routes = true + + ingress_rules = [ + { + name = "allow-ssh" + priority = 500 + log_config = { + metadata = "INCLUDE_ALL_METADATA" + } + source_ranges = ["0.0.0.0/0"] + allow = [ + { + protocol = "tcp" + ports = ["22"] + } + ] + }, + ] + + subnets = [ + { + subnet_name = "nat-subnet" + subnet_ip = local.nat_proxy_vm_ip_range + subnet_region = "us-central1" + subnet_private_access = true + }, + ] +} diff --git a/test/setup/modules/private_workerpool/outputs.tf b/test/setup/modules/private_workerpool/outputs.tf new file mode 100644 index 00000000..d28a8c13 --- /dev/null +++ b/test/setup/modules/private_workerpool/outputs.tf @@ -0,0 +1,44 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "workerpool_project_id" { + value = var.project_id_standalone +} + +output "workerpool_id" { + value = google_cloudbuild_worker_pool.pool.id +} + +output "workerpool_project_number" { + value = var.project_number_standalone +} + +output "workerpool_network_name" { + value = module.vpc.network_name +} + +output "workerpool_network_id" { + value = module.vpc.network_id +} + +output "workerpool_network_self_link" { + value = module.vpc.network_self_link +} + +output "workerpool_subnets_self_links" { + value = module.vpc.subnets_self_links +} + diff --git a/test/setup/modules/private_workerpool/variables.tf b/test/setup/modules/private_workerpool/variables.tf new file mode 100644 index 00000000..f0a9cda5 --- /dev/null +++ b/test/setup/modules/private_workerpool/variables.tf @@ -0,0 +1,57 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "folder_id" { + description = "The folder to deploy in" + type = string +} + +variable "project_id_standalone" { + description = "The seed project id" + type = string +} + +variable "project_number_standalone" { + description = "The seed project number" + type = string +} + +variable "org_id" { + description = "The numeric organization id" + type = string +} + +variable "billing_account" { + description = "The billing account id associated with the project, e.g. XXXXXX-YYYYYY-ZZZZZZ" + type = string +} + +variable "network_name" { + description = "Required. Immutable. The network definition that the workers are peered to. If this section is left empty, the workers will be peered to WorkerPool.project_id on the service producer network. Must be in the format projects/{project}/global/networks/{network}, where {project} is a project number, such as 12345, and {network} is the name of a VPC network in the project." + type = string +} + +variable "workpool_region" { + description = "The region to deploy in" + type = string + default = "us-central1" +} + +variable "workerpool_machine_type" { + description = "The project to deploy in" + type = string + default = "e2-standard-4" +} diff --git a/test/setup/modules/private_workerpool/versions.tf b/test/setup/modules/private_workerpool/versions.tf new file mode 100644 index 00000000..7fc9df44 --- /dev/null +++ b/test/setup/modules/private_workerpool/versions.tf @@ -0,0 +1,37 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 6.6 , < 7" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 6.6, < 7" + } + time = { + source = "hashicorp/time" + version = ">= 0.12.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.7.2" + } + } +} diff --git a/test/setup/outputs.tf b/test/setup/outputs.tf index 2f3166b0..f00fff67 100644 --- a/test/setup/outputs.tf +++ b/test/setup/outputs.tf @@ -14,14 +14,14 @@ * limitations under the License. */ -output "project_id" { - value = module.project.project_id -} - output "project_id_standalone" { value = module.project_standalone.project_id } +output "project_number_standalone" { + value = module.project_standalone.project_number +} + output "sa_key" { value = google_service_account_key.int_test.private_key sensitive = true @@ -39,37 +39,6 @@ output "billing_account" { value = var.billing_account } -output "gke_project_ids" { - description = "List of GKE project IDs" - value = zipmap(local.envs, [for env in local.envs : module.gke_project[env].project_id]) -} - -output "gke_vpc_names" { - description = "List of GKE project IDs" - value = zipmap(local.envs, [for env in local.envs : module.vpc[env].network_name]) -} - -output "gke_service_accounts" { - description = "List of GKE service accounts" - value = zipmap(local.envs, [for env in local.envs : module.gke_cluster[env].service_account]) -} - -output "gke_cluster_names" { - description = "List of GKE clusters" - value = zipmap(local.envs, [for env in local.envs : module.gke_cluster[env].name]) -} - -output "gke_private_vpc_names" { - description = "List of GKE project IDs" - value = zipmap(local.envs, [for env in local.envs : module.vpc_private_cluster[env].network_name]) -} - -output "gke_private_service_accounts" { - description = "List of GKE private cluster service accounts" - value = zipmap(local.envs, [for env in local.envs : module.gke_private_cluster[env].service_account]) -} - -output "gke_private_cluster_names" { - description = "List of GKE private clusters" - value = zipmap(local.envs, [for env in local.envs : module.gke_private_cluster[env].name]) +output "primary_location" { + value = local.primary_location } diff --git a/test/setup/scripts/gitlab_self_hosted.sh b/test/setup/scripts/gitlab_self_hosted.sh index a8692e30..bef8ead7 100644 --- a/test/setup/scripts/gitlab_self_hosted.sh +++ b/test/setup/scripts/gitlab_self_hosted.sh @@ -1,11 +1,25 @@ #!/bin/bash -# GitLab Installation + +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# apt-get update apt-get install -y curl openssh-server ca-certificates tzdata perl jq curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.deb.sh | bash apt-get install gitlab-ee=17.11.2-ee.0 -# Retrieve values from Metadata Server + EXTERNAL_IP=$(curl http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip -H "Metadata-Flavor: Google") PROJECT_ID=$(curl http://metadata.google.internal/computeMetadata/v1/project/project-id -H "Metadata-Flavor: Google") URL="https://$EXTERNAL_IP.sslip.io" @@ -19,6 +33,7 @@ mv cert.pem gitlab.crt mkdir -p /etc/gitlab/ssl cp gitlab.* /etc/gitlab/ssl +gcloud storage cp gitlab.crt gs://"${PROJECT_ID}"-ssl-cert cat > /etc/gitlab/gitlab.rb < Date: Mon, 8 Jun 2026 17:54:43 -0300 Subject: [PATCH 3/6] fix: addressing linter issues --- examples/standalone_single_project/cicd.tf | 3 +-- modules/cloudbuild-private-pool/metadata.yaml | 1 + modules/secure-cd/metadata.yaml | 1 + modules/secure-cd/outputs.tf | 2 +- modules/secure-ci/README.md | 6 +---- modules/secure-ci/locals.tf | 6 +---- modules/secure-ci/metadata.yaml | 21 ++++------------- modules/secure-ci/outputs.tf | 3 ++- modules/secure-ci/variables.tf | 23 ------------------- modules/secure-ci/versions.tf | 5 ++++ modules/workerpool-gke-ha-vpn/metadata.yaml | 2 +- 11 files changed, 19 insertions(+), 54 deletions(-) diff --git a/examples/standalone_single_project/cicd.tf b/examples/standalone_single_project/cicd.tf index 857d2c2e..2d067e75 100644 --- a/examples/standalone_single_project/cicd.tf +++ b/examples/standalone_single_project/cicd.tf @@ -123,8 +123,7 @@ module "cloudbuild_private_pool" { } module "attestors" { - source = "GoogleCloudPlatform/secure-cicd/google//modules/attestor" - version = "~> 1.0" + source = "../../modules/attestor" project_id = var.project_id diff --git a/modules/cloudbuild-private-pool/metadata.yaml b/modules/cloudbuild-private-pool/metadata.yaml index 8e505a29..15f7b88a 100644 --- a/modules/cloudbuild-private-pool/metadata.yaml +++ b/modules/cloudbuild-private-pool/metadata.yaml @@ -25,6 +25,7 @@ spec: repo: https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd.git sourceType: git dir: /modules/cloudbuild-private-pool + version: 1.2.2 actuationTool: flavor: Terraform version: ~> 1.3 diff --git a/modules/secure-cd/metadata.yaml b/modules/secure-cd/metadata.yaml index 500ac59b..6b6ca8b2 100644 --- a/modules/secure-cd/metadata.yaml +++ b/modules/secure-cd/metadata.yaml @@ -25,6 +25,7 @@ spec: repo: https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd.git sourceType: git dir: /modules/secure-cd + version: 1.2.2 actuationTool: flavor: Terraform version: ~> 1.3 diff --git a/modules/secure-cd/outputs.tf b/modules/secure-cd/outputs.tf index c8bc38cf..fe46e509 100644 --- a/modules/secure-cd/outputs.tf +++ b/modules/secure-cd/outputs.tf @@ -21,7 +21,7 @@ output "deploy_trigger_names" { output "binauthz_policy_required_attestations" { description = "Binary Authorization policy required attestation in GKE projects" - value = [for policy in google_binary_authorization_policy.deployment_policy : policy.cluster_admission_rules.*.require_attestations_by] + value = [for policy in google_binary_authorization_policy.deployment_policy : policy.cluster_admission_rules[*].require_attestations_by] } output "clouddeploy_delivery_pipeline_id" { diff --git a/modules/secure-ci/README.md b/modules/secure-ci/README.md index 66bb199a..e0aebcd8 100644 --- a/modules/secure-ci/README.md +++ b/modules/secure-ci/README.md @@ -102,7 +102,6 @@ The template `cloudbuild-ci.yaml` build configuration runs container structure a | app\_build\_trigger\_yaml | Name of application cloudbuild yaml file | `string` | n/a | yes | | attestor\_names\_prefix | A list of Binary Authorization attestors to create. The first attestor specified in this list will be used as the build-attestor during the CI phase. | `list(string)` | n/a | yes | | bucket\_kms\_key | KMS Key id to be used to encrypt bucket. | `string` | `null` | no | -| build\_image\_config\_yaml | Name of image builder yaml file | `string` | n/a | yes | | cache\_bucket\_name | Name of cloudbuild artifact and cache GCS bucket | `string` | `""` | no | | ci\_repository | The CI repository to configure. The key is a short name for the service. |
object({
repository_name = string
repository_url = string
})
| `null` | no | | cloudbuild\_private\_pool | Cloud Build private pool self-link | `string` | `""` | no | @@ -116,10 +115,7 @@ The template `cloudbuild-ci.yaml` build configuration runs container structure a | primary\_location | Region used for key-ring | `string` | n/a | yes | | project\_id | Project ID for CICD Pipeline Project | `string` | n/a | yes | | repository\_type | The type of the repository. Must be one of 'GITHUB', 'GITLAB', or 'CSR'. | `string` | n/a | yes | -| runner\_build\_folder | Path to the source folder for the cloud builds submit command. Leave blank if `skip_provisioners = true` | `string` | `""` | no | -| skip\_provisioners | Skip modules that use provisioners/local-exec | `bool` | `false` | no | | trigger\_branch\_name | A regular expression to match one or more branches for the build trigger. | `string` | n/a | yes | -| use\_tf\_google\_credentials\_env\_var | Optional GOOGLE\_CREDENTIALS environment variable to be activated. | `bool` | `false` | no | ## Outputs @@ -134,6 +130,6 @@ The template `cloudbuild-ci.yaml` build configuration runs container structure a | skaffold\_builder\_image\_tag | The full path to the built Skaffold builder image in Artifact Registry. | | source\_repo\_name | Name of the created CSR repos | | source\_repo\_url | URLS of the created CSR repos | -| standalone\_bucket\_kms\_key | n/a | +| standalone\_bucket\_kms\_key | KMS Key for standalone bucket. | diff --git a/modules/secure-ci/locals.tf b/modules/secure-ci/locals.tf index a8529301..22c9e167 100644 --- a/modules/secure-ci/locals.tf +++ b/modules/secure-ci/locals.tf @@ -16,13 +16,9 @@ locals { gar_name = split("/", google_artifact_registry_repository.image_repo.name)[length(split("/", google_artifact_registry_repository.image_repo.name)) - 1] - cache_bucket_name = var.cache_bucket_name == "" ? "bkt-cloudbuild" : "${var.cache_bucket_name}" + cache_bucket_name = var.cache_bucket_name == "" ? "bkt-cloudbuild" : var.cache_bucket_name use_csr = var.repository_type == "CSR" repos = local.use_csr ? {} : { for repo in [var.ci_repository] : repo.repository_name => repo } - projects_re = "projects/([^/]+)/" - worker_pool_project = var.cloudbuild_private_pool != null ? regex(local.projects_re, var.cloudbuild_private_pool)[0] : null - kms_project = var.bucket_kms_key != null ? regex(local.projects_re, var.bucket_kms_key)[0] : null - second_gen_repo_url = local.use_csr ? null : values(module.cloudbuild_repositories[0].cloud_build_repositories_2nd_gen_repositories)[0].url second_gen_repo_id = local.use_csr ? null : values(module.cloudbuild_repositories[0].cloud_build_repositories_2nd_gen_repositories)[0].id second_gen_repo_name = local.use_csr ? null : keys(module.cloudbuild_repositories[0].cloud_build_repositories_2nd_gen_repositories)[0] common_substitutions = merge( diff --git a/modules/secure-ci/metadata.yaml b/modules/secure-ci/metadata.yaml index 85f69c8c..df81611f 100644 --- a/modules/secure-ci/metadata.yaml +++ b/modules/secure-ci/metadata.yaml @@ -25,8 +25,10 @@ spec: repo: https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd.git sourceType: git dir: /modules/secure-ci + version: 1.2.2 actuationTool: flavor: Terraform + version: ~> 1.3 description: {} content: examples: @@ -50,14 +52,6 @@ spec: description: Name of application cloudbuild yaml file varType: string required: true - - name: runner_build_folder - description: Path to the source folder for the cloud builds submit command. Leave blank if `skip_provisioners = true` - varType: string - defaultValue: "" - - name: build_image_config_yaml - description: Name of image builder yaml file - varType: string - required: true - name: csr_app_source_repo description: Name of repo that contains app source code along with cloudbuild yaml varType: string @@ -70,10 +64,6 @@ spec: description: Docker artifact registry repo to store app build images varType: string defaultValue: app-image-repo - - name: use_tf_google_credentials_env_var - description: Optional GOOGLE_CREDENTIALS environment variable to be activated. - varType: bool - defaultValue: false - name: trigger_branch_name description: A regular expression to match one or more branches for the build trigger. varType: string @@ -107,10 +97,6 @@ spec: description: Cloud Deploy pipeline name varType: string defaultValue: deploy-pipeline - - name: skip_provisioners - description: Skip modules that use provisioners/local-exec - varType: bool - defaultValue: false - name: labels description: A set of key/value label pairs to assign to the resources deployed by this blueprint. varType: map(string) @@ -169,6 +155,7 @@ spec: - name: source_repo_url description: URLS of the created CSR repos - name: standalone_bucket_kms_key + description: KMS Key for standalone bucket. requirements: roles: - level: Project @@ -253,5 +240,7 @@ spec: version: "> 4, < 8" - source: hashicorp/google-beta version: "> 4, < 8" + - source: hashicorp/null + version: ">= 3.3.0" - source: hashicorp/random version: ">= 3.7.2" diff --git a/modules/secure-ci/outputs.tf b/modules/secure-ci/outputs.tf index 32cde59e..0c0cdb73 100644 --- a/modules/secure-ci/outputs.tf +++ b/modules/secure-ci/outputs.tf @@ -55,7 +55,8 @@ output "build_sa_email" { } output "standalone_bucket_kms_key" { - value = var.bucket_kms_key + description = "KMS Key for standalone bucket." + value = var.bucket_kms_key } output "skaffold_builder_image_tag" { diff --git a/modules/secure-ci/variables.tf b/modules/secure-ci/variables.tf index 9fd0d35a..daef54a9 100644 --- a/modules/secure-ci/variables.tf +++ b/modules/secure-ci/variables.tf @@ -34,17 +34,6 @@ variable "app_build_trigger_yaml" { description = "Name of application cloudbuild yaml file" } -variable "runner_build_folder" { - type = string - description = "Path to the source folder for the cloud builds submit command. Leave blank if `skip_provisioners = true`" - default = "" -} - -variable "build_image_config_yaml" { - type = string - description = "Name of image builder yaml file" -} - variable "csr_app_source_repo" { type = string description = "Name of repo that contains app source code along with cloudbuild yaml" @@ -63,12 +52,6 @@ variable "gar_repo_name_suffix" { default = "app-image-repo" } -variable "use_tf_google_credentials_env_var" { - description = "Optional GOOGLE_CREDENTIALS environment variable to be activated." - type = bool - default = false -} - variable "trigger_branch_name" { type = string description = "A regular expression to match one or more branches for the build trigger." @@ -112,12 +95,6 @@ variable "clouddeploy_pipeline_name" { default = "deploy-pipeline" } -variable "skip_provisioners" { - description = "Skip modules that use provisioners/local-exec" - type = bool - default = false -} - variable "labels" { description = "A set of key/value label pairs to assign to the resources deployed by this blueprint." type = map(string) diff --git a/modules/secure-ci/versions.tf b/modules/secure-ci/versions.tf index c736cab3..182da3b8 100644 --- a/modules/secure-ci/versions.tf +++ b/modules/secure-ci/versions.tf @@ -15,6 +15,7 @@ */ terraform { + required_version = "~> 1.3" required_providers { google = { source = "hashicorp/google" @@ -28,6 +29,10 @@ terraform { source = "hashicorp/google-beta" version = "> 4, < 8" } + null = { + source = "hashicorp/null" + version = ">= 3.3.0" + } } provider_meta "google" { diff --git a/modules/workerpool-gke-ha-vpn/metadata.yaml b/modules/workerpool-gke-ha-vpn/metadata.yaml index 6414903b..0d339e19 100644 --- a/modules/workerpool-gke-ha-vpn/metadata.yaml +++ b/modules/workerpool-gke-ha-vpn/metadata.yaml @@ -25,7 +25,7 @@ spec: repo: https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd.git sourceType: git dir: /modules/workerpool-gke-ha-vpn - version: 1.2.1 + version: 1.2.2 actuationTool: flavor: Terraform version: ~> 1.3 From f9222e53a342dad067a72e8a7b5b91a0a9569ced Mon Sep 17 00:00:00 2001 From: marcelorobj Date: Tue, 9 Jun 2026 08:31:32 -0300 Subject: [PATCH 4/6] fix: removing unused variables from secure-ci module --- examples/standalone_single_project/cicd.tf | 14 +++++--------- examples/standalone_single_project/gke.tf | 5 +++-- examples/standalone_single_project/network.tf | 3 ++- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/examples/standalone_single_project/cicd.tf b/examples/standalone_single_project/cicd.tf index 2d067e75..0b4efea1 100644 --- a/examples/standalone_single_project/cicd.tf +++ b/examples/standalone_single_project/cicd.tf @@ -56,8 +56,7 @@ locals { # Secure-CI module "ci_pipeline" { - source = "GoogleCloudPlatform/secure-cicd/google//modules/secure-ci" - version = "~> 1.0" + source = "../../modules/secure-ci" project_id = var.project_id repository_type = var.repository_type @@ -69,17 +68,14 @@ module "ci_pipeline" { primary_location = var.region attestor_names_prefix = module.attestors.binauth_attestor_names app_build_trigger_yaml = "cloudbuild-ci.yaml" - build_image_config_yaml = "cloudbuild-skaffold-build-image.yaml" trigger_branch_name = ".*" cloudbuild_private_pool = var.private_worker_pool_id == null ? module.cloudbuild_private_pool.workerpool_id : var.private_worker_pool_id clouddeploy_pipeline_name = local.clouddeploy_pipeline_name - skip_provisioners = true labels = var.labels } module "cd_pipeline" { - source = "GoogleCloudPlatform/secure-cicd/google//modules/secure-cd" - version = "~> 1.0" + source = "../../modules/secure-cd" project_id = var.project_id primary_location = var.region @@ -103,9 +99,9 @@ module "cd_pipeline" { # Cloud Build Private Pool module "cloudbuild_private_pool" { - count = var.private_worker_pool_id == null ? 1 : 0 - source = "GoogleCloudPlatform/secure-cicd/google//modules/cloudbuild-private-pool" - version = "~> 1.0" + source = "../../modules/cloudbuild-private-pool" + + count = var.private_worker_pool_id == null ? 1 : 0 project_id = var.project_id network_project_id = var.project_id diff --git a/examples/standalone_single_project/gke.tf b/examples/standalone_single_project/gke.tf index 784b4d76..dee00e4b 100644 --- a/examples/standalone_single_project/gke.tf +++ b/examples/standalone_single_project/gke.tf @@ -37,9 +37,10 @@ resource "random_shuffle" "available_zones" { # Private GKE Clusters module "gke_cluster" { + source = "terraform-google-modules/kubernetes-engine/google//modules/private-cluster" + version = "44.0" + for_each = toset(local.envs) - source = "terraform-google-modules/kubernetes-engine/google//modules/private-cluster" - version = "44.0" project_id = var.project_id name = "${var.app_name}-cluster-${each.value}" diff --git a/examples/standalone_single_project/network.tf b/examples/standalone_single_project/network.tf index 4d6b6d22..1fd9f276 100644 --- a/examples/standalone_single_project/network.tf +++ b/examples/standalone_single_project/network.tf @@ -39,10 +39,11 @@ locals { # Private Cluster VPCs module "vpc" { - count = var.network_name == null ? 1 : 0 source = "terraform-google-modules/network/google" version = "~> 18.0" + count = var.network_name == null ? 1 : 0 + project_id = var.project_id network_name = "${var.app_name}-vpc" routing_mode = "REGIONAL" From 0bdcadbc3e92c1e7da7ef1af7adb65f3d345c7b6 Mon Sep 17 00:00:00 2001 From: marcelorobj Date: Fri, 19 Jun 2026 10:54:32 -0300 Subject: [PATCH 5/6] feat: adding service perimeter to integration test harness. adding required ingress and egress rules to both the integration test harness and secure ci module. updated the integration test pipeline. --- build/int.cloudbuild.yaml | 216 ++++---- examples/standalone_single_project/README.md | 19 +- examples/standalone_single_project/cicd.tf | 1 + examples/standalone_single_project/outputs.tf | 71 +-- .../terraform.tfvars | 4 +- .../standalone_single_project/variables.tf | 6 + modules/secure-cd/README.md | 1 + modules/secure-cd/metadata.yaml | 2 + modules/secure-cd/outputs.tf | 5 + modules/secure-ci/README.md | 2 + modules/secure-ci/iam.tf | 92 ++++ modules/secure-ci/main.tf | 22 - modules/secure-ci/metadata.display.yaml | 3 + modules/secure-ci/metadata.yaml | 5 + modules/secure-ci/outputs.tf | 5 + modules/secure-ci/variables.tf | 6 + .../bootstrap_gitlab_environment_test.go | 10 +- test/integration/harness_vpcsc/vcp_test.go | 72 +++ .../standalone_single_project_test.go | 5 +- test/integration/testutils/gcp.go | 64 +++ test/integration/testutils/retry.go | 43 +- test/integration/testutils/strings.go | 48 ++ .../harness/service_perimeter/outputs.tf | 44 ++ .../harness/service_perimeter/variables.tf | 52 ++ .../harness/service_perimeter/versions.tf | 37 ++ .../setup/harness/service_perimeter/vpc-sc.tf | 493 ++++++++++++++++++ test/setup/iam.tf | 30 ++ test/setup/main.tf | 6 + test/setup/outputs.tf | 4 + 29 files changed, 1209 insertions(+), 159 deletions(-) create mode 100644 modules/secure-ci/iam.tf create mode 100644 test/integration/harness_vpcsc/vcp_test.go create mode 100644 test/integration/testutils/gcp.go create mode 100644 test/integration/testutils/strings.go create mode 100644 test/setup/harness/service_perimeter/outputs.tf create mode 100644 test/setup/harness/service_perimeter/variables.tf create mode 100644 test/setup/harness/service_perimeter/versions.tf create mode 100644 test/setup/harness/service_perimeter/vpc-sc.tf diff --git a/build/int.cloudbuild.yaml b/build/int.cloudbuild.yaml index d50bb32e..d43fe8c9 100644 --- a/build/int.cloudbuild.yaml +++ b/build/int.cloudbuild.yaml @@ -24,105 +24,133 @@ steps: - 'TF_VAR_org_id=$_ORG_ID' - 'TF_VAR_folder_id=$_FOLDER_ID' - 'TF_VAR_billing_account=$_BILLING_ACCOUNT' -- id: init-all - name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run all --stage init --verbose'] # app_cicd -- id: go-init-app-cicd-example - waitFor: - - init-all - name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestAppCICDExample --stage init --verbose'] -- id: go-apply-app-cicd-example - waitFor: - - go-init-app-cicd-example - name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestAppCICDExample --stage apply --verbose'] -- id: go-verify-app-cicd-example - waitFor: - - go-apply-app-cicd-example - name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestAppCICDExample --stage verify --verbose'] -- id: go-destroy-app-cicd-example - waitFor: - - go-verify-app-cicd-example - name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestAppCICDExample --stage destroy --verbose'] +timeout: 14400s +steps: + - id: prepare + name: "gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS" + args: + [ + "/bin/bash", + "-c", + "source /usr/local/bin/task_helper_functions.sh && prepare_environment", + ] + env: + - "TF_VAR_org_id=$_ORG_ID" + - "TF_VAR_folder_id=$_FOLDER_ID" + - "TF_VAR_billing_account=$_BILLING_ACCOUNT" -# cloudbuild_private_pool -- id: go-init-cloudbuild_private_pool-example - waitFor: - - init-all - - go-destroy-app-cicd-example - name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestCloudBuildPrivatePoolExample --stage init --verbose'] -- id: go-apply-cloudbuild_private_pool-example - waitFor: - - go-init-cloudbuild_private_pool-example - name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestCloudBuildPrivatePoolExample --stage apply --verbose'] -- id: go-verify-cloudbuild_private_pool-example - waitFor: - - go-apply-cloudbuild_private_pool-example - name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestCloudBuildPrivatePoolExample --stage verify --verbose'] -- id: go-destroy-cloudbuild_private_pool-example - waitFor: - - go-verify-cloudbuild_private_pool-example - name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestCloudBuildPrivatePoolExample --stage destroy --verbose'] + - id: private-workerpool-init + name: "gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS" + args: + ["/bin/bash", "-c", "cft test run TestPrivateWorkerPool --stage init --verbose"] -# private_cluster_cicd -- id: go-init-private-cluster-cicd-example - waitFor: - - init-all - - go-destroy-cloudbuild_private_pool-example - name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestPrivateClusterCICDExample --stage init --verbose'] -- id: go-apply-private-cluster-cicd-example - waitFor: - - go-init-private-cluster-cicd-example - name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestPrivateClusterCICDExample --stage apply --verbose'] -- id: go-verify-private-cluster-cicd-example - waitFor: - - go-apply-private-cluster-cicd-example - name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestPrivateClusterCICDExample --stage verify --verbose'] -- id: go-destroy-private-cluster-cicd-example - waitFor: - - go-verify-private-cluster-cicd-example - name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestPrivateClusterCICDExample --stage destroy --verbose'] + - id: private-workerpool-apply + name: "gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS" + args: + ["/bin/bash", "-c", "cft test run TestPrivateWorkerPool --stage apply --verbose"] -# standalone_single_project -- id: go-init-standalone-single-proj-example - waitFor: - - init-all - name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestStandaloneSingleProjectExample --stage init --verbose'] -- id: go-apply-standalone-single-proj-example - waitFor: - - go-init-standalone-single-proj-example - name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestStandaloneSingleProjectExample --stage apply --verbose'] -- id: go-verify-standalone-single-proj-example - waitFor: - - go-apply-standalone-single-proj-example - name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestStandaloneSingleProjectExample --stage verify --verbose'] -- id: go-destroy-standalone-single-proj-example - waitFor: - - go-verify-standalone-single-proj-example - name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' - args: ['/bin/bash', '-c', 'cft test run TestStandaloneSingleProjectExample --stage destroy --verbose'] + - id: logging-bucket-init + name: "gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS" + args: + ["/bin/bash", "-c", "cft test run TestLoggingBucket --stage init --verbose"] + + - id: logging-bucket-apply + name: "gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS" + args: + ["/bin/bash", "-c", "cft test run TestLoggingBucket --stage apply --verbose"] + + - id: gitlab-init + name: "gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS" + args: + ["/bin/bash", "-c", "cft test run TestGitLab --stage init --verbose"] + + - id: gitlab-apply + name: "gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS" + args: + ["/bin/bash", "-c", "cft test run TestGitLab --stage apply --verbose"] + + - id: harness-init + name: "gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS" + args: + ["/bin/bash", "-c", "cft test run TestSingleProjectHarness --stage init --verbose"] + + - id: harness-apply + name: "gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS" + args: + ["/bin/bash", "-c", "cft test run TestSingleProjectHarness --stage apply --verbose"] + + - id: wait-startup-script + name: "gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS" + args: + ["/bin/bash", "-c", "cft test run TestValidateStartupScript --stage verify --verbose"] + + - id: bootstrap-gitlab-vm + name: "gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS" + entrypoint: "bash" + args: + - "-c" + - |- + gcloud storage cp gs://$(terraform -chdir=/workspace/test/setup/harness/gitlab output -raw gitlab_ssl_bucket_name)/gitlab.crt /usr/local/share/ca-certificates + update-ca-certificates + if (ls /etc/ssl/certs | grep gitlab.pem); then + cft test run TestBootstrapGitlabVM --stage verify --verbose || exit 1 + else + echo ERROR: Was not able to download certificate and update system bundle. && exit 1 + fi + + - id: vpc-sc + name: "gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS" + args: + [ + "/bin/bash", + "-c", + "cft test run TestVPCSC --stage init --verbose && cft test run TestVPCSC --stage apply --verbose", + ] + env: + - "TF_VAR_org_id=$_ORG_ID" + + - id: single-project-init + name: "gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS" + args: + [ + "/bin/bash", + "-c", + "cft test run TestStandaloneSingleProjectExample --stage init --verbose", + ] + + - id: single-project-apply + name: "gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS" + args: + [ + "/bin/bash", + "-c", + "cft test run TestStandaloneSingleProjectExample --stage apply --verbose", + ] + + - id: single-project-verify + name: "gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS" + args: + [ + "/bin/bash", + "-c", + "cft test run TestStandaloneSingleProjectExample --stage verify --verbose", + ] tags: -- 'ci' -- 'integration' -- 'cd' -- 'deployment' + - "ci" + - "integration" + - "cd" + - "deployment" substitutions: - _DOCKER_IMAGE_DEVELOPER_TOOLS: 'cft/developer-tools' - _DOCKER_TAG_VERSION_DEVELOPER_TOOLS: '1' + _DOCKER_IMAGE_DEVELOPER_TOOLS: "cft/developer-tools" + _DOCKER_TAG_VERSION_DEVELOPER_TOOLS: "1.25" +options: + machineType: E2_HIGHCPU_8 + env: + - 'TF_PLUGIN_CACHE_DIR=""' + - 'TF_VAR_branch_name=$BRANCH_NAME' + - "TF_VAR_service_perimeter_mode=ENFORCE" + - "TF_VAR_deletion_protection=false" + - "TF_VAR_cloud_build_sa=$SERVICE_ACCOUNT_EMAIL" diff --git a/examples/standalone_single_project/README.md b/examples/standalone_single_project/README.md index 613951be..df3f8793 100644 --- a/examples/standalone_single_project/README.md +++ b/examples/standalone_single_project/README.md @@ -11,6 +11,7 @@ For simplified deployment and demonstration purposes, this blueprint creates GKE | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| access\_level\_name | (VPC-SC) Access Level full name. When providing this variable, additional identities will be added to the access level, these are required to work within an enforced VPC-SC Perimeter. | `string` | `null` | no | | app\_name | Name of intended deployed application; to be used as a prefix for certain resources | `string` | `"ci-cd"` | no | | cd\_repository | The CI repository to configure. The key is a short name for the service. |
object({
repository_name = string
repository_url = string
})
| `null` | no | | ci\_repository | The CI repository to configure. The key is a short name for the service. |
object({
repository_name = string
repository_url = string
})
| `null` | no | @@ -31,17 +32,19 @@ For simplified deployment and demonstration purposes, this blueprint creates GKE | Name | Description | |------|-------------| -| cd\_repo\_name | The name of the CD repository. | +| attestors | Map of Binary Authorization attestor IDs by name | +| cd\_build\_trigger\_ids | Map of CD Cloud Build trigger names by environment | +| cd\_repo\_name | Name of the CD source repository | | cd\_repo\_url | The URL of the CD repository. | -| ci\_repo\_name | The name of the CI repository. | +| ci\_build\_trigger\_id | ID of the CI Cloud Build trigger | +| ci\_repo\_name | Name of the CI source repository | | ci\_repo\_url | The URL of the CI repository. | +| cloudbuild\_workerpool\_id | ID of the Cloud Build private worker pool | | clouddeploy\_pipeline\_id | ID of the Cloud Deploy delivery pipeline | -| cluster\_names | Comma-separated names of the deployed GKE clusters | -| console\_walkthrough\_link | n/a | -| gar\_repo | Artifact Registry repo | +| gar\_repo\_name | Name of the Artifact Registry repository | | gitlab\_url | The URL of the GitLab instance. | -| neos\_tutorial\_url | n/a | -| project\_id | Project ID | -| region | Region | +| gke\_cluster\_names | Map of GKE Cluster names by environment | +| project\_id | Project ID in which all resources were deployed | +| region | Region in which all regional resources were deployed | diff --git a/examples/standalone_single_project/cicd.tf b/examples/standalone_single_project/cicd.tf index 0b4efea1..70c7ad1a 100644 --- a/examples/standalone_single_project/cicd.tf +++ b/examples/standalone_single_project/cicd.tf @@ -66,6 +66,7 @@ module "ci_pipeline" { gar_repo_name_suffix = "${var.app_name}-image-repo" cache_bucket_name = "${var.app_name}-cloudbuild" primary_location = var.region + access_level_name = var.access_level_name attestor_names_prefix = module.attestors.binauth_attestor_names app_build_trigger_yaml = "cloudbuild-ci.yaml" trigger_branch_name = ".*" diff --git a/examples/standalone_single_project/outputs.tf b/examples/standalone_single_project/outputs.tf index bd3f01d2..391c35ae 100644 --- a/examples/standalone_single_project/outputs.tf +++ b/examples/standalone_single_project/outputs.tf @@ -15,60 +15,71 @@ */ output "project_id" { - description = "Project ID" + description = "Project ID in which all resources were deployed" value = var.project_id } output "region" { - description = "Region" + description = "Region in which all regional resources were deployed" value = var.region } -output "gitlab_url" { - description = "The URL of the GitLab instance." - value = var.gitlab_auth.enterprise_host_uri +output "gke_cluster_names" { + description = "Map of GKE Cluster names by environment" + value = { for k, v in module.gke_cluster : k => v.name } } -output "ci_repo_url" { - description = "The URL of the CI repository." - value = var.ci_repository.repository_url +output "clouddeploy_pipeline_id" { + description = "ID of the Cloud Deploy delivery pipeline" + value = module.cd_pipeline.clouddeploy_delivery_pipeline_id } -output "ci_repo_name" { - description = "The name of the CI repository." - value = var.ci_repository.repository_name +output "cloudbuild_workerpool_id" { + description = "ID of the Cloud Build private worker pool" + value = var.private_worker_pool_id == null ? module.cloudbuild_private_pool[0].workerpool_id : var.private_worker_pool_id } -output "cd_repo_url" { - description = "The URL of the CD repository." - value = var.cd_repository.repository_url +output "ci_build_trigger_id" { + description = "ID of the CI Cloud Build trigger" + value = module.ci_pipeline.ci_build_trigger_id } -output "cd_repo_name" { - description = "The name of the CD repository." - value = var.cd_repository.repository_name +output "cd_build_trigger_ids" { + description = "Map of CD Cloud Build trigger names by environment" + value = module.cd_pipeline.deploy_trigger_names } -output "gar_repo" { - description = "Artifact Registry repo" +output "gar_repo_name" { + description = "Name of the Artifact Registry repository" value = module.ci_pipeline.app_artifact_repo } -output "neos_tutorial_url" { - value = "https://console.cloud.google.com/products/solutions/catalog?walkthrough_id=solutions-in-console--secure-cicd-pipeline--tour&project=${var.project_id}" +output "attestors" { + description = "Map of Binary Authorization attestor IDs by name" + value = module.attestors.binauth_attestor_ids } -output "console_walkthrough_link" { - value = "https://shell.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2FGoogleCloudPlatform%2Fterraform-google-secure-cicd.git&cloudshell_git_branch=main&cloudshell_tutorial=examples%2Fstandalone_single_project%2Fwalkthrough.md&project=${var.project_id}" +output "ci_repo_name" { + description = "Name of the CI source repository" + value = var.repository_type == "CSR" ? module.ci_pipeline.source_repo_name : var.ci_repository.repository_name } -output "clouddeploy_pipeline_id" { - description = "ID of the Cloud Deploy delivery pipeline" - value = module.cd_pipeline.clouddeploy_delivery_pipeline_id +output "cd_repo_name" { + description = "Name of the CD source repository" + value = var.repository_type == "CSR" ? module.ci_pipeline.source_repo_name : var.cd_repository.repository_name +} + +output "gitlab_url" { + description = "The URL of the GitLab instance." + value = var.gitlab_auth.enterprise_host_uri } -output "cluster_names" { - description = "Comma-separated names of the deployed GKE clusters" - # Use join() to create a single string like: "cluster-dev,cluster-qa,cluster-prod" - value = join(",", [for k, v in module.gke_cluster : v.name]) +output "ci_repo_url" { + description = "The URL of the CI repository." + value = var.ci_repository.repository_url +} + +output "cd_repo_url" { + description = "The URL of the CD repository." + value = var.cd_repository.repository_url } diff --git a/examples/standalone_single_project/terraform.tfvars b/examples/standalone_single_project/terraform.tfvars index 74df512e..94920cf3 100644 --- a/examples/standalone_single_project/terraform.tfvars +++ b/examples/standalone_single_project/terraform.tfvars @@ -32,4 +32,6 @@ ci_repository = { cd_repository = { repository_name = "secure-cd" repository_url = "https://gitlab.com/user/secure-cd.git" -} \ No newline at end of file +} + +access_level_name = "{ACCESS_LEVEL_NAME}" diff --git a/examples/standalone_single_project/variables.tf b/examples/standalone_single_project/variables.tf index 864b7bb4..c0e6b44e 100644 --- a/examples/standalone_single_project/variables.tf +++ b/examples/standalone_single_project/variables.tf @@ -118,3 +118,9 @@ variable "network_name" { description = "Optional vpc network name if using already existing vpc" default = null } + +variable "access_level_name" { + description = "(VPC-SC) Access Level full name. When providing this variable, additional identities will be added to the access level, these are required to work within an enforced VPC-SC Perimeter." + type = string + default = null +} diff --git a/modules/secure-cd/README.md b/modules/secure-cd/README.md index 7bd44ab4..d7b85b64 100644 --- a/modules/secure-cd/README.md +++ b/modules/secure-cd/README.md @@ -156,6 +156,7 @@ module "cd_pipeline" { | Name | Description | |------|-------------| | binauthz\_policy\_required\_attestations | Binary Authorization policy required attestation in GKE projects | +| cd\_repo\_name | Name of the CD source repository | | clouddeploy\_delivery\_pipeline\_id | ID of the Cloud Deploy delivery pipeline | | clouddeploy\_target\_id | ID(s) of Cloud Deploy targets | | deploy\_trigger\_names | Names of CD Cloud Build triggers | diff --git a/modules/secure-cd/metadata.yaml b/modules/secure-cd/metadata.yaml index 6b6ca8b2..e182f161 100644 --- a/modules/secure-cd/metadata.yaml +++ b/modules/secure-cd/metadata.yaml @@ -127,6 +127,8 @@ spec: outputs: - name: binauthz_policy_required_attestations description: Binary Authorization policy required attestation in GKE projects + - name: cd_repo_name + description: Name of the CD source repository - name: clouddeploy_delivery_pipeline_id description: ID of the Cloud Deploy delivery pipeline - name: clouddeploy_target_id diff --git a/modules/secure-cd/outputs.tf b/modules/secure-cd/outputs.tf index fe46e509..cddeb4ba 100644 --- a/modules/secure-cd/outputs.tf +++ b/modules/secure-cd/outputs.tf @@ -33,3 +33,8 @@ output "clouddeploy_target_id" { description = "ID(s) of Cloud Deploy targets" value = [for target in google_clouddeploy_target.deploy_target : target.id] } + +output "cd_repo_name" { + description = "Name of the CD source repository" + value = local.use_csr ? google_sourcerepo_repository.csr_cd_repository[0].name : null +} diff --git a/modules/secure-ci/README.md b/modules/secure-ci/README.md index e0aebcd8..794476bb 100644 --- a/modules/secure-ci/README.md +++ b/modules/secure-ci/README.md @@ -98,6 +98,7 @@ The template `cloudbuild-ci.yaml` build configuration runs container structure a | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| access\_level\_name | (VPC-SC) Access Level full name. When providing this variable, additional identities will be added to the access level, these are required to work within an enforced VPC-SC Perimeter. | `string` | `null` | no | | additional\_substitutions | Parameters to be substituted in the build specification. All keys should begin with an underscore. | `map(string)` | `{}` | no | | app\_build\_trigger\_yaml | Name of application cloudbuild yaml file | `string` | n/a | yes | | attestor\_names\_prefix | A list of Binary Authorization attestors to create. The first attestor specified in this list will be used as the build-attestor during the CI phase. | `list(string)` | n/a | yes | @@ -127,6 +128,7 @@ The template `cloudbuild-ci.yaml` build configuration runs container structure a | binauth\_attestor\_project\_id | Project ID where attestors get created | | build\_sa\_email | Cloud Build Service Account email address | | cache\_bucket\_name | The name of the storage bucket for cloud build. | +| ci\_build\_trigger\_id | ID of the CI Cloud Build trigger. | | skaffold\_builder\_image\_tag | The full path to the built Skaffold builder image in Artifact Registry. | | source\_repo\_name | Name of the created CSR repos | | source\_repo\_url | URLS of the created CSR repos | diff --git a/modules/secure-ci/iam.tf b/modules/secure-ci/iam.tf new file mode 100644 index 00000000..b41ea8ba --- /dev/null +++ b/modules/secure-ci/iam.tf @@ -0,0 +1,92 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +data "google_project" "project" { + project_id = var.project_id +} + +resource "google_service_account" "build_sa" { + account_id = "build-sa" + display_name = "Service Account for ${var.csr_app_source_repo} Cloud Build triggers" + project = var.project_id +} + +resource "google_project_service_identity" "cloudbuild_service_identity" { + provider = google-beta + + project = var.project_id + service = "cloudbuild.googleapis.com" +} + +resource "google_project_service_identity" "cloud_deploy_sa" { + provider = google-beta + + project = var.project_id + service = "clouddeploy.googleapis.com" +} + +resource "google_project_service_identity" "pubsub_sa" { + provider = google-beta + + project = var.project_id + service = "pubsub.googleapis.com" +} + +resource "google_access_context_manager_access_level_condition" "access-level-conditions" { + count = var.access_level_name != null ? 1 : 0 + access_level = var.access_level_name + members = [ + google_project_service_identity.cloudbuild_service_identity.member, + google_project_service_identity.cloud_deploy_sa.member, + google_project_service_identity.pubsub_sa.member, + google_service_account.build_sa.member, + "serviceAccount:service-${data.google_project.project.number}@gcp-sa-cloudbuild.iam.gserviceaccount.com" + ] + + depends_on = [ + time_sleep.wait_access_level_propagation + ] +} + +resource "google_project_iam_member" "int_test_secret_viewer" { + count = var.repository_type == "GITLAB" ? 1 : 0 + project = var.project_id + role = "roles/secretmanager.admin" + member = "serviceAccount:${google_service_account.build_sa.email}" +} + +resource "google_storage_bucket_iam_member" "cloudbuild_artifacts_iam" { + bucket = google_storage_bucket.cache_bucket.name + role = "roles/storage.admin" + member = "serviceAccount:${google_service_account.build_sa.email}" + depends_on = [google_storage_bucket.cache_bucket] +} + +resource "google_project_iam_member" "build_sa_project_iam" { + for_each = toset(var.cloudbuild_service_account_roles) + project = var.project_id + role = each.value + member = "serviceAccount:${google_service_account.build_sa.email}" +} + +resource "time_sleep" "wait_access_level_propagation" { + depends_on = [ + google_project_service_identity.cloudbuild_service_identity, + google_project_service_identity.cloud_deploy_sa, + google_service_account.build_sa, + ] + destroy_duration = "5m" + create_duration = "2m" +} diff --git a/modules/secure-ci/main.tf b/modules/secure-ci/main.tf index 61d9dbfd..d9c9d9f4 100644 --- a/modules/secure-ci/main.tf +++ b/modules/secure-ci/main.tf @@ -72,19 +72,6 @@ resource "google_storage_bucket" "cache_bucket" { } } -resource "google_service_account" "build_sa" { - account_id = "build-sa" - display_name = "Service Account for ${var.csr_app_source_repo} Cloud Build triggers" - project = var.project_id -} - -resource "google_storage_bucket_iam_member" "cloudbuild_artifacts_iam" { - bucket = google_storage_bucket.cache_bucket.name - role = "roles/storage.admin" - member = "serviceAccount:${google_service_account.build_sa.email}" - depends_on = [google_storage_bucket.cache_bucket] -} - resource "google_cloudbuild_trigger" "csr_app_build_trigger" { count = local.use_csr ? 1 : 0 project = var.project_id @@ -119,15 +106,6 @@ resource "google_cloudbuild_trigger" "app_build_trigger" { depends_on = [module.cloudbuild_repositories] } - -# Cloud Build Service Account permissions -resource "google_project_iam_member" "build_sa_project_iam" { - for_each = toset(var.cloudbuild_service_account_roles) - project = var.project_id - role = each.value - member = "serviceAccount:${google_service_account.build_sa.email}" -} - resource "google_artifact_registry_repository" "image_repo" { project = var.project_id location = var.primary_location diff --git a/modules/secure-ci/metadata.display.yaml b/modules/secure-ci/metadata.display.yaml index 0d62e542..4e202160 100644 --- a/modules/secure-ci/metadata.display.yaml +++ b/modules/secure-ci/metadata.display.yaml @@ -26,6 +26,9 @@ spec: ui: input: variables: + access_level_name: + name: access_level_name + title: Access Level Name additional_substitutions: name: additional_substitutions title: Additional Substitutions diff --git a/modules/secure-ci/metadata.yaml b/modules/secure-ci/metadata.yaml index df81611f..183cb553 100644 --- a/modules/secure-ci/metadata.yaml +++ b/modules/secure-ci/metadata.yaml @@ -48,6 +48,9 @@ spec: description: A list of Binary Authorization attestors to create. The first attestor specified in this list will be used as the build-attestor during the CI phase. varType: list(string) required: true + - name: access_level_name + description: (VPC-SC) Access Level full name. When providing this variable, additional identities will be added to the access level, these are required to work within an enforced VPC-SC Perimeter. + varType: string - name: app_build_trigger_yaml description: Name of application cloudbuild yaml file varType: string @@ -148,6 +151,8 @@ spec: description: Cloud Build Service Account email address - name: cache_bucket_name description: The name of the storage bucket for cloud build. + - name: ci_build_trigger_id + description: ID of the CI Cloud Build trigger. - name: skaffold_builder_image_tag description: The full path to the built Skaffold builder image in Artifact Registry. - name: source_repo_name diff --git a/modules/secure-ci/outputs.tf b/modules/secure-ci/outputs.tf index 0c0cdb73..b3c439bd 100644 --- a/modules/secure-ci/outputs.tf +++ b/modules/secure-ci/outputs.tf @@ -63,3 +63,8 @@ output "skaffold_builder_image_tag" { description = "The full path to the built Skaffold builder image in Artifact Registry." value = local.skaffold_builder_image_tag } + +output "ci_build_trigger_id" { + description = "ID of the CI Cloud Build trigger." + value = google_cloudbuild_trigger.app_build_trigger[0].id +} diff --git a/modules/secure-ci/variables.tf b/modules/secure-ci/variables.tf index daef54a9..af47f7ca 100644 --- a/modules/secure-ci/variables.tf +++ b/modules/secure-ci/variables.tf @@ -29,6 +29,12 @@ variable "attestor_names_prefix" { type = list(string) } +variable "access_level_name" { + description = "(VPC-SC) Access Level full name. When providing this variable, additional identities will be added to the access level, these are required to work within an enforced VPC-SC Perimeter." + type = string + default = null +} + variable "app_build_trigger_yaml" { type = string description = "Name of application cloudbuild yaml file" diff --git a/test/integration/harness_setup_gitlab/bootstrap_gitlab_environment_test.go b/test/integration/harness_setup_gitlab/bootstrap_gitlab_environment_test.go index d014a19c..eefa544e 100644 --- a/test/integration/harness_setup_gitlab/bootstrap_gitlab_environment_test.go +++ b/test/integration/harness_setup_gitlab/bootstrap_gitlab_environment_test.go @@ -76,6 +76,8 @@ func TestBootstrapGitlabVM(t *testing.T) { setupOutput := tft.NewTFBlueprintTest(t, tft.WithTFDir("../../setup")) + servicePerimeterOutput := tft.NewTFBlueprintTest(t, tft.WithTFDir("../../setup/harness/service_perimeter")) + clusterNetworkOutput := tft.NewTFBlueprintTest(t, tft.WithTFDir("../../setup/harness/cluster_network")) privateWorkerPoolOutput := tft.NewTFBlueprintTest(t, tft.WithTFDir("../../setup/harness/private_workerpool")) @@ -94,6 +96,8 @@ func TestBootstrapGitlabVM(t *testing.T) { privateWorkerPoolId := privateWorkerPoolOutput.GetStringOutput("workerpool_id") clusterNetworkName := clusterNetworkOutput.GetStringOutput("network_name") + accessLevelName := servicePerimeterOutput.GetStringOutput("access_level_name") + token, err := testutils.GetSecretFromSecretManager(t, gitlabPersonalTokenSecretName, gitlabSecretProject) if err != nil { t.Fatal(err) @@ -127,12 +131,16 @@ func TestBootstrapGitlabVM(t *testing.T) { root := "../../.." - // Replace Service Directory err = testutils.ReplacePatternInTfVars("{PROJECT_ID}", projectID, root) if err != nil { t.Fatal(err) } + err = testutils.ReplacePatternInTfVars("{ACCESS_LEVEL_NAME}", accessLevelName, root) + if err != nil { + t.Fatal(err) + } + err = testutils.ReplacePatternInTfVars("{REGION}", region, root) if err != nil { t.Fatal(err) diff --git a/test/integration/harness_vpcsc/vcp_test.go b/test/integration/harness_vpcsc/vcp_test.go new file mode 100644 index 00000000..1a02e7e4 --- /dev/null +++ b/test/integration/harness_vpcsc/vcp_test.go @@ -0,0 +1,72 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package harness_vpcsc + +import ( + "fmt" + "os" + "strings" + "testing" + "time" + + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" + "github.com/terraform-google-modules/terraform-google-secure-cicd/test/integration/testutils" +) + +func TestVPCSC(t *testing.T) { + vpcPath := "../../setup/harness/service_perimeter" + + setupOutput := tft.NewTFBlueprintTest(t, tft.WithTFDir("../../setup")) + projectID := setupOutput.GetStringOutput("project_id_standalone") + projectNumber := setupOutput.GetStringOutput("project_number_standalone") + serviceAccount := setupOutput.GetTFSetupStringOutput("sa_email") + addAccessLevelMembers := strings.Split(os.Getenv("TF_VAR_access_level_members"), ",") + protected_projects := []string{} + + accessLevelMembers := []string{ + fmt.Sprintf("serviceAccount:%s", serviceAccount), + "serviceAccount:cloud-build@system.gserviceaccount.com", + fmt.Sprintf("serviceAccount:service-%s@gcp-sa-cloudbuild.iam.gserviceaccount.com", projectNumber), + } + accessLevelMembers = append(accessLevelMembers, addAccessLevelMembers...) + t.Logf("accessLevelMembers: %v", accessLevelMembers) + vars := map[string]interface{}{ + "access_level_members": accessLevelMembers, + "protected_projects": protected_projects, + "project_id": projectID, + } + + vpcsc := tft.NewTFBlueprintTest(t, + tft.WithTFDir(vpcPath), + tft.WithVars(vars), + tft.WithRetryableTerraformErrors(testutils.RetryableTransientErrors, 3, 2*time.Minute), + tft.WithParallelism(100), + ) + vpcsc.Test() + +} + +func TestCleanVPCSC(t *testing.T) { + vpcPath := "../../setup/harness/service_perimeter" + temp := tft.NewTFBlueprintTest(t, tft.WithTFDir(vpcPath)) + orgID := temp.GetTFSetupStringOutput("organization_id") + testutils.CleanOrgACMPolicyID(t, orgID) + if testutils.GetOrgACMPolicyID(t, orgID) == "" { + _, err := testutils.CreateOrgACMPolicyID(t, orgID) + if err != nil { + t.Logf("Error creating the ACM policy: %s", err) + } + } +} diff --git a/test/integration/standalone_single_project/standalone_single_project_test.go b/test/integration/standalone_single_project/standalone_single_project_test.go index 07ee073f..0355ef1d 100644 --- a/test/integration/standalone_single_project/standalone_single_project_test.go +++ b/test/integration/standalone_single_project/standalone_single_project_test.go @@ -165,14 +165,15 @@ func TestStandaloneSingleProjectExample(t *testing.T) { ) standaloneSingleProjT.DefineVerify(func(assert *assert.Assertions) { + // Verifier's Note: A successful apply is just the beginning. + // We will now query the data plane to assert the state of our deployed resources. ciRepoName := standaloneSingleProjT.GetStringOutput("ci_repo_name") cdRepoName := standaloneSingleProjT.GetStringOutput("cd_repo_name") setupGitOperations(t, blueprintFolder, workspaceFolder, ciRepoName, cdRepoName) standaloneSingleProjT.DefaultVerify(assert) - }) - // 5. Execute the Test + }) standaloneSingleProjT.Test() } diff --git a/test/integration/testutils/gcp.go b/test/integration/testutils/gcp.go new file mode 100644 index 00000000..beb58f81 --- /dev/null +++ b/test/integration/testutils/gcp.go @@ -0,0 +1,64 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "fmt" + + "github.com/mitchellh/go-testing-interface" + + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/gcloud" +) + +// GetOrgACMPolicyID gets the Organization Access Context Manager Policy ID +func GetOrgACMPolicyID(t testing.TB, orgID string) string { + filter := fmt.Sprintf("parent:organizations/%s", orgID) + t.Log("Getting Access Policy id.") + id := gcloud.Runf(t, "access-context-manager policies list --organization %s --filter %s --quiet", orgID, filter).Array() + if len(id) == 0 { + t.Log("Access Policy does not exists.") + return "" + } + acmD := GetLastSplitElement(id[0].Get("name").String(), "/") + t.Logf("Access Policy successfuly retrieved %s.", acmD) + return acmD +} + +// CreateOrgACMPolicyID gets the Organization Access Context Manager Policy ID +func CreateOrgACMPolicyID(t testing.TB, orgID string) (string, error) { + t.Log("Creating Access Policy id.") + return gcloud.RunCmdE(t, fmt.Sprintf("access-context-manager policies create --organization %s --title '%s' --quiet", orgID, "Organization access level policy")) +} + +// DeleteOrgACMPolicyID deletes the Organization Access Context Manager Policy ID +func DeleteOrgACMPolicyID(t testing.TB, acmD string) (string, error) { + t.Log("Deleting Access Policy id.") + return gcloud.RunCmdE(t, fmt.Sprintf("access-context-manager policies delete %s --quiet", acmD)) +} + +func CleanOrgACMPolicyID(t testing.TB, orgID string) { + t.Log("Cleaning Access Policy id.") + filter := fmt.Sprintf("parent:organizations/%s AND scopes:folders/*", orgID) + t.Log("Getting Access Policy id.") + acmDs := gcloud.Runf(t, "access-context-manager policies list --organization %s --filter \"%s\" --quiet", orgID, filter).Array() + t.Log(acmDs) + for _, acmD := range acmDs { + t.Log("Deleting Access Policy id %s", acmD.Get("name")) + _, err := gcloud.RunCmdE(t, fmt.Sprintf("access-context-manager policies delete %s --quiet", acmD.Get("name"))) + if err != nil { + t.Log(err) + } + } +} diff --git a/test/integration/testutils/retry.go b/test/integration/testutils/retry.go index 6ea7211b..2d5f9355 100644 --- a/test/integration/testutils/retry.go +++ b/test/integration/testutils/retry.go @@ -14,6 +14,13 @@ package testutils +import ( + "fmt" + "log" + "regexp" + "time" +) + var ( RetryableTransientErrors = map[string]string{ // Error 409: unable to queue the operation @@ -28,7 +35,7 @@ var ( // Error 403: Kubernetes Engine API has not been used in project {} before or it is disabled. ".*Error 403.*Kubernetes Engine API is not enabled for this project*": "Kubernetes Engine API not enabled", - // google_gke_hub_feature - Error: Error waiting to create Feature: Error waiting for Creating Feature: Error code 13, message: an internal error has occurred + // google_gke_hub_feature - Error: Error waiting to create Feature: Error code 13, message: an internal error has occurred ".*Error waiting for Creating Feature: Error code 13, message: an internal error has occurred*.": "Error creating feature", ".*Error waiting for Creating Connection: Error code 9, message: Failed to verify authorizer_credential.*": "servicedirectory.networks.access propagation time", @@ -70,3 +77,37 @@ var ( ".*Error: Error creating FeatureMembership: Resource already exists - apply blocked by lifecycle params.*": "Duplicated membership request", } ) + +// Retry retries a function a given number of times with a delay between attempts. +func Retry(retries int, delay time.Duration, f func() error) error { + for i := 0; i < retries; i++ { + err := f() + if err == nil { + return nil + } + + log.Printf("Attempt %d failed: %v. Retrying in %s...", i+1, err, delay) + + // Check if the error is a transient one + isTransient := false + errMsg := err.Error() + for pattern, desc := range RetryableTransientErrors { + if matched, _ := regexp.MatchString(pattern, errMsg); matched { + log.Printf("Detected transient error: %s - %s", desc, errMsg) + isTransient = true + break + } + } + + if !isTransient && i == retries-1 { + return fmt.Errorf("function failed after %d retries with non-transient error: %w", retries, err) + } else if !isTransient { + // Not a transient error, and not the last retry, so we can't assume it will pass. + // Break early for non-retryable errors. + return fmt.Errorf("function failed with non-transient error: %w", err) + } + + time.Sleep(delay) + } + return fmt.Errorf("function failed after %d retries", retries) +} diff --git a/test/integration/testutils/strings.go b/test/integration/testutils/strings.go new file mode 100644 index 00000000..3e4d9146 --- /dev/null +++ b/test/integration/testutils/strings.go @@ -0,0 +1,48 @@ +// Copyright 2024-2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "strings" +) + +var ( + ServicesNames = map[string][]string{ + "cymbal-bank": { + "accounts-contacts", + "accounts-userservice", + "frontend", + "ledger-balancereader", + "ledger-ledgerwriter", + "ledger-transactionhistory", + }, + } + + ServicesWithEnvProject = map[string][]string{ + "cymbal-bank": { + "userservice", + "ledgerwriter", + }, + } + + AppNames = []string{ + "cymbal-bank", + } +) + +func GetLastSplitElement(value string, sep string) string { + splitted := strings.Split(value, sep) + return splitted[len(splitted)-1] +} diff --git a/test/setup/harness/service_perimeter/outputs.tf b/test/setup/harness/service_perimeter/outputs.tf new file mode 100644 index 00000000..2edac0fe --- /dev/null +++ b/test/setup/harness/service_perimeter/outputs.tf @@ -0,0 +1,44 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "protected_projects" { + value = var.protected_projects + depends_on = [time_sleep.wait_vpc_sc_propagation] +} + +output "service_perimeter_name" { + description = "Service Perimeter name." + value = "accessPolicies/${google_access_context_manager_access_policy.access_policy.name}/servicePerimeters/${module.regular_service_perimeter.perimeter_name}" + depends_on = [time_sleep.wait_vpc_sc_propagation] +} + +output "access_level_name" { + description = "Access level name." + value = module.access_level_members.name_id + depends_on = [time_sleep.wait_vpc_sc_propagation] +} + +output "service_perimeter_mode" { + description = "(VPC-SC) Service perimeter mode: ENFORCE, DRY_RUN." + value = var.service_perimeter_mode + depends_on = [time_sleep.wait_vpc_sc_propagation] +} + +output "access_context_manager_name" { + description = "Access context manager name." + value = google_access_context_manager_access_policy.access_policy.name + depends_on = [time_sleep.wait_vpc_sc_propagation] +} diff --git a/test/setup/harness/service_perimeter/variables.tf b/test/setup/harness/service_perimeter/variables.tf new file mode 100644 index 00000000..11647386 --- /dev/null +++ b/test/setup/harness/service_perimeter/variables.tf @@ -0,0 +1,52 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "org_id" { + description = "The numeric organization id" + type = string +} + +variable "folder_id" { + description = "Seed folder id." + type = string +} + +variable "project_id" { + type = string + description = "Project ID in which all resources will be deployed" +} + +variable "protected_projects" { + description = "The projects number to be protected." + type = list(string) +} + +variable "service_perimeter_mode" { + description = "(VPC-SC) Service perimeter mode: ENFORCE, DRY_RUN." + type = string + default = "DRY_RUN" + + validation { + condition = contains(["ENFORCE", "DRY_RUN"], var.service_perimeter_mode) + error_message = "The service_perimeter_mode value must be one of: ENFORCE, DRY_RUN." + } +} + +variable "access_level_members" { + description = "Extra access level members. serviceAccount:EMAIL@DOMAIN or user:EMAIL@DOMAIN" + type = list(string) + default = [] +} diff --git a/test/setup/harness/service_perimeter/versions.tf b/test/setup/harness/service_perimeter/versions.tf new file mode 100644 index 00000000..c93ccfe0 --- /dev/null +++ b/test/setup/harness/service_perimeter/versions.tf @@ -0,0 +1,37 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 6.20, < 8" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 6.20, < 8" + } + time = { + source = "hashicorp/time" + version = ">= 0.12.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.7.2" + } + } +} diff --git a/test/setup/harness/service_perimeter/vpc-sc.tf b/test/setup/harness/service_perimeter/vpc-sc.tf new file mode 100644 index 00000000..272eeff7 --- /dev/null +++ b/test/setup/harness/service_perimeter/vpc-sc.tf @@ -0,0 +1,493 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 = "secure_ci_cd" + access_level_name = "alp_${local.prefix}_members_${random_string.random_access_level_suffix.result}" + perimeter_name = "sp_${local.prefix}_perimeter_${random_string.random_access_level_suffix.result}" + access_level_members = concat( + var.access_level_members, + [ + "serviceAccount:${google_project_service_identity.cloudbuild_service_agent.email}", + "serviceAccount:${google_project_service_identity.clouddeploy_service_agent.email}", + ] + ) + supported_restricted_service = [ + "accessapproval.googleapis.com", + "agentcommunication.googleapis.com", + "addressvalidation.googleapis.com", + "adsdatahub.googleapis.com", + "aiplatform.googleapis.com", + "alloydb.googleapis.com", + "analyticshub.googleapis.com", + "apigee.googleapis.com", + "apigeeconnect.googleapis.com", + "apihub.googleapis.com", + "apikeys.googleapis.com", + "apphub.googleapis.com", + "artifactregistry.googleapis.com", + "assuredoss.googleapis.com", + "assuredworkloads.googleapis.com", + "auditmanager.googleapis.com", + "automl.googleapis.com", + "autoscaling.googleapis.com", + "backupdr.googleapis.com", + "baremetalsolution.googleapis.com", + "batch.googleapis.com", + "beyondcorp.googleapis.com", + "biglake.googleapis.com", + "bigquery.googleapis.com", + "bigquerydatapolicy.googleapis.com", + "bigquerydatatransfer.googleapis.com", + "bigquerymigration.googleapis.com", + "bigtable.googleapis.com", + "binaryauthorization.googleapis.com", + "blockchainnodeengine.googleapis.com", + "certificatemanager.googleapis.com", + "cloud.googleapis.com", + "cloudaicompanion.googleapis.com", + "cloudasset.googleapis.com", + "cloudbuild.googleapis.com", + "cloudcode.googleapis.com", + "cloudcontrolspartner.googleapis.com", + "clouddeploy.googleapis.com", + "clouderrorreporting.googleapis.com", + "cloudfunctions.googleapis.com", + "cloudkms.googleapis.com", + "cloudprofiler.googleapis.com", + "cloudquotas.googleapis.com", + "cloudresourcemanager.googleapis.com", + "cloudscheduler.googleapis.com", + "cloudsearch.googleapis.com", + "cloudsecuritycompliance.googleapis.com", + "cloudsupport.googleapis.com", + "cloudtasks.googleapis.com", + "cloudtrace.googleapis.com", + "commerceorggovernance.googleapis.com", + "composer.googleapis.com", + "compute.googleapis.com", + "confidentialcomputing.googleapis.com", + "config.googleapis.com", + "configdelivery.googleapis.com", + "connectgateway.googleapis.com", + "connectors.googleapis.com", + "contactcenteraiplatform.googleapis.com", + "contactcenterinsights.googleapis.com", + "container.googleapis.com", + "containeranalysis.googleapis.com", + "containerfilesystem.googleapis.com", + "containerregistry.googleapis.com", + "containersecurity.googleapis.com", + "containerthreatdetection.googleapis.com", + "contentwarehouse.googleapis.com", + "databasecenter.googleapis.com", + "databaseinsights.googleapis.com", + "datacatalog.googleapis.com", + "dataflow.googleapis.com", + "dataform.googleapis.com", + "datafusion.googleapis.com", + "datalineage.googleapis.com", + "datamigration.googleapis.com", + "datapipelines.googleapis.com", + "dataplex.googleapis.com", + "dataproc.googleapis.com", + "dataprocgdc.googleapis.com", + "datastream.googleapis.com", + "developerconnect.googleapis.com", + "dialogflow.googleapis.com", + "discoveryengine.googleapis.com", + "dlp.googleapis.com", + "dns.googleapis.com", + "documentai.googleapis.com", + "domains.googleapis.com", + "earthengine.googleapis.com", + "edgecontainer.googleapis.com", + "edgenetwork.googleapis.com", + "essentialcontacts.googleapis.com", + "eventarc.googleapis.com", + "eventarcpublishing.googleapis.com", + "file.googleapis.com", + "financialservices.googleapis.com", + "firebaseappcheck.googleapis.com", + "firebasecrashlytics.googleapis.com", + "firebasedataconnect.googleapis.com", + "firebaserules.googleapis.com", + "firebasevertexai.googleapis.com", + "firestore.googleapis.com", + "gameservices.googleapis.com", + "geocoding-backend.googleapis.com", + "gkebackup.googleapis.com", + "gkeconnect.googleapis.com", + "gkehub.googleapis.com", + "gkemulticloud.googleapis.com", + "gkeonprem.googleapis.com", + "healthcare.googleapis.com", + "iam.googleapis.com", + "iamcredentials.googleapis.com", + "iap.googleapis.com", + "iaptunnel.googleapis.com", + "identitytoolkit.googleapis.com", + "ids.googleapis.com", + "integrations.googleapis.com", + "kmsinventory.googleapis.com", + "krmapihosting.googleapis.com", + "kubernetesmetadata.googleapis.com", + "language.googleapis.com", + "licensemanager.googleapis.com", + "lifesciences.googleapis.com", + "livestream.googleapis.com", + "logging.googleapis.com", + "looker.googleapis.com", + "managedidentities.googleapis.com", + "managedkafka.googleapis.com", + "memcache.googleapis.com", + "memorystore.googleapis.com", + "meshca.googleapis.com", + "meshconfig.googleapis.com", + "metastore.googleapis.com", + "microservices.googleapis.com", + "migrationcenter.googleapis.com", + "ml.googleapis.com", + "modelarmor.googleapis.com", + "monitoring.googleapis.com", + "netapp.googleapis.com", + "networkconnectivity.googleapis.com", + "networkmanagement.googleapis.com", + "networksecurity.googleapis.com", + "networkservices.googleapis.com", + "notebooks.googleapis.com", + "ondemandscanning.googleapis.com", + "opsconfigmonitoring.googleapis.com", + "orgpolicy.googleapis.com", + "osconfig.googleapis.com", + "oslogin.googleapis.com", + "parallelstore.googleapis.com", + "parametermanager.googleapis.com", + "places.googleapis.com", + "policysimulator.googleapis.com", + "policytroubleshooter.googleapis.com", + "privateca.googleapis.com", + "privilegedaccessmanager.googleapis.com", + "publicca.googleapis.com", + "pubsub.googleapis.com", + "pubsublite.googleapis.com", + "rapidmigrationassessment.googleapis.com", + "recaptchaenterprise.googleapis.com", + "recommender.googleapis.com", + "redis.googleapis.com", + "retail.googleapis.com", + "run.googleapis.com", + "seclm.googleapis.com", + "secretmanager.googleapis.com", + "securesourcemanager.googleapis.com", + "securetoken.googleapis.com", + "securitycenter.googleapis.com", + "securitycentermanagement.googleapis.com", + "servicecontrol.googleapis.com", + "servicedirectory.googleapis.com", + "servicehealth.googleapis.com", + "servicenetworking.googleapis.com", + "serviceusage.googleapis.com", + "spanner.googleapis.com", + "speakerid.googleapis.com", + "speech.googleapis.com", + "sqladmin.googleapis.com", + "ssh-serialport.googleapis.com", + "storage.googleapis.com", + "storagebatchoperations.googleapis.com", + "storageinsights.googleapis.com", + "storagetransfer.googleapis.com", + "sts.googleapis.com", + "telemetry.googleapis.com", + "texttospeech.googleapis.com", + "timeseriesinsights.googleapis.com", + "tpu.googleapis.com", + "trafficdirector.googleapis.com", + "transcoder.googleapis.com", + "translate.googleapis.com", + "videointelligence.googleapis.com", + "videostitcher.googleapis.com", + "vision.googleapis.com", + "visionai.googleapis.com", + "visualinspection.googleapis.com", + "vmmigration.googleapis.com", + "vmwareengine.googleapis.com", + "vpcaccess.googleapis.com", + "webrisk.googleapis.com", + "websecurityscanner.googleapis.com", + "workflows.googleapis.com", + "workloadmanager.googleapis.com", + "workstations.googleapis.com", + ] + + egress_rules = [ + { + title = "Egress to service networking project" + from = { + identity_type = "ANY_IDENTITY" + sources = { + resources = ["projects/${data.google_project.project.number}"] + } + }, + to = { + resources = ["projects/213331819513"], //service networking project + operations = { + "compute.googleapis.com" = { methods = ["*"] } + } + } + }, + { + title = "Egress to bank of anthos by AR, CA and BAuthz" + from = { + identity_type = "ANY_IDENTITY" + sources = { + resources = ["projects/${data.google_project.project.number}"] + } + } + to = { + resources = [ + "projects/682719828243" // projects/bank-of-anthos-ci/locations/us-central1/repositories/bank-of-anthos + ] + operations = { + "artifactregistry.googleapis.com" = { methods = ["*"] } + "binaryauthorization.googleapis.com" = { methods = ["*"] } + "cloudkms.googleapis.com" = { methods = ["*"] } + "container.googleapis.com" = { methods = ["*"] } + "containerfilesystem.googleapis.com" = { methods = ["*"] } + "containeranalysis.googleapis.com" = { methods = ["*"] } + "containerregistry.googleapis.com" = { methods = ["*"] } + "storage.googleapis.com" = { methods = ["*"] } + "iamcredentials.googleapis.com" = { methods = ["*"] } + "compute.googleapis.com" = { methods = ["*"] } + "containerfilesystem.googleapis.com" = { methods = ["*"] } + } + } + }, + { + title = "Egress to Proxy Golang Storage project" + from = { + identity_type = "ANY_IDENTITY" + sources = { + resources = ["projects/${data.google_project.project.number}"] + } + } + to = { + resources = [ + "projects/912338787515", //proxy-golang-org-prod + ] + operations = { + "storage.googleapis.com" = { methods = ["*"] } + } + } + }, + { + title = "Egress to Storage project" + from = { + identity_type = "ANY_IDENTITY" + sources = { + resources = ["projects/${data.google_project.project.number}"] + } + } + to = { + resources = [ + "projects/213358688945", + ] + operations = { + "storage.googleapis.com" = { methods = ["*"] } + } + } + }, + { + title = "Egress to Logging bucket project" + from = { + identity_type = "ANY_IDENTITY" + sources = { + resources = ["projects/${data.google_project.project.number}"] + } + } + to = { + resources = [ + "projects/${data.google_project.project.number}" //logging bucket + ] + operations = { + "storage.googleapis.com" = { methods = ["*"] } + } + } + }, + { + title = "Egress from ANY_IDENTITY to artifact-registry-docker-cache" + from = { + identity_type = "ANY_IDENTITY" //https://cloud.google.com/artifact-registry/docs/securing-with-vpc-sc + sources = { + resources = ["projects/${data.google_project.project.number}"] + } + }, + to = { + resources = ["projects/342927644502"], //artifact-registry-docker-cache + operations = { + "artifactregistry.googleapis.com" = { methods = ["*"] } + "binaryauthorization.googleapis.com" = { methods = ["*"] } + "cloudkms.googleapis.com" = { methods = ["*"] } + "container.googleapis.com" = { methods = ["*"] } + "containeranalysis.googleapis.com" = { methods = ["*"] } + "containerfilesystem.googleapis.com" = { methods = ["*"] } + "containerregistry.googleapis.com" = { methods = ["*"] } + "storage.googleapis.com" = { methods = ["*"] } + "iamcredentials.googleapis.com" = { methods = ["*"] } + "compute.googleapis.com" = { methods = ["*"] } + } + } + }, + { + title = "Allow Services from ${var.project_id} to ${var.project_id}" + from = { + identity_type = "ANY_IDENTITY" + sources = { + resources = ["projects/${data.google_project.project.number}"] + } + } + to = { + resources = [ + "projects/${data.google_project.project.number}" //worker pool project + ] + operations = { + "servicedirectory.googleapis.com" = { methods = ["*"] } + "cloudbuild.googleapis.com" = { methods = ["*"] } + "clouddeploy.googleapis.com" = { methods = ["*"] } + "pubsub.googleapis.com" = { methods = ["*"] } + "compute.googleapis.com" = { methods = ["SubnetworksService.Get"] } + } + } + } + ] + + ingress_rules = [ + { + title = "Ingress to Required Resources on Single Project" + from = { + sources = { access_levels = [module.access_level_members.name] } + identity_type = "ANY_IDENTITY" + identities = [] + }, + to = { + resources = ["projects/${data.google_project.project.number}"], + operations = { + "secretmanager.googleapis.com" = { methods = ["*"] } + "cloudkms.googleapis.com" = { methods = ["*"] } + "cloudbuild.googleapis.com" = { methods = ["*"] } + "clouddeploy.googleapis.com" = { methods = ["*"] } + "containeranalysis.googleapis.com" = { methods = ["*"] } + "binaryauthorization.googleapis.com" = { methods = ["*"] } + "serviceusage.googleapis.com" = { methods = ["*"] } + "artifactregistry.googleapis.com" = { methods = ["*"] } + "iam.googleapis.com" = { methods = ["*"] } + "cloudresourcemanager.googleapis.com" = { methods = ["*"] } + "compute.googleapis.com" = { methods = ["*"] } + "storage.googleapis.com" = { methods = ["*"] } + "container.googleapis.com" = { methods = ["*"] } + "pubsub.googleapis.com" = { methods = ["*"] } + "gkehub.googleapis.com" = { methods = ["*"] } + "logging.googleapis.com" = { methods = ["*"] } + } + } + } + ] +} + +data "google_project" "project" { + project_id = var.project_id +} + +resource "google_project_service_identity" "cloudkms_service_account" { + provider = google-beta + project = var.project_id + service = "cloudkms.googleapis.com" +} + +resource "google_project_service_identity" "cloudbuild_service_agent" { + provider = google-beta + project = var.project_id + service = "cloudbuild.googleapis.com" +} + + +resource "google_project_service_identity" "clouddeploy_service_agent" { + provider = google-beta + project = var.project_id + service = "clouddeploy.googleapis.com" +} + +resource "random_string" "random_access_level_suffix" { + length = 4 + lower = true + numeric = true + upper = false + special = false +} + +/****************************************** + Access Context Manager Policy +*******************************************/ + +resource "google_access_context_manager_access_policy" "access_policy" { + parent = "organizations/${var.org_id}" + scopes = [var.folder_id] + title = "Secure CI/CD policy for ${var.folder_id}" +} + +module "access_level_members" { + source = "terraform-google-modules/vpc-service-controls/google//modules/access_level" + version = "~> 8.0" + description = "${local.prefix} Access Level" + policy = google_access_context_manager_access_policy.access_policy.name + name = local.access_level_name + members = local.access_level_members + combining_function = "OR" +} + +module "regular_service_perimeter" { + source = "terraform-google-modules/vpc-service-controls/google//modules/regular_service_perimeter" + version = "~> 8.0" + policy = google_access_context_manager_access_policy.access_policy.name + perimeter_name = local.perimeter_name + resources = var.service_perimeter_mode == "ENFORCE" ? [data.google_project.project.number] : [] + resources_dry_run = [data.google_project.project.number] + description = "Serverless VPC Service Controls perimeter" + access_levels = var.service_perimeter_mode == "ENFORCE" ? [module.access_level_members.name] : [] + access_levels_dry_run = [module.access_level_members.name] + egress_policies = var.service_perimeter_mode == "ENFORCE" ? local.egress_rules : [] + egress_policies_dry_run = local.egress_rules + ingress_policies = var.service_perimeter_mode == "ENFORCE" ? local.ingress_rules : [] + ingress_policies_dry_run = local.ingress_rules + vpc_accessible_services = var.service_perimeter_mode == "ENFORCE" ? ["*"] : [] + vpc_accessible_services_dry_run = ["*"] + restricted_services_dry_run = local.supported_restricted_service + restricted_services = var.service_perimeter_mode == "ENFORCE" ? local.supported_restricted_service : [] +} + +resource "time_sleep" "wait_vpc_sc_propagation" { + depends_on = [ + google_access_context_manager_access_policy.access_policy, + module.access_level_members, + module.regular_service_perimeter + ] + destroy_duration = "5m" + create_duration = "2m" +} + +resource "time_sleep" "destroy_wait_propagation" { + destroy_duration = "5m" +} diff --git a/test/setup/iam.tf b/test/setup/iam.tf index 3e174603..24b18d0b 100644 --- a/test/setup/iam.tf +++ b/test/setup/iam.tf @@ -77,3 +77,33 @@ resource "google_project_iam_member" "gke_int_test_singleproj" { role = each.value member = "serviceAccount:${google_service_account.int_test.email}" } + +resource "google_organization_iam_member" "org_iam_member" { + role = "roles/resourcemanager.organizationAdmin" + member = "serviceAccount:${google_service_account.int_test.email}" + org_id = var.org_id +} + +resource "google_organization_iam_member" "organizationServiceAgent_role" { + org_id = var.org_id + role = "roles/resourcemanager.organizationAdmin" + member = "serviceAccount:${google_service_account.int_test.email}" +} + +resource "google_organization_iam_member" "organization_xpn_role" { + org_id = var.org_id + role = "roles/compute.xpnAdmin" + member = "serviceAccount:${google_service_account.int_test.email}" +} + +resource "google_organization_iam_member" "orgPolicyAdmin_role" { + org_id = var.org_id + role = "roles/orgpolicy.policyAdmin" + member = "serviceAccount:${google_service_account.int_test.email}" +} + +resource "google_organization_iam_member" "policyAdmin_role" { + org_id = var.org_id + role = "roles/accesscontextmanager.policyAdmin" + member = "serviceAccount:${google_service_account.int_test.email}" +} diff --git a/test/setup/main.tf b/test/setup/main.tf index a0e26ed2..bc725898 100644 --- a/test/setup/main.tf +++ b/test/setup/main.tf @@ -123,3 +123,9 @@ resource "google_kms_crypto_key_iam_member" "standalone_gcs_sa_kms" { role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" member = "serviceAccount:${data.google_storage_project_service_account.standalone_gcs_account.email_address}" } + +resource "google_organization_iam_member" "int_test_access_context_manager_admin" { + org_id = var.org_id + role = "roles/accesscontextmanager.policyAdmin" + member = "serviceAccount:${google_service_account.int_test.email}" +} diff --git a/test/setup/outputs.tf b/test/setup/outputs.tf index f00fff67..c5600f0d 100644 --- a/test/setup/outputs.tf +++ b/test/setup/outputs.tf @@ -27,6 +27,10 @@ output "sa_key" { sensitive = true } +output "sa_email" { + value = google_service_account.int_test.email +} + output "folder_id" { value = var.folder_id } From 6269320dea80b2597f4fb48b750643f58fece77a Mon Sep 17 00:00:00 2001 From: marcelorobj Date: Fri, 19 Jun 2026 10:58:14 -0300 Subject: [PATCH 6/6] fix: addressing linter issues. --- modules/secure-ci/versions.tf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/secure-ci/versions.tf b/modules/secure-ci/versions.tf index 182da3b8..c81b0e68 100644 --- a/modules/secure-ci/versions.tf +++ b/modules/secure-ci/versions.tf @@ -33,6 +33,10 @@ terraform { source = "hashicorp/null" version = ">= 3.3.0" } + time = { + source = "hashicorp/time" + version = ">= 0.12.0" + } } provider_meta "google" {