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/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 bba06cb4..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 @@ -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/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/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/app_cicd/policies/container-analysis-policy.yaml b/build/policies/container-analysis-policy.yaml similarity index 97% rename from examples/app_cicd/policies/container-analysis-policy.yaml rename to build/policies/container-analysis-policy.yaml index 82ba2fce..6a31383a 100644 --- a/examples/app_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/app_cicd/policies/githound.yaml b/build/policies/githound.yaml similarity index 97% rename from examples/app_cicd/policies/githound.yaml rename to build/policies/githound.yaml index 55edf755..4cb6343d 100644 --- a/examples/app_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/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/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-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/variables.tf b/examples/app_cicd/variables.tf deleted file mode 100644 index 83a545fe..00000000 --- a/examples/app_cicd/variables.tf +++ /dev/null @@ -1,42 +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 "primary_location" { - type = string - description = "Region used for key-ring" - default = "us-central1" -} - - -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/cloudbuild_private_pool/README.md b/examples/cloudbuild_private_pool/README.md deleted file mode 100644 index 9cacc6e8..00000000 --- a/examples/cloudbuild_private_pool/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Cloud Build Private Pool example -This example demonstrates how to use the [Cloud Build private pool](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/tree/main/modules/cloudbuild-private-pool) and [HA VPN](https://github.com/GoogleCloudPlatform/terraform-google-secure-cicd/tree/main/modules/workerpool-gke-ha-vpn) modules to enable connectivity between Cloud Build and private GKE clusters. - -## Setup -To deploy this example: - -1. Run `terraform init`. - -2. Create a `terraform.tfvars` to provide values for `project_id`, `primary_location` and `gke_networks`. Optionally override any variables if necessary. -See below for an example of the `gke_networks` object: -```tf -gke_networks = [ - { - project_id = gke-dev-project-1 - location = us-central1 - network = gke-private-vpc-dev - control_plane_cidrs = { - "172.16.1.0/28" = "Dev GKE control plane" - } - }, - { - project_id = gke-qa-project-1 - location = us-central1 - network = gke-private-vpc-qa - control_plane_cidrs = { - "172.16.2.0/28" = "QA GKE control plane" - } - }, - { - project_id = gke-prod-project-1 - location = us-central1 - network = gke-private-vpc-prod - control_plane_cidrs = { - "172.16.3.0/28" = "Prod GKE control plane" - } - } -] -``` -3. Modify the `vpn_config` local variable in [main.tf](./main.tf) so that the object's keys correspond to the network names specified in `var.gke_networks`. -4. Run `terraform apply`. - - - -## 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/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/cloud-build-builder/Dockerfile b/examples/private_cluster_cicd/cloud-build-builder/Dockerfile deleted file mode 100644 index 695a7766..00000000 --- a/examples/private_cluster_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/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/policies/container-analysis-policy.yaml b/examples/private_cluster_cicd/policies/container-analysis-policy.yaml deleted file mode 100644 index 82ba2fce..00000000 --- a/examples/private_cluster_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/private_cluster_cicd/policies/githound.yaml b/examples/private_cluster_cicd/policies/githound.yaml deleted file mode 100644 index 55edf755..00000000 --- a/examples/private_cluster_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 | |------|-------------|------|---------|:--------:| -| app\_name | Name of intended deployed application; to be used as a prefix for certain resources | `string` | `"my-app"` | no | +| 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 | | 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. | -| gar\_repo | Artifact Registry repo | -| neos\_tutorial\_url | The URL to launch the in-console tutorial for the Secure CI/CD pipeline solution | +| 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\_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 | +| gar\_repo\_name | Name of the Artifact Registry repository | +| gitlab\_url | The URL of the GitLab instance. | +| 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 ecb2927c..70c7ad1a 100644 --- a/examples/standalone_single_project/cicd.tf +++ b/examples/standalone_single_project/cicd.tf @@ -20,32 +20,32 @@ 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.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}" = { 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.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}" = { 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.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,51 +56,53 @@ 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 = 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" primary_location = var.region - attestor_names_prefix = ["build", "security", "quality"] + access_level_name = var.access_level_name + 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 = 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 } -# 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 + 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 - 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 + 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 - labels = var.labels 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" + 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 @@ -116,3 +118,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..dee00e4b 100644 --- a/examples/standalone_single_project/gke.tf +++ b/examples/standalone_single_project/gke.tf @@ -37,16 +37,16 @@ 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 = "~> 25.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 + 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" @@ -54,6 +54,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] @@ -78,13 +79,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 +101,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..1fd9f276 100644 --- a/examples/standalone_single_project/network.tf +++ b/examples/standalone_single_project/network.tf @@ -40,7 +40,9 @@ locals { # Private Cluster VPCs module "vpc" { source = "terraform-google-modules/network/google" - version = "~> 6.0" + version = "~> 18.0" + + count = var.network_name == null ? 1 : 0 project_id = var.project_id network_name = "${var.app_name}-vpc" @@ -49,14 +51,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..391c35ae 100644 --- a/examples/standalone_single_project/outputs.tf +++ b/examples/standalone_single_project/outputs.tf @@ -14,27 +14,72 @@ * 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 in which all resources were deployed" + value = var.project_id } -output "cloudbuild_cd_repo_name" { - description = "URL of the created CSR app soure repo" - value = "${var.app_name}-cloudbuild-cd-config" +output "region" { + description = "Region in which all regional resources were deployed" + value = var.region } -output "gar_repo" { - description = "Artifact Registry repo" +output "gke_cluster_names" { + description = "Map of GKE Cluster names by environment" + value = { for k, v in module.gke_cluster : k => v.name } +} + +output "clouddeploy_pipeline_id" { + description = "ID of the Cloud Deploy delivery pipeline" + value = module.cd_pipeline.clouddeploy_delivery_pipeline_id +} + +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 "ci_build_trigger_id" { + description = "ID of the CI Cloud Build trigger" + value = module.ci_pipeline.ci_build_trigger_id +} + +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_name" { + description = "Name of the Artifact Registry repository" value = module.ci_pipeline.app_artifact_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 "attestors" { + description = "Map of Binary Authorization attestor IDs by name" + value = module.attestors.binauth_attestor_ids +} + +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 "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 "ci_repo_url" { + description = "The URL of the CI repository." + value = var.ci_repository.repository_url } -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 "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 new file mode 100644 index 00000000..94920cf3 --- /dev/null +++ b/examples/standalone_single_project/terraform.tfvars @@ -0,0 +1,37 @@ +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.yaml b/metadata.yaml index 5366539f..3866d13d 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -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 new code or a code change for a container-based application to Cloud Source Repositories. - - 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. + - 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/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 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/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/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/test/fixtures/app_cicd/outputs.tf b/modules/attestor/outputs.tf similarity index 53% rename from test/fixtures/app_cicd/outputs.tf rename to modules/attestor/outputs.tf index 4dd67449..8faae13f 100644 --- a/test/fixtures/app_cicd/outputs.tf +++ b/modules/attestor/outputs.tf @@ -14,22 +14,17 @@ * 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 "binauth_attestor_names" { + description = "Names of Attestors" + value = [for attestor_name in var.attestor_names_prefix : module.attestors[attestor_name].attestor] } -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 "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 "gke_project_ids" { - description = "The name of the cloud build trigger for the bank of anthos repo." - value = values(var.gke_project_ids) +output "binauth_attestor_project_id" { + description = "Project ID where attestors get created" + value = var.project_id } 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 73% rename from test/fixtures/app_cicd/versions.tf rename to modules/attestor/versions.tf index c001c4e3..0185ed5b 100644 --- a/test/fixtures/app_cicd/versions.tf +++ b/modules/attestor/versions.tf @@ -15,5 +15,15 @@ */ terraform { - required_version = ">= 0.13" + required_version = ">= 1.3" + 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..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" @@ -49,8 +48,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 @@ -81,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.yaml b/modules/cloudbuild-private-pool/metadata.yaml index fad3dc2b..15f7b88a 100644 --- a/modules/cloudbuild-private-pool/metadata.yaml +++ b/modules/cloudbuild-private-pool/metadata.yaml @@ -28,124 +28,150 @@ spec: version: 1.2.2 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 - defaultValue: null - 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 - defaultValue: null - 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 - defaultValue: null - 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 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_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 0ce7cd32..3962530d 100644 --- a/modules/cloudbuild-private-pool/versions.tf +++ b/modules/cloudbuild-private-pool/versions.tf @@ -15,15 +15,15 @@ */ terraform { - required_version = ">= 0.13.0" + required_version = "~> 1.3" 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-beta = { source = "hashicorp/google-beta" - version = ">= 4.3.0" + version = ">= 4.3.0, < 8" } } diff --git a/modules/secure-cd/README.md b/modules/secure-cd/README.md index 6627380d..d7b85b64 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,21 +137,26 @@ 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 | 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/build.tf b/modules/secure-cd/build.tf index d0bc80f0..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,19 +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" { - 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}" @@ -41,9 +169,9 @@ resource "google_cloudbuild_trigger" "deploy_trigger" { } 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" + ref = "main" + repo_type = local.cd_repo_source.repo_type + repository = local.cd_repo_source.repository } substitutions = merge( @@ -60,17 +188,15 @@ 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) 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..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,12 +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" { @@ -59,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 @@ -74,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 @@ -82,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 @@ -98,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 @@ -112,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 @@ -128,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/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..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. @@ -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" @@ -87,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 28adcce6..867f63ce 100644 --- a/modules/secure-cd/metadata.display.yaml +++ b/modules/secure-cd/metadata.display.yaml @@ -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 63545dcc..e182f161 100644 --- a/modules/secure-cd/metadata.yaml +++ b/modules/secure-cd/metadata.yaml @@ -28,141 +28,194 @@ spec: version: 1.2.2 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 - defaultValue: null - required: true - - name: cache_bucket_name - description: cloud build artifact bucket name - varType: string - defaultValue: null - required: true - - name: cloudbuild_cd_repo - description: Name of repo that stores the Cloud Build CD phase configs - for post-deployment checks - varType: string - defaultValue: null - 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 - defaultValue: null - required: true - - name: clouddeploy_pipeline_name - description: Cloud Deploy pipeline name - varType: string - defaultValue: null - 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 - defaultValue: null - 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 - defaultValue: null - required: true - - name: project_id - description: Project ID for CICD Pipeline Project - varType: string - defaultValue: null - 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: 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 + description: ID(s) of Cloud Deploy targets + - name: deploy_trigger_names + description: Names of CD Cloud Build triggers 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" diff --git a/modules/secure-cd/outputs.tf b/modules/secure-cd/outputs.tf index d7af95ff..cddeb4ba 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. @@ -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" { @@ -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-cd/variables.tf b/modules/secure-cd/variables.tf index a364630e..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. @@ -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 0a5f0b7f..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,15 +15,15 @@ */ terraform { - required_version = ">= 1.0" + required_version = "~> 1.3" 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" } } diff --git a/modules/secure-ci/README.md b/modules/secure-ci/README.md index 425d80a2..794476bb 100644 --- a/modules/secure-ci/README.md +++ b/modules/secure-ci/README.md @@ -1,58 +1,122 @@ # 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 | 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 | -| 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 | -| build\_image\_config\_yaml | Name of image builder yaml file | `string` | n/a | yes | +| bucket\_kms\_key | KMS Key id to be used to encrypt bucket. | `string` | `null` | no | | 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 | -| 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 | +| repository\_type | The type of the repository. Must be one of 'GITHUB', 'GITLAB', or 'CSR'. | `string` | n/a | yes | | 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 @@ -63,9 +127,11 @@ 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 | +| 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 | +| standalone\_bucket\_kms\_key | KMS Key for standalone bucket. | 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/cloudbuild_private_pool/outputs.tf b/modules/secure-ci/checks.tf similarity index 61% rename from examples/cloudbuild_private_pool/outputs.tf rename to modules/secure-ci/checks.tf index b2acdd66..550a1850 100644 --- a/examples/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" { - value = var.project_id - description = "The project to run tests against" +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/examples/app_cicd/cloud-build-builder/Dockerfile b/modules/secure-ci/cloud-build-builder/Dockerfile similarity index 86% rename from examples/app_cicd/cloud-build-builder/Dockerfile rename to modules/secure-ci/cloud-build-builder/Dockerfile index 695a7766..9065220f 100644 --- a/examples/app_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/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/locals.tf b/modules/secure-ci/locals.tf new file mode 100644 index 00000000..22c9e167 --- /dev/null +++ b/modules/secure-ci/locals.tf @@ -0,0 +1,40 @@ +/** + * 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 } + 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..d9c9d9f4 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,20 +14,48 @@ * 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 "random_string" "suffix" { + length = 4 + special = false + upper = false } -resource "google_sourcerepo_repository" "repos" { - for_each = toset([var.cloudbuild_cd_repo, var.app_source_repo]) - name = each.key - project = var.project_id +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 +} + +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" { 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 @@ -35,79 +63,71 @@ resource "google_storage_bucket" "cache_bucket" { enabled = true } labels = var.labels -} -resource "google_service_account" "build_sa" { - account_id = "build-sa" - display_name = "Service Account for ${var.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] + dynamic "encryption" { + for_each = var.bucket_kms_key != null ? [1] : [] + content { + default_kms_key_name = var.bucket_kms_key + } + } } -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] -} - -# 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 + depends_on = [google_sourcerepo_repository.csr_ci_repository] } -# Cloud Build Service Account permissions -resource "google_project_iam_member" "build_sa_project_iam" { - for_each = toset(var.cloudbuild_service_account_roles) +resource "google_cloudbuild_trigger" "app_build_trigger" { + count = local.use_csr ? 0 : 1 project = var.project_id - role = each.value - member = "serviceAccount:${google_service_account.build_sa.email}" + 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] } 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/metadata.display.yaml b/modules/secure-ci/metadata.display.yaml index be1445fb..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 @@ -38,12 +41,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 +65,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 +86,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 a9473f32..183cb553 100644 --- a/modules/secure-ci/metadata.yaml +++ b/modules/secure-ci/metadata.yaml @@ -28,176 +28,224 @@ spec: version: 1.2.2 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: 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 - defaultValue: null - 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) - defaultValue: null - required: true - - name: build_image_config_yaml - description: Name of image builder yaml file - varType: string - defaultValue: null - 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 - defaultValue: null - required: true - - name: project_id - description: Project ID for CICD Pipeline Project - varType: string - defaultValue: null - 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 - defaultValue: null - 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: 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 + 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: 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: 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: 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 + description: Name of the created CSR repos + - 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 - 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/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 039bc861..b3c439bd 100644 --- a/modules/secure-ci/outputs.tf +++ b/modules/secure-ci/outputs.tf @@ -19,19 +19,14 @@ 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}" } + value = { for key, attestor in local.attestors : key => attestor.id } } output "binauth_attestor_project_id" { @@ -44,17 +39,32 @@ 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" { description = "Cloud Build Service Account email address" value = google_service_account.build_sa.email } + +output "standalone_bucket_kms_key" { + description = "KMS Key for standalone bucket." + 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 +} + +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/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..af47f7ca 100644 --- a/modules/secure-ci/variables.tf +++ b/modules/secure-ci/variables.tf @@ -29,34 +29,23 @@ variable "attestor_names_prefix" { type = list(string) } -variable "app_build_trigger_yaml" { - type = string - description = "Name of application cloudbuild yaml file" -} - -variable "runner_build_folder" { +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 - description = "Path to the source folder for the cloud builds submit command. Leave blank if `skip_provisioners = true`" - default = "" + default = null } -variable "build_image_config_yaml" { +variable "app_build_trigger_yaml" { type = string - description = "Name of image builder yaml file" + description = "Name of application cloudbuild 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,16 +54,10 @@ 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" } -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." @@ -118,14 +101,73 @@ 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) 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 e9434ee9..c81b0e68 100644 --- a/modules/secure-ci/versions.tf +++ b/modules/secure-ci/versions.tf @@ -15,19 +15,27 @@ */ terraform { - required_version = ">= 0.13.0" + required_version = "~> 1.3" required_providers { google = { source = "hashicorp/google" - version = ">= 3.45" + version = "> 4, < 8" + } + random = { + source = "hashicorp/random" + version = ">= 3.7.2" } google-beta = { source = "hashicorp/google-beta" - version = ">= 3.45" + version = "> 4, < 8" } - random = { - source = "hashicorp/random" - version = ">= 3.3.2" + null = { + source = "hashicorp/null" + version = ">= 3.3.0" + } + time = { + source = "hashicorp/time" + version = ">= 0.12.0" } } diff --git a/modules/workerpool-gke-ha-vpn/main.tf b/modules/workerpool-gke-ha-vpn/main.tf index 195dd0b6..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,10 +38,12 @@ 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 @@ -53,10 +54,12 @@ 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 @@ -86,10 +89,12 @@ 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 @@ -100,10 +105,12 @@ 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.yaml b/modules/workerpool-gke-ha-vpn/metadata.yaml index 42b9bd21..0d339e19 100644 --- a/modules/workerpool-gke-ha-vpn/metadata.yaml +++ b/modules/workerpool-gke-ha-vpn/metadata.yaml @@ -28,142 +28,162 @@ spec: version: 1.2.2 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) - defaultValue: null - required: true - - name: gke_location - description: Region of GKE subnet & cluster - varType: string - defaultValue: null - required: true - - name: gke_network - description: Name of GKE VPC - varType: string - defaultValue: null - required: true - - name: gke_project - description: Project ID of GKE VPC and cluster - varType: string - defaultValue: null - 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 - defaultValue: null - 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 - defaultValue: null - required: true - - name: workerpool_range - description: Address range of Cloud Build Workerpool - varType: string - defaultValue: null - 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 56edfa60..de928eec 100644 --- a/modules/workerpool-gke-ha-vpn/versions.tf +++ b/modules/workerpool-gke-ha-vpn/versions.tf @@ -15,15 +15,11 @@ */ terraform { - required_version = ">= 0.13.0" + required_version = "~> 1.3" required_providers { google = { source = "hashicorp/google" - version = ">= 3.45" - } - google-beta = { - source = "hashicorp/google-beta" - version = ">= 3.45" + version = "> 4, < 8" } } 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/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/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/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..3eb8c0ae 100644 --- a/test/integration/go.mod +++ b/test/integration/go.mod @@ -1,77 +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 ( + 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/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 + 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/otiai10/copy v1.11.0 - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/tidwall/gjson v1.14.4 // indirect + 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 b849132b..96d4554a 100644 --- a/test/integration/go.sum +++ b/test/integration/go.sum @@ -1,446 +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/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/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/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= @@ -448,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..eefa544e --- /dev/null +++ b/test/integration/harness_setup_gitlab/bootstrap_gitlab_environment_test.go @@ -0,0 +1,221 @@ +// 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")) + + 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")) + + 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") + + accessLevelName := servicePerimeterOutput.GetStringOutput("access_level_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 := "../../.." + + 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) + } + + // 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/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/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..0355ef1d 100644 --- a/test/integration/standalone_single_project/standalone_single_project_test.go +++ b/test/integration/standalone_single_project/standalone_single_project_test.go @@ -1,147 +1,179 @@ -/** - * 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 +// 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" - // 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/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/terraform-google-modules/terraform-google-secure-cicd/test/integration/testutils" + "github.com/tidwall/gjson" ) -// name the function as Test* -func TestStandaloneSingleProjectExample(t *testing.T) { +// 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)) +} - // initialize Terraform test from the Blueprints test framework - setupOutput := tft.NewTFBlueprintTest(t) - projectID := setupOutput.GetTFSetupStringOutput("project_id_standalone") +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) + } +} - // wire setup output project_id_standalone to example var.project_id - standaloneSingleProjT := tft.NewTFBlueprintTest(t, tft.WithVars(map[string]interface{}{"project_id": projectID})) +func setupGitOperations(t *testing.T, bpFolder, wsFolder, ciRepoName string, cdRepoName string) { + t.Log("Starting Git Operations Setup...") - // define and write a custom verifier for this test case call the default verify for confirming no additional changes - standaloneSingleProjT.DefineVerify(func(assert *assert.Assertions) { - // perform default verification ensuring Terraform reports no additional changes on an applied blueprint - standaloneSingleProjT.DefaultVerify(assert) + 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") - 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) - } - } + token, err := testutils.GetSecretFromSecretManager(t, gitlabPersonalTokenSecretName, gitlabSecretProject) + if err != nil { + t.Fatal(err) + } - 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) - } + 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) + + // Configure Git + runCmd(t, wsFolder, "git", "config", "--global", "user.email", "test-user@example.com") + runCmd(t, wsFolder, "git", "config", "--global", "user.name", "Test User") + + cdRepoLocalPath := filepath.Join(wsFolder, cdRepoName) + + 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) + } + } + + 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") - 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 - } + cfdYamlDest := filepath.Join(cdRepoLocalPath, "cloudbuild-cd.yaml") + runCmd(t, bpFolder, "cp", "/workspace/build/cloudbuild-cd.yaml", cfdYamlDest) + + cdGitRun("add", ".") + cdGit.CommitWithMsg("initial commit", []string{"--allow-empty"}) + cdGitRun("push", "origin", "main", "--force") + + appRepoLocalPath := filepath.Join(wsFolder, "bank-of-anthos") + + 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) } - 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 - } + } + + 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) } - utils.Poll(t, pollCloudDeploy(rolloutListCmd), 30, 60*time.Second) + } + appGitRun("checkout", "-b", "main") + + ciYamlDest := filepath.Join(appRepoLocalPath, "cloudbuild-ci.yaml") + runCmd(t, bpFolder, "cp", "/workspace/build/cloudbuild-ci.yaml", ciYamlDest) + + policiesDest := filepath.Join(appRepoLocalPath, "policies") + runCmd(t, bpFolder, "cp", "-R", "/workspace/build/policies", policiesDest) + + 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.") +} + +func TestStandaloneSingleProjectExample(t *testing.T) { + + setupOutput := tft.NewTFBlueprintTest(t, tft.WithTFDir("../../setup")) + projectID := setupOutput.GetStringOutput("project_id_standalone") + region := setupOutput.GetStringOutput("primary_location") + + blueprintFolder := ("../../../../") + + 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) { + // 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) + }) - // call the test function to execute the integration 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 new file mode 100644 index 00000000..2d5f9355 --- /dev/null +++ b/test/integration/testutils/retry.go @@ -0,0 +1,113 @@ +// 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 ( + "fmt" + "log" + "regexp" + "time" +) + +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 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", + } +) + +// 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/integration/testutils/utils.go b/test/integration/testutils/utils.go new file mode 100644 index 00000000..e1b7bea8 --- /dev/null +++ b/test/integration/testutils/utils.go @@ -0,0 +1,96 @@ +// 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 ( + "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) +} + +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 +} + +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 +} + +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/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 new file mode 100644 index 00000000..fba2377e --- /dev/null +++ b/test/setup/harness/gitlab/gitlab_vm.tf @@ -0,0 +1,348 @@ +/** + * 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_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_standalone + display_name = "Custom SA for VM Instance" +} + +resource "google_project_iam_member" "secret_manager_admin_vm_instance" { + project = var.project_id_standalone + role = "roles/secretmanager.admin" + member = google_service_account.gitlab_vm.member +} + +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_standalone + machine_type = "n2-standard-4" + zone = "us-central1-a" + + tags = ["git-vm", "direct-gateway-access"] + + 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_standalone + } + + metadata_startup_script = file("./../../scripts/gitlab_self_hosted.sh") + + service_account { + 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_standalone + 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_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" { + 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_standalone + + allow { + protocol = "tcp" + ports = ["443"] + } + + source_ranges = ["0.0.0.0/0"] + target_tags = ["git-vm"] +} + +// ======================================================= +// 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_standalone +} + +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 = "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 + } + } +} + +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" +} + +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" +} + +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" +} + +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_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 new file mode 100644 index 00000000..471c0975 --- /dev/null +++ b/test/setup/harness/gitlab/outputs.tf @@ -0,0 +1,59 @@ +/** + * 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 +} + +output "gitlab_pat_secret_name" { + value = "gitlab-pat-from-vm" +} + +output "gitlab_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_standalone +} + +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 +} + +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 new file mode 100644 index 00000000..b6bab93e --- /dev/null +++ b/test/setup/harness/gitlab/variables.tf @@ -0,0 +1,61 @@ +/** + * 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 +} + +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 "project_number_standalone" { + description = "The seed project number." + type = string +} + +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/fixtures/private_cluster_cicd/versions.tf b/test/setup/harness/gitlab/versions.tf similarity index 57% rename from test/fixtures/private_cluster_cicd/versions.tf rename to test/setup/harness/gitlab/versions.tf index c001c4e3..1a5d8c9e 100644 --- a/test/fixtures/private_cluster_cicd/versions.tf +++ b/test/setup/harness/gitlab/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,5 +15,23 @@ */ terraform { - required_version = ">= 0.13" + 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/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/fixtures/cloudbuild_private_pool/outputs.tf b/test/setup/harness/logging_bucket/outputs.tf similarity index 64% rename from test/fixtures/cloudbuild_private_pool/outputs.tf rename to test/setup/harness/logging_bucket/outputs.tf index 51febe77..e1bfce1b 100644 --- a/test/fixtures/cloudbuild_private_pool/outputs.tf +++ b/test/setup/harness/logging_bucket/outputs.tf @@ -1,5 +1,5 @@ /** - * Copyright 2021 Google LLC + * 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. @@ -14,7 +14,18 @@ * limitations under the License. */ -output "project_id" { - description = "The ID of the project in which resources are provisioned." - value = var.project_id +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/fixtures/cloudbuild_private_pool/variables.tf b/test/setup/harness/logging_bucket/variables.tf similarity index 54% rename from test/fixtures/cloudbuild_private_pool/variables.tf rename to test/setup/harness/logging_bucket/variables.tf index 78bc11e5..bc43cd71 100644 --- a/test/fixtures/cloudbuild_private_pool/variables.tf +++ b/test/setup/harness/logging_bucket/variables.tf @@ -1,5 +1,5 @@ /** - * Copyright 2021 Google LLC + * 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. @@ -14,28 +14,23 @@ * limitations under the License. */ -variable "project_id" { - description = "The ID of the CI/CD project to provision resources." +variable "project_id_standalone" { + description = "The seed project." type = string } -variable "primary_location" { +variable "sa_email" { + description = "The ci service account email created by setup to run the tests." 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 "cloud_build_sa" { + description = "Cloud Build Service Account email to be granted Encrypt/Decrypt role." + type = string } -variable "gke_private_service_accounts" { - type = map(string) - description = "map of env name to GKE service account" +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/fixtures/app_cicd/variables.tf b/test/setup/harness/private_workerpool/variables.tf similarity index 67% rename from test/fixtures/app_cicd/variables.tf rename to test/setup/harness/private_workerpool/variables.tf index b462f419..6572e423 100644 --- a/test/fixtures/app_cicd/variables.tf +++ b/test/setup/harness/private_workerpool/variables.tf @@ -1,5 +1,5 @@ /** - * Copyright 2021 Google LLC + * 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. @@ -14,36 +14,39 @@ * limitations under the License. */ -variable "project_id" { - description = "The ID of the CI/CD project to provision resources." +variable "folder_id" { + description = "The folder to deploy in" type = string } -variable "org_id" { - description = "The numeric organization id" +variable "project_id_standalone" { + description = "The seed project id" + type = string } -variable "folder_id" { - description = "The folder to deploy in" +variable "project_number_standalone" { + description = "The seed project number" + type = string } -variable "primary_location" { +variable "org_id" { + description = "The numeric organization id" 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" + type = string } -variable "gke_project_ids" { - type = map(string) - description = "map of env name to GKE project ID" +variable "workpool_region" { + description = "The region to deploy in" + type = string + default = "us-central1" } -variable "gke_service_accounts" { - type = map(string) - description = "map of env name to GKE service account" +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/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 a8beda20..24b18d0b 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) @@ -101,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 dff793fc..bc725898 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,38 @@ 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}" +} + +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/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/fixtures/private_cluster_cicd/variables.tf b/test/setup/modules/private_workerpool/variables.tf similarity index 51% rename from test/fixtures/private_cluster_cicd/variables.tf rename to test/setup/modules/private_workerpool/variables.tf index 6b0ecc08..f0a9cda5 100644 --- a/test/fixtures/private_cluster_cicd/variables.tf +++ b/test/setup/modules/private_workerpool/variables.tf @@ -1,5 +1,5 @@ /** - * Copyright 2021 Google LLC + * 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. @@ -14,46 +14,44 @@ * limitations under the License. */ -variable "project_id" { - description = "The ID of the CI/CD project to provision resources." +variable "folder_id" { + description = "The folder to deploy in" type = string } -variable "org_id" { - description = "The numeric organization id" +variable "project_id_standalone" { + description = "The seed project id" + type = string } -variable "folder_id" { - description = "The folder to deploy in" +variable "project_number_standalone" { + description = "The seed project number" + type = string } -variable "primary_location" { +variable "org_id" { + description = "The numeric organization id" 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" + type = string } -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 "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 "gke_private_vpc_names" { - type = map(string) - description = "map of env name to GKE network name" +variable "workpool_region" { + description = "The region to deploy in" + type = string + default = "us-central1" } -variable "gke_private_service_accounts" { - type = map(string) - description = "map of env name to GKE service account" +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..c5600f0d 100644 --- a/test/setup/outputs.tf +++ b/test/setup/outputs.tf @@ -14,19 +14,23 @@ * 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 } +output "sa_email" { + value = google_service_account.int_test.email +} + output "folder_id" { value = var.folder_id } @@ -39,37 +43,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 new file mode 100644 index 00000000..bef8ead7 --- /dev/null +++ b/test/setup/scripts/gitlab_self_hosted.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# 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 + + +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 +gcloud storage cp gitlab.crt gs://"${PROJECT_ID}"-ssl-cert + +cat > /etc/gitlab/gitlab.rb <