diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 54d675d84..fa0a0e116 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,6 @@ repos: - marko - requests - yamale - - yapf - jsonschema - BeautifulSoup4 # types: [terraform] @@ -56,7 +55,6 @@ repos: - marko - requests - yamale - - yapf - jsonschema - BeautifulSoup4 pass_filenames: true @@ -74,12 +72,10 @@ repos: - marko - requests - yamale - - yapf - jsonschema - BeautifulSoup4 - pass_filenames: false + pass_filenames: true require_serial: true - files: ^fast/.*tf - id: versions name: Align Terraform/OpenTofu provider versions language: python @@ -99,7 +95,6 @@ repos: - marko - requests - yamale - - yapf - jsonschema - BeautifulSoup4 pass_filenames: false @@ -116,7 +111,6 @@ repos: - marko - requests - yamale - - yapf - jsonschema - BeautifulSoup4 entry: tools/check_links.py --no-show-summary --scan-files @@ -158,6 +152,7 @@ repos: rev: v0.40.2 hooks: - id: yapf + require_serial: true - repo: https://github.com/codespell-project/codespell rev: v2.4.1 diff --git a/fast/stages/0-org-setup/cicd.tf b/fast/stages/0-org-setup/cicd.tf index 6d09e56e7..f7006a797 100644 --- a/fast/stages/0-org-setup/cicd.tf +++ b/fast/stages/0-org-setup/cicd.tf @@ -23,7 +23,7 @@ locals { "$iam_principals:${k}" => v } cicd_ctx_wif = try({ - "$wif_pools:${local.wif_pool_name}" = google_iam_workload_identity_pool.default.0.name + "$wif_pools:${local.wif_pool_name}" = google_iam_workload_identity_pool.default[0].name }, {}) # normalize workflow configurations cicd_workflows = { diff --git a/fast/stages/0-org-setup/datasets/hardened/README.md b/fast/stages/0-org-setup/datasets/hardened/README.md index 5334b7678..d92a1a350 100644 --- a/fast/stages/0-org-setup/datasets/hardened/README.md +++ b/fast/stages/0-org-setup/datasets/hardened/README.md @@ -70,12 +70,12 @@ In that case, the controls placed in the `organization/scc-sha-custom-modules` f | `compute.disableGuestAttributesAccess` | Prevent the use of Guest Attributes for Compute Engine instance metadata. | | | `compute.disableInternetNetworkEndpointGroup` | Prevent configuration of internet network endpoint groups. | | | `compute.disableNestedVirtualization` | Prevent the creation of Compute Engine instances with nested virtualization enabled. | | -| `compute.disableSerialPortAccess` | Prevent the enablement of serial port access for VM instances. | **CIS Controls 8.0**: 4.8
**PCI-DSS 4.0**: 1.2.1, 1.4.1
**NIST 800-53 R5**: CM-6, CM-7
**ISO-2700-1 v2022**: A.8.9
**SOC2 v2017**: CC6.6.1, CC6.6.3, CC6.6.4 | +| `compute.disableSerialPortAccess` | Prevent the enablement of serial port access for VM instances. | **CIS for GCP 3.0**: 4.5
**CIS Controls 8.0**: 4.8
**PCI-DSS 4.0**: 1.2.1, 1.4.1
**NIST 800-53 R5**: CM-6, CM-7
**ISO-2700-1 v2022**: A.8.9
**SOC2 v2017**: CC6.6.1, CC6.6.3, CC6.6.4 | | `compute.disableVpcExternalIpv6` | Prevent configuration of subnets with external IPv6 ranges. | | | `compute.managed.blockPreviewFeatures` | Ensures that preview feature updates are blocked unless explicitly allowed | | -| `compute.requireOsLogin` | Enforce the use of OS Login for all Compute Engine instances. | **CIS Controls 8.0**: 5.6, 6.7
**PCI-DSS 4.0**: 1.2.5,
2.2.4
6.4.1
**NIST 800-53 R5**: AC-2
**ISO-2700-1 v2022**: A.5.15
**SOC2 v2017**: CC6.1.4, CC6.1.6, CC6.1.8, CC6.1.9 | -| `compute.requireShieldedVm` | Enforce the use of Shielded VM for all Compute Engine instances. | | -| `compute.requireSslPolicy` | Prevent the use of weak cipher suites and TLS versions on HTTPS and SSL Proxy load balancers. | **NIST 800-53 R4**: SC-7
**ISO-2700-1 v2013**: A.14.1.3 | +| `compute.requireOsLogin` | Enforce the use of OS Login for all Compute Engine instances. | **CIS for GCP 3.0**: 4.4
**CIS Controls 8.0**: 5.6, 6.7
**PCI-DSS 4.0**: 1.2.5,
2.2.4
6.4.1
**NIST 800-53 R5**: AC-2
**ISO-2700-1 v2022**: A.5.15
**SOC2 v2017**: CC6.1.4, CC6.1.6, CC6.1.8, CC6.1.9 | +| `compute.requireShieldedVm` | Enforce the use of Shielded VM for all Compute Engine instances. | **CIS for GCP 3.0**: 4.8 | +| `compute.requireSslPolicy` | Prevent the use of weak cipher suites and TLS versions on HTTPS and SSL Proxy load balancers. | **CIS for GCP 3.0**: 3.9
**NIST 800-53 R4**: SC-7
**ISO-2700-1 v2013**: A.14.1.3 | | `compute.restrictDedicatedInterconnectUsage` | Restrict the use of Dedicated Interconnect. | | | `compute.restrictLoadBalancerCreationForTypes` | Restrict the creation of load balancers based on type. | | | `compute.restrictPartnerInterconnectUsage` | Restrict the use of Partner Interconnect. | | @@ -84,7 +84,7 @@ In that case, the controls placed in the `organization/scc-sha-custom-modules` f | `compute.setNewProjectDefaultToZonalDNSOnly` | Ensure project created use Zonal DNS instead of global. | | | `compute.skipDefaultNetworkCreation` | Prevent the automatic creation of the default VPC network in new projects. | **CIS Controls 8.0**: 4.2
**NIST 800-53 R5**: AC-18, CM-2, CM-6, CM-7, CM-9
**NIST Cybersecurity Framework 1.0**: PR-IP-1
**ISO-2700-1 v2022**: A.8.9
**SOC2 v2017**: CC5.2.2
**Cloud Controls Matrix 4**: ISV-04 | | `compute.trustedImageProjects` | Restrict the use of VM images to an authorized list of projects. | | -| `compute.vmExternalIpAccess` | Prevent Compute Engine instances from having public IP addresses. | **CIS Controls 8.0**: 3.3
**PCI-DSS 4.0**: 1.3.1
**NIST 800-53 R4**: CA-3, SC-7
**NIST 800-53 R5**: AC-3, AC-5, AC-6, MP-2
**NIST Cybersecurity Framework 1.0**: PR-AC-4
**ISO-2700-1 v2022**: A.5.10, A.5.15, A.8.3, A.8.4
**SOC2 v2017**: CC5.2.3, CC6.1.3, CC6.1.7
**HIPAA**: 164.308(a)(3)(i), 164.308(a)(3)(ii), 164.312(a)(1)
**Cloud Controls Matrix 4**: DSP-17 | +| `compute.vmExternalIpAccess` | Prevent Compute Engine instances from having public IP addresses. | **CIS for GCP 3.0**: 4.9
**CIS Controls 8.0**: 3.3
**PCI-DSS 4.0**: 1.3.1
**NIST 800-53 R4**: CA-3, SC-7
**NIST 800-53 R5**: AC-3, AC-5, AC-6, MP-2
**NIST Cybersecurity Framework 1.0**: PR-AC-4
**ISO-2700-1 v2022**: A.5.10, A.5.15, A.8.3, A.8.4
**SOC2 v2017**: CC5.2.3, CC6.1.3, CC6.1.7
**HIPAA**: 164.308(a)(3)(i), 164.308(a)(3)(ii), 164.312(a)(1)
**Cloud Controls Matrix 4**: DSP-17 | | `container.managed.disableABAC` | Require that Attribute-Based Access Control is disabled | | | `container.managed.disableLegacyClientCertificateIssuance` | Prevent the use of legacy authentication methods for GKE API servers. | **CIS for GKE 1.5**: 2.1.1
5.8.1
**PCI-DSS 4.0**: 4.1 | | `container.managed.enableCloudLogging` | Enforce that GKE clusters logging is enabled | **CIS for GKE 1.5**: 5.7.1
**PCI-DSS 4.0**: 10.2 | @@ -93,12 +93,12 @@ In that case, the controls placed in the `organization/scc-sha-custom-modules` f | `container.managed.enableShieldedNodes` | Enforce that GKE nodes is configured with shielded GKE nodes | **CIS for GKE 1.5**: 5.5.5 | | `container.managed.enableWorkloadIdentityFederation` | Enforce that GKE clusters are enabled with Workload Identity | **CIS for GKE 1.5**: 5.2.2
**PCI-DSS 4.0**: 7.2.2 | | `essentialcontacts.allowedContactDomains` | Restrict essential contact domains to an authorized list. | | -| `gcp.restrictTLSCipherSuites` | Prevent the use of weak cipher suites and TLS versions on HTTPS and SSL Proxy load balancers. | **NIST 800-53 R4**: SC-7
**ISO-2700-1 v2013**: A.14.1.3 | -| `gcp.restrictTLSVersion` | Prevent the use of weak cipher suites and TLS versions on HTTPS and SSL Proxy load balancers. | **NIST 800-53 R4**: SC-7
**ISO-2700-1 v2013**: A.14.1.3 | -| `iam.allowedPolicyMemberDomains` | Restrict domain sharing to authorized domains. | **NIST 800-53 R4**: AC-3
**ISO-2700-1 v2013**: A.9.2.3 | -| `iam.automaticIamGrantsForDefaultServiceAccounts` | Prevent the automatic granting of IAM roles to default service accounts. | **PCI-DSS 4.0**: 2.2.2
2.3.1 | +| `gcp.restrictTLSCipherSuites` | Prevent the use of weak cipher suites and TLS versions on HTTPS and SSL Proxy load balancers. | **CIS for GCP 3.0**: 3.9
**NIST 800-53 R4**: SC-7
**ISO-2700-1 v2013**: A.14.1.3 | +| `gcp.restrictTLSVersion` | Prevent the use of weak cipher suites and TLS versions on HTTPS and SSL Proxy load balancers. | **CIS for GCP 3.0**: 3.9
**NIST 800-53 R4**: SC-7
**ISO-2700-1 v2013**: A.14.1.3 | +| `iam.allowedPolicyMemberDomains` | Restrict domain sharing to authorized domains. | **CIS for GCP 3.0**: 1.1
**NIST 800-53 R4**: AC-3
**ISO-2700-1 v2013**: A.9.2.3 | +| `iam.automaticIamGrantsForDefaultServiceAccounts` | Prevent the automatic granting of IAM roles to default service accounts. | **CIS for GCP 3.0**: 4.1
**PCI-DSS 4.0**: 2.2.2
2.3.1 | | `iam.disableAuditLoggingExemption` | Prevent the use of audit logging exemptions. Detect also if audit logging are has been disabled. | | -| `iam.disableServiceAccountKeyCreation` | Enforce the use of only GCP-managed service account keys. | | +| `iam.disableServiceAccountKeyCreation` | Enforce the use of only GCP-managed service account keys. | **CIS for GCP 3.0**: 1.4 | | `iam.disableServiceAccountKeyUpload` | Prevent the uploading of service account keys. | | | `iam.managed.disableServiceAccountApiKeyCreation` | Prevent the creation of service account API key bindings. | | | `iam.serviceAccountKeyExposureResponse` | Enforce Google to disable the service keys if a service account linked key is detected to be exposed publicly. | | @@ -109,11 +109,11 @@ In that case, the controls placed in the `organization/scc-sha-custom-modules` f | `run.allowedVPCEgress` | Ensure all traffic from Cloud Run services and jobs is routed through a VPC connector. | | | `run.managed.requireInvokerIam` | Enforce an IAM invoker check for Cloud Run services. | | | `sql.restrictAuthorizedNetworks` | Prevent the ability to add Authorized Networks for unproxied database access to Cloud SQL instances. | | -| `sql.restrictPublicIp` | Ensure That Cloud SQL Database Instances Do Not Have Public IPs | **CIS Controls 8.0**: 3.3, 4.6
**PCI-DSS 4.0**: 1.3.1
**NIST 800-53 R5**: AC-3, AC-5, AC-6, MA-4, MP-2
**NIST Cybersecurity Framework 1.0**: PR-AC-4
**ISO-2700-1 v2022**: A.5.10, A.5.15, A.8.3, A.8.4
**SOC2 v2017**: CC5.2.2, CC5.2.3, CC6.1.3, CC6.1.7
**HIPAA**: 164.308(a)(3)(i), 164.308(a)(3)(ii), 164.312(a)(1)
**Cloud Controls Matrix 4**: DSP-17 | -| `storage.publicAccessPrevention` | Prevent anonymous or public access to Cloud Storage buckets. | **CIS Controls 8.0**: 3.3
**PCI-DSS 4.0**: 1.3.1
**NIST 800-53 R4**: AC-2
**NIST 800-53 R5**: AC-3, AC-5, AC-6, MP-2
**NIST Cybersecurity Framework 1.0**: PR-AC-4
**ISO-2700-1 v2013**: A.14.1.3, A.8.2.3
**ISO-2700-1 v2022**: A.5.10, A.5.15, A.8.3, A.8.4
**SOC2 v2017**: CC5.2.3, CC6.1.3, CC6.1.7
**HIPAA**: 164.308(a)(3)(i), 164.308(a)(3)(ii), 164.312(a)(1)
**Cloud Controls Matrix 4**: DSP-17 | +| `sql.restrictPublicIp` | Ensure That Cloud SQL Database Instances Do Not Have Public IPs | **CIS for GCP 3.0**: 6.6
**CIS Controls 8.0**: 3.3, 4.6
**PCI-DSS 4.0**: 1.3.1
**NIST 800-53 R5**: AC-3, AC-5, AC-6, MA-4, MP-2
**NIST Cybersecurity Framework 1.0**: PR-AC-4
**ISO-2700-1 v2022**: A.5.10, A.5.15, A.8.3, A.8.4
**SOC2 v2017**: CC5.2.2, CC5.2.3, CC6.1.3, CC6.1.7
**HIPAA**: 164.308(a)(3)(i), 164.308(a)(3)(ii), 164.312(a)(1)
**Cloud Controls Matrix 4**: DSP-17 | +| `storage.publicAccessPrevention` | Prevent anonymous or public access to Cloud Storage buckets. | **CIS for GCP 3.0**: 5.1
**CIS Controls 8.0**: 3.3
**PCI-DSS 4.0**: 1.3.1
**NIST 800-53 R4**: AC-2
**NIST 800-53 R5**: AC-3, AC-5, AC-6, MP-2
**NIST Cybersecurity Framework 1.0**: PR-AC-4
**ISO-2700-1 v2013**: A.14.1.3, A.8.2.3
**ISO-2700-1 v2022**: A.5.10, A.5.15, A.8.3, A.8.4
**SOC2 v2017**: CC5.2.3, CC6.1.3, CC6.1.7
**HIPAA**: 164.308(a)(3)(i), 164.308(a)(3)(ii), 164.312(a)(1)
**Cloud Controls Matrix 4**: DSP-17 | | `storage.restrictAuthTypes` | Restrict authentication types for Cloud Storage. | | | `storage.secureHttpTransport` | Restrict unencrypted HTTP access to Cloud Storage. | | -| `storage.uniformBucketLevelAccess` | Enforce the enablement of uniform bucket-level access to Cloud Storage buckets. | **CIS Controls 8.0**: 3.3
**PCI-DSS 4.0**: 1.3.1
**NIST 800-53 R5**: AC-3, AC-5, AC-6, MP-2
**NIST Cybersecurity Framework 1.0**: PR-AC-4
**ISO-2700-1 v2022**: A.5.10, A.5.15, A.8.3, A.8.4
**SOC2 v2017**: CC5.2.3, CC6.1.3, CC6.1.7
**HIPAA**: 164.308(a)(3)(i), 164.308(a)(3)(ii), 164.312(a)(1)
**Cloud Controls Matrix 4**: DSP-17 | +| `storage.uniformBucketLevelAccess` | Enforce the enablement of uniform bucket-level access to Cloud Storage buckets. | **CIS for GCP 3.0**: 5.2
**CIS Controls 8.0**: 3.3
**PCI-DSS 4.0**: 1.3.1
**NIST 800-53 R5**: AC-3, AC-5, AC-6, MP-2
**NIST Cybersecurity Framework 1.0**: PR-AC-4
**ISO-2700-1 v2022**: A.5.10, A.5.15, A.8.3, A.8.4
**SOC2 v2017**: CC5.2.3, CC6.1.3, CC6.1.7
**HIPAA**: 164.308(a)(3)(i), 164.308(a)(3)(ii), 164.312(a)(1)
**Cloud Controls Matrix 4**: DSP-17 | **Note:** For organizations with strict requirements to ensure all resources are created and stored within specific geographic regions (e.g., for data sovereignty or regulatory compliance), the `gcp.resourceLocations` Organization Policy present in file [gcp.yaml](organization/org-policies/gcp.yaml) can be enabled. @@ -122,22 +122,22 @@ In that case, the controls placed in the `organization/scc-sha-custom-modules` f | Constraint | Description | Compliance Mapping | |---|---|---| | `accesscontextmanagerDisableBridgePerimeters` | Ensure no perimeter bridges are used. Instead, use ingress and egress rules. | | -| `cloudrunDisableEnvironmentVariablePattern` | Prevent secrets from being stored in Cloud Run environment variables. | | -| `cloudsqlDisablePublicAuthorizedNetworks` | Ensure That Cloud SQL Database Instances Do Not Implicitly Whitelist All Public IP Addresses | **CIS Controls 8.0**: 3.3
**PCI-DSS 4.0**: 1.3.1
**NIST 800-53 R4**: CA-3, SC-7
**NIST 800-53 R5**: AC-3, AC-5, AC-6, MP-2
**NIST Cybersecurity Framework 1.0**: PR-AC-4
**ISO-2700-1 v2013**: A.13.1.3, A.14.1.3, A.8.2.3
**ISO-2700-1 v2022**: A.5.10, A.5.15, A.8.3, A.8.4
**SOC2 v2017**: CC5.2.3, CC6.1.3, CC6.1.7
**HIPAA**: 164.308(a)(3)(i), 164.308(a)(3)(ii), 164.312(a)(1)
**Cloud Controls Matrix 4**: DSP-17 | +| `cloudrunDisableEnvironmentVariablePattern` | Prevent secrets from being stored in Cloud Run environment variables. | **CIS for GCP 3.0**: 1.17 | +| `cloudsqlDisablePublicAuthorizedNetworks` | Ensure That Cloud SQL Database Instances Do Not Implicitly Whitelist All Public IP Addresses | **CIS for GCP 3.0**: 6.5
**CIS Controls 8.0**: 3.3
**PCI-DSS 4.0**: 1.3.1
**NIST 800-53 R4**: CA-3, SC-7
**NIST 800-53 R5**: AC-3, AC-5, AC-6, MP-2
**NIST Cybersecurity Framework 1.0**: PR-AC-4
**ISO-2700-1 v2013**: A.13.1.3, A.14.1.3, A.8.2.3
**ISO-2700-1 v2022**: A.5.10, A.5.15, A.8.3, A.8.4
**SOC2 v2017**: CC5.2.3, CC6.1.3, CC6.1.7
**HIPAA**: 164.308(a)(3)(i), 164.308(a)(3)(ii), 164.312(a)(1)
**Cloud Controls Matrix 4**: DSP-17 | | `cloudsqlEnforcePasswordComplexity` | Enforce password complexity for Cloud SQL instance users. | | -| `cloudsqlRequireAutomatedBackup` | Ensure That Cloud SQL Database Instances Are Configured With Automated Backups | **CIS Controls 8.0**: 11.2
**NIST 800-53 R4**: CP-9
**NIST 800-53 R5**: CP-10, CP-9
**NIST Cybersecurity Framework 1.0**: PR-IP-4
**ISO-2700-1 v2013**: A.12.3.1
**ISO-2700-1 v2022**: A.8.13
**HIPAA**: 164.308(a)(7)(ii) | -| `cloudsqlRequireMySQLDatabaseFlags` | Ensure ‘Skip_show_database’ Database Flag for Cloud SQL MySQL Instance Is Set to ‘On’ | **CIS Controls 8.0**: 3.3
**PCI-DSS 4.0**: 1.3.1
**NIST 800-53 R5**: AC-3, AC-5, AC-6, MP-2
**NIST Cybersecurity Framework 1.0**: PR-AC-4
**ISO-2700-1 v2022**: A.5.10, A.5.15, A.8.3, A.8.4
**SOC2 v2017**: CC5.2.3, CC6.1.3, CC6.1.7
**HIPAA**: 164.308(a)(3)(i), 164.308(a)(3)(ii), 164.312(a)(1)
**Cloud Controls Matrix 4**: DSP-17 | +| `cloudsqlRequireAutomatedBackup` | Ensure That Cloud SQL Database Instances Are Configured With Automated Backups | **CIS for GCP 3.0**: 6.7
**CIS Controls 8.0**: 11.2
**NIST 800-53 R4**: CP-9
**NIST 800-53 R5**: CP-10, CP-9
**NIST Cybersecurity Framework 1.0**: PR-IP-4
**ISO-2700-1 v2013**: A.12.3.1
**ISO-2700-1 v2022**: A.8.13
**HIPAA**: 164.308(a)(7)(ii) | +| `cloudsqlRequireMySQLDatabaseFlags` | Ensure ‘Skip_show_database’ Database Flag for Cloud SQL MySQL Instance Is Set to ‘On’ | **CIS for GCP 3.0**: 6.1.2
**CIS Controls 8.0**: 3.3
**PCI-DSS 4.0**: 1.3.1
**NIST 800-53 R5**: AC-3, AC-5, AC-6, MP-2
**NIST Cybersecurity Framework 1.0**: PR-AC-4
**ISO-2700-1 v2022**: A.5.10, A.5.15, A.8.3, A.8.4
**SOC2 v2017**: CC5.2.3, CC6.1.3, CC6.1.7
**HIPAA**: 164.308(a)(3)(i), 164.308(a)(3)(ii), 164.312(a)(1)
**Cloud Controls Matrix 4**: DSP-17 | | `cloudsqlRequirePointInTimeRecovery` | Enforce point-in-time recovery for all Cloud SQL backup configurations. | | -| `cloudsqlRequirePostgreSQLDatabaseFlags` | Ensure That the ‘Log_connections’ Database Flag for Cloud SQL PostgreSQL Instance Is Set to ‘On’ | **CIS Controls 8.0**: 8.5
**PCI-DSS 4.0**: 10.2.1, 10.2.1.2, 10.2.1.5, 9.4.5
**NIST 800-53 R5**: AU-12, AU-3, AU-7
**NIST Cybersecurity Framework 1.0**: DE-AE-3, DE-CM-1
**ISO-2700-1 v2022**: A.5.28, A.8.15
**SOC2 v2017**: CC5.2.3, CC7.2.1, CC7.2.2, CC7.2.3
**Cloud Controls Matrix 4**: DSP-17 | -| `cloudsqlRequireRootPassword` | Ensure That a MySQL Database Instance Does Not Allow Anyone To Connect With Administrative Privileges | **NIST 800-53 R4**: AC-3
**ISO-2700-1 v2013**: A.8.2.3, A.9.4.2
**ISO-2700-1 v2022**: A.8.5 | -| `cloudsqlRequireSQLServerDatabaseFlags` | Ensure 'external scripts enabled' database flag for Cloud SQL SQL Server instance is set to 'off' | **CIS Controls 8.0**: 2.7
**PCI-DSS 4.0**: 1.2.5, 2.2.4, 6.4.3
**NIST 800-53 R5**: CM-7, SI-7
**NIST Cybersecurity Framework 1.0**: PR-IP-1, PR-PT-3
**SOC2 v2017**: CC5.2.1, CC5.2.2, CC5.2.3, CC5.2.4 | -| `cloudsqlRequireSSLConnection` | Ensure That the Cloud SQL Database Instance Requires All Incoming Connections To Use SSL | **NIST 800-53 R4**: SC-7
**ISO-2700-1 v2013**: A.13.2.1, A.14.1.3, A.8.2.3 | -| `dnsAllowedSigningAlgorithms` | Prevent the use of the RSASHA1 algorithm for the Key-Signing Key in Cloud DNS DNSSEC. | **PCI-DSS 4.0**: 1.1.1, 1.2.1, 1.2.6, 1.2.7, 1.4.2, 1.5.1, 2.1.1, 2.2.1
**NIST 800-53 R4**: 4.2
**NIST 800-53 R5**: AC-18, CM-2, CM-6, CM-7, CM-9
**NIST Cybersecurity Framework 1.0**: PR-IP-1
**ISO-2700-1 v2022**: A.8.9
**SOC2 v2017**: CC5.2.2
**Cloud Controls Matrix 4**: IVS-04 | -| `dnsRequireManageZoneDNSSEC` | Enforce the enablement of DNSSEC for all Cloud DNS zones. | **CIS Controls 8.0**: 4.2
**PCI-DSS 4.0**: 1.1.1, 1.2.1, 1.2.6, 1.2.7, 1.4.2, 1.5.1, 2.1.1, 2.2.1
**NIST 800-53 R5**: AC-18, CM-2, CM-6, CM-7, CM-9
**NIST Cybersecurity Framework 1.0**: PR-IP-1
**ISO-2700-1 v2013**: A.8.2.3
**ISO-2700-1 v2022**: A.8.9
**SOC2 v2017**: CC5.2.2
**Cloud Controls Matrix 4**: ISV-04 | -| `dnsRequirePolicyLogging` | Enforce the enablement of Cloud DNS logging for all VPC networks. | **PCI-DSS 4.0**: 10.4.1, 10.4.1.1, 10.4.2, 10.4.3
**NIST 800-53 R5**: AU-6, AU-7
**NIST Cybersecurity Framework 1.0**: DE-AE-2, PR-PT-1, RS-AN-1
**ISO-2700-1 v2022**: A.5.25
**SOC2 v2017**: CC4.1.1, CC4.1.2, CC4.1.3, CC4.1.4, CC4.1.5, CC4.1.6, CC4.1.7, CC4.1.8, CC7.3.1, CC7.3.2, CC7.3.3, CC7.3.4, CC7.3.5
**HIPAA**: 164.308(a)(1)(ii), 164.312(b)
**Cloud Controls Matrix 4**: LOG-05 | +| `cloudsqlRequirePostgreSQLDatabaseFlags` | Ensure That the ‘Log_connections’ Database Flag for Cloud SQL PostgreSQL Instance Is Set to ‘On’ | **CIS for GCP 3.0**: 6.2.2
**CIS Controls 8.0**: 8.5
**PCI-DSS 4.0**: 10.2.1, 10.2.1.2, 10.2.1.5, 9.4.5
**NIST 800-53 R5**: AU-12, AU-3, AU-7
**NIST Cybersecurity Framework 1.0**: DE-AE-3, DE-CM-1
**ISO-2700-1 v2022**: A.5.28, A.8.15
**SOC2 v2017**: CC5.2.3, CC7.2.1, CC7.2.2, CC7.2.3
**Cloud Controls Matrix 4**: DSP-17 | +| `cloudsqlRequireRootPassword` | Ensure That a MySQL Database Instance Does Not Allow Anyone To Connect With Administrative Privileges | **CIS for GCP 3.0**: 6.1.1
**NIST 800-53 R4**: AC-3
**ISO-2700-1 v2013**: A.8.2.3, A.9.4.2
**ISO-2700-1 v2022**: A.8.5 | +| `cloudsqlRequireSQLServerDatabaseFlags` | Ensure 'external scripts enabled' database flag for Cloud SQL SQL Server instance is set to 'off' | **CIS for GCP 3.0**: 6.3.1
**CIS Controls 8.0**: 2.7
**PCI-DSS 4.0**: 1.2.5, 2.2.4, 6.4.3
**NIST 800-53 R5**: CM-7, SI-7
**NIST Cybersecurity Framework 1.0**: PR-IP-1, PR-PT-3
**SOC2 v2017**: CC5.2.1, CC5.2.2, CC5.2.3, CC5.2.4 | +| `cloudsqlRequireSSLConnection` | Ensure That the Cloud SQL Database Instance Requires All Incoming Connections To Use SSL | **CIS for GCP 3.0**: 6.4
**NIST 800-53 R4**: SC-7
**ISO-2700-1 v2013**: A.13.2.1, A.14.1.3, A.8.2.3 | +| `dnsAllowedSigningAlgorithms` | Prevent the use of the RSASHA1 algorithm for the Key-Signing Key in Cloud DNS DNSSEC. | **CIS for GCP 3.0**: 3.4
**PCI-DSS 4.0**: 1.1.1, 1.2.1, 1.2.6, 1.2.7, 1.4.2, 1.5.1, 2.1.1, 2.2.1
**NIST 800-53 R4**: 4.2
**NIST 800-53 R5**: AC-18, CM-2, CM-6, CM-7, CM-9
**NIST Cybersecurity Framework 1.0**: PR-IP-1
**ISO-2700-1 v2022**: A.8.9
**SOC2 v2017**: CC5.2.2
**Cloud Controls Matrix 4**: IVS-04 | +| `dnsRequireManageZoneDNSSEC` | Enforce the enablement of DNSSEC for all Cloud DNS zones. | **CIS for GCP 3.0**: 3.3
**CIS Controls 8.0**: 4.2
**PCI-DSS 4.0**: 1.1.1, 1.2.1, 1.2.6, 1.2.7, 1.4.2, 1.5.1, 2.1.1, 2.2.1
**NIST 800-53 R5**: AC-18, CM-2, CM-6, CM-7, CM-9
**NIST Cybersecurity Framework 1.0**: PR-IP-1
**ISO-2700-1 v2013**: A.8.2.3
**ISO-2700-1 v2022**: A.8.9
**SOC2 v2017**: CC5.2.2
**Cloud Controls Matrix 4**: ISV-04 | +| `dnsRequirePolicyLogging` | Enforce the enablement of Cloud DNS logging for all VPC networks. | **CIS for GCP 3.0**: 2.12
**PCI-DSS 4.0**: 10.4.1, 10.4.1.1, 10.4.2, 10.4.3
**NIST 800-53 R5**: AU-6, AU-7
**NIST Cybersecurity Framework 1.0**: DE-AE-2, PR-PT-1, RS-AN-1
**ISO-2700-1 v2022**: A.5.25
**SOC2 v2017**: CC4.1.1, CC4.1.2, CC4.1.3, CC4.1.4, CC4.1.5, CC4.1.6, CC4.1.7, CC4.1.8, CC7.3.1, CC7.3.2, CC7.3.3, CC7.3.4, CC7.3.5
**HIPAA**: 164.308(a)(1)(ii), 164.312(b)
**Cloud Controls Matrix 4**: LOG-05 | | `firewallRestrictOpenWorldRule` | Prevent the creation of VPC firewall rules with a source or destination of `0.0.0.0/0`. | | -| `firewallRestrictRdpPolicyRule` | Prevent RDP access from the internet via firewall policies. | **CIS Controls 8.0**: 4.4, 4.5
**PCI-DSS 4.0**: 1.2.1, 1.4.1
**NIST 800-53 R4**: SC-7
**NIST 800-53 R5**: CA-9, SC-7
**ISO-2700-1 v2013**: A.13.1.1
**SOC2 v2017**: CC6.6.1, CC6.6.4 | -| `firewallRestrictSshPolicyRule` | Prevent SSH access from the internet via firewall policies. | **CIS Controls 8.0**: 4.4, 4.5
**PCI-DSS 4.0**: 1.2.1, 1.4.1
**NIST 800-53 R4**: SC-7
**NIST 800-53 R5**: CA-9, SC-7
**ISO-2700-1 v2013**: A.13.1.1
**SOC2 v2017**: CC6.6.1, CC6.6.4 | +| `firewallRestrictRdpPolicyRule` | Prevent RDP access from the internet via firewall policies. | **CIS for GCP 3.0**: 3.7
**CIS Controls 8.0**: 4.4, 4.5
**PCI-DSS 4.0**: 1.2.1, 1.4.1
**NIST 800-53 R4**: SC-7
**NIST 800-53 R5**: CA-9, SC-7
**ISO-2700-1 v2013**: A.13.1.1
**SOC2 v2017**: CC6.6.1, CC6.6.4 | +| `firewallRestrictSshPolicyRule` | Prevent SSH access from the internet via firewall policies. | **CIS for GCP 3.0**: 3.6
**CIS Controls 8.0**: 4.4, 4.5
**PCI-DSS 4.0**: 1.2.1, 1.4.1
**NIST 800-53 R4**: SC-7
**NIST 800-53 R5**: CA-9, SC-7
**ISO-2700-1 v2013**: A.13.1.1
**SOC2 v2017**: CC6.6.1, CC6.6.4 | | `gkeAllowedNodePoolImages` | Enforce that GKE nodes are using authorized node images | **CIS for GKE 1.5**: 5.5.1
**PCI-DSS 4.0**: 2.2.6, 5.2, 6.2.1 | | `gkeAllowedReleaseChannels` | Enforce that GKE cluster are using authorized release channels | **CIS for GKE 1.5**: 5.5.4 | | `gkeDisableAlphaCluster` | Prevent the use of alpha features for GKE clusters in production workloads. | **CIS for GKE 1.5**: 5.10.2 | @@ -154,10 +154,10 @@ In that case, the controls placed in the `organization/scc-sha-custom-modules` f | `gkeRequireRegionalClusters` | Enforce the creation of regional GKE clusters | | | `gkeRequireSecureBoot` | Enforce that GKE nodes are configured with secure boot enabled | **CIS for GKE 1.5**: 5.5.7 | | `gkeRequireVPCNativeCluster` | Enforce that GKE clusters are created with VPC-native | **CIS for GKE 1.5**: 5.6.2
**PCI-DSS 4.0**: 1.4.3 | -| `iamDisablePublicBindings` | Ensure That BigQuery Datasets Are Not Anonymously or Publicly Accessible | **CIS Controls 8.0**: 3.3
**PCI-DSS 4.0**: 1.3.1
**NIST 800-53 R4**: AC-2
**NIST 800-53 R5**: AC-3, AC-5, AC-6, MP-2
**NIST Cybersecurity Framework 1.0**: PR-AC-4
**ISO-2700-1 v2013**: A.14.1.3, A.8.2.3
**ISO-2700-1 v2022**: A.5.10, A.5.15, A.8.3, A.8.4
**SOC2 v2017**: CC5.2.3, CC6.1.3, CC6.1.7
**HIPAA**: 164.308(a)(3)(i), 164.308(a)(3)(ii), 164.312(a)(1)
**Cloud Controls Matrix 4**: DSP-17 | -| `networkDisableTargetHTTPProxy` | Prevent the use of weak SSL policies on HTTPS and SSL Proxy load balancers. | | -| `networkDisableWeakSSLPolicy` | Prevent the use of weak SSL policies on HTTPS and SSL Proxy load balancers. | | -| `networkRequireCustomModeVpc` | Ensure That the Default Network Does Not Exist in a Project | **CIS for GKE 1.5**: 3.1
**CIS Controls 8.0**: 4.2
**PCI-DSS 4.0**: 1.1.1, 1.2.1, 1.2.6, 1.2.7, 1.4.2, 1.5.1, 2.1.1, 2.2.1
**NIST 800-53 R5**: AC-18, CM-2, CM-6, CM-7, CM-9
**NIST Cybersecurity Framework 1.0**: PR-IP-1
**ISO-2700-1 v2022**: A.8.9
**SOC2 v2017**: CC5.2.2
**Cloud Controls Matrix 4**: ISV-04 | +| `iamDisablePublicBindings` | Ensure That BigQuery Datasets Are Not Anonymously or Publicly Accessible | **CIS for GCP 3.0**: 7.1
**CIS Controls 8.0**: 3.3
**PCI-DSS 4.0**: 1.3.1
**NIST 800-53 R4**: AC-2
**NIST 800-53 R5**: AC-3, AC-5, AC-6, MP-2
**NIST Cybersecurity Framework 1.0**: PR-AC-4
**ISO-2700-1 v2013**: A.14.1.3, A.8.2.3
**ISO-2700-1 v2022**: A.5.10, A.5.15, A.8.3, A.8.4
**SOC2 v2017**: CC5.2.3, CC6.1.3, CC6.1.7
**HIPAA**: 164.308(a)(3)(i), 164.308(a)(3)(ii), 164.312(a)(1)
**Cloud Controls Matrix 4**: DSP-17 | +| `networkDisableTargetHTTPProxy` | Prevent the use of weak SSL policies on HTTPS and SSL Proxy load balancers. | **CIS for GCP 3.0**: 3.9 | +| `networkDisableWeakSSLPolicy` | Prevent the use of weak SSL policies on HTTPS and SSL Proxy load balancers. | **CIS for GCP 3.0**: 3.9 | +| `networkRequireCustomModeVpc` | Ensure That the Default Network Does Not Exist in a Project | **CIS for GCP 3.0**: 3.1
**CIS Controls 8.0**: 4.2
**PCI-DSS 4.0**: 1.1.1, 1.2.1, 1.2.6, 1.2.7, 1.4.2, 1.5.1, 2.1.1, 2.2.1
**NIST 800-53 R5**: AC-18, CM-2, CM-6, CM-7, CM-9
**NIST Cybersecurity Framework 1.0**: PR-IP-1
**ISO-2700-1 v2022**: A.8.9
**SOC2 v2017**: CC5.2.2
**Cloud Controls Matrix 4**: ISV-04 | | `networkRequireSubnetPrivateGoogleAccess` | Enforce Private Google Access for all VPC network subnets. | | ### Detective Controls @@ -189,9 +189,9 @@ SCC Custom SHA Detectors are available only for organization have subscribed to | Alert | Description | Compliance Mapping | |---|---|---| -| `auditConfigChanges` | Ensure log metric filters and alerts exist for audit configuration changes. | **PCI-DSS 4.0**: 10.2.1, 10.2.1.1, 10.2.1.2, 10.2.1.3, 10.2.1.4, 10.2.1.5, 10.2.1.6, 10.2.1.7, 10.2.2, 5.3.4, 6.4.1, 6.4.2 | -| `customRoleChanges` | Ensure log metric filters and alerts exist for custom role changes. | **PCI-DSS 4.0**: 10.2.1, 10.2.1.1, 10.2.1.2, 10.2.1.3, 10.2.1.4, 10.2.1.5, 10.2.1.6, 10.2.1.7, 10.2.2, 5.3.4, 6.4.1, 6.4.2 | -| `projectOwnershipChange` | Ensure log metric filters and alerts exist for project ownership changes. | **PCI-DSS 4.0**: 10.2.1, 10.2.1.1, 10.2.1.2, 10.2.1.3, 10.2.1.4, 10.2.1.5, 10.2.1.6, 10.2.1.7, 10.2.2, 5.3.4, 6.4.1, 6.4.2 | +| `auditConfigChanges` | Ensure log metric filters and alerts exist for audit configuration changes. | **CIS for GCP 3.0**: 2.5
**PCI-DSS 4.0**: 10.2.1, 10.2.1.1, 10.2.1.2, 10.2.1.3, 10.2.1.4, 10.2.1.5, 10.2.1.6, 10.2.1.7, 10.2.2, 5.3.4, 6.4.1, 6.4.2 | +| `customRoleChanges` | Ensure log metric filters and alerts exist for custom role changes. | **CIS for GCP 3.0**: 2.6
**PCI-DSS 4.0**: 10.2.1, 10.2.1.1, 10.2.1.2, 10.2.1.3, 10.2.1.4, 10.2.1.5, 10.2.1.6, 10.2.1.7, 10.2.2, 5.3.4, 6.4.1, 6.4.2 | +| `projectOwnershipChange` | Ensure log metric filters and alerts exist for project ownership changes. | **CIS for GCP 3.0**: 2.4
**PCI-DSS 4.0**: 10.2.1, 10.2.1.1, 10.2.1.2, 10.2.1.3, 10.2.1.4, 10.2.1.5, 10.2.1.6, 10.2.1.7, 10.2.2, 5.3.4, 6.4.1, 6.4.2 | ## Troubleshooting diff --git a/fast/stages/0-org-setup/output-files.tf b/fast/stages/0-org-setup/output-files.tf index 1a3513dcc..bb0cb1207 100644 --- a/fast/stages/0-org-setup/output-files.tf +++ b/fast/stages/0-org-setup/output-files.tf @@ -84,7 +84,7 @@ locals { folder_ids = local.of_ctx.folder_ids iam_principals = local.of_ctx.iam_principals logging = { - writer_identities = module.organization-iam.0.sink_writer_identities + writer_identities = module.organization-iam[0].sink_writer_identities project_number = module.factory.project_numbers["log-0"] } project_ids = local.of_ctx.project_ids, diff --git a/modules/agent-engine/README.md b/modules/agent-engine/README.md index 4996f6234..f886a4972 100644 --- a/modules/agent-engine/README.md +++ b/modules/agent-engine/README.md @@ -190,7 +190,7 @@ module "agent_engine" { ## Define environment variables and use secrets -You can define environment variables and load existing secrets as environment variables into your agent. +You can define environment variables and load existing secrets as environment variables into your agent. ```hcl module "agent_engine" { @@ -236,8 +236,8 @@ The module allows you to dynamically reference context values for resources crea | [description](variables.tf#L57) | The Agent Engine description. | string | | "Terraform managed." | | [encryption_key](variables.tf#L64) | The full resource name of the Cloud KMS CryptoKey. | string | | null | | [generate_pickle](variables.tf#L70) | Generate the pickle file from a source file. | bool | | true | -| [service_account_config](variables.tf#L95) | Service account configurations. | object({…}) | | {} | -| [source_files](variables.tf#L112) | The to source files path and names. | object({…}) | | {} | +| [service_account_config](variables-serviceaccount.tf#L18) | Service account configurations. | object({…}) | | {} | +| [source_files](variables.tf#L95) | The to source files path and names. | object({…}) | | {} | ## Outputs diff --git a/modules/agent-engine/main.tf b/modules/agent-engine/main.tf index 5c94bd339..a50c42c27 100644 --- a/modules/agent-engine/main.tf +++ b/modules/agent-engine/main.tf @@ -16,15 +16,6 @@ locals { _ctx_p = "$" - _service_account_external_email = ( - var.service_account_config.email == null - ? null - : lookup( - local.ctx.iam_principals, - var.service_account_config.email, - var.service_account_config.email - ) - ) bucket_name = ( var.bucket_config.create ? google_storage_bucket.default[0].name @@ -41,15 +32,6 @@ locals { project_id = lookup( local.ctx.project_ids, var.project_id, var.project_id ) - service_account_email = ( - var.service_account_config.create - ? google_service_account.default[0].email - : local._service_account_external_email - ) - service_account_roles = [ - for role in var.service_account_config.roles - : lookup(local.ctx.custom_roles, role, role) - ] } resource "google_vertex_ai_reasoning_engine" "default" { @@ -192,21 +174,3 @@ resource "google_storage_bucket_object" "requirements" { source = "${var.source_files.path}/${var.source_files.requirements}" source_md5hash = filemd5("${var.source_files.path}/${var.source_files.requirements}") } - -resource "google_service_account" "default" { - count = var.service_account_config.create ? 1 : 0 - account_id = coalesce(var.service_account_config.name, var.name) - project = local.project_id - display_name = "Agent Engine ${coalesce(var.service_account_config.name, var.name)}." -} - -resource "google_project_iam_member" "default" { - for_each = ( - var.service_account_config.create - ? toset(local.service_account_roles) - : toset([]) - ) - role = each.key - project = local.project_id - member = google_service_account.default[0].member -} diff --git a/modules/agent-engine/outputs.tf b/modules/agent-engine/outputs.tf index 8bce46940..0b418d1c1 100644 --- a/modules/agent-engine/outputs.tf +++ b/modules/agent-engine/outputs.tf @@ -21,5 +21,5 @@ output "id" { output "service_account" { description = "Service account resource." - value = try(google_service_account.default[0], null) + value = try(google_service_account.service_account[0], null) } diff --git a/modules/agent-engine/serviceaccount.tf b/modules/agent-engine/serviceaccount.tf new file mode 100644 index 000000000..46c7499e4 --- /dev/null +++ b/modules/agent-engine/serviceaccount.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 { + service_account_email = ( + var.service_account_config.create + ? google_service_account.service_account[0].email # use managed SA, when creating + : (var.service_account_config.email == null ? null # set to null, if no email provided + : lookup( # lookup SA in context + local.ctx.iam_principals, + var.service_account_config.email, + var.service_account_config.email + ) + ) + ) + service_account_roles = [ + for role in var.service_account_config.roles + : lookup(local.ctx.custom_roles, role, role) + ] +} + +resource "google_service_account" "service_account" { + count = var.service_account_config.create ? 1 : 0 + project = local.project_id + account_id = coalesce(var.service_account_config.name, var.name) + display_name = coalesce( + var.service_account_config.display_name, + var.service_account_config.name, + var.name + ) +} + +resource "google_project_iam_member" "default" { + for_each = ( + var.service_account_config.create + ? toset(local.service_account_roles) + : toset([]) + ) + role = each.key + project = local.project_id + member = google_service_account.service_account[0].member +} diff --git a/modules/agent-engine/variables-serviceaccount.tf b/modules/agent-engine/variables-serviceaccount.tf new file mode 100644 index 000000000..5f4166c69 --- /dev/null +++ b/modules/agent-engine/variables-serviceaccount.tf @@ -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. + */ + +# this differs from serverless, as it has different roles assigned by default +variable "service_account_config" { + description = "Service account configurations." + type = object({ + create = optional(bool, true) + display_name = optional(string) + email = optional(string) + name = optional(string) + roles = optional(list(string), [ + "roles/aiplatform.user", + "roles/storage.objectViewer", + # TODO: remove when b/441480710 is solved + "roles/viewer" + ]) + }) + nullable = false + default = {} +} diff --git a/modules/agent-engine/variables.tf b/modules/agent-engine/variables.tf index 1098ab871..c40ae635c 100644 --- a/modules/agent-engine/variables.tf +++ b/modules/agent-engine/variables.tf @@ -92,23 +92,6 @@ variable "region" { nullable = false } -variable "service_account_config" { - description = "Service account configurations." - type = object({ - create = optional(bool, true) - email = optional(string) - name = optional(string) - roles = optional(list(string), [ - "roles/aiplatform.user", - "roles/storage.objectViewer", - # TODO: remove when b/441480710 is solved - "roles/viewer" - ]) - }) - nullable = false - default = {} -} - variable "source_files" { description = "The to source files path and names." type = object({ diff --git a/modules/cloud-function-v1/README.md b/modules/cloud-function-v1/README.md index 158522750..d0897592f 100644 --- a/modules/cloud-function-v1/README.md +++ b/modules/cloud-function-v1/README.md @@ -500,8 +500,8 @@ module "cf_http" { | [secrets](variables.tf#L194) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) | | {} | | [service_account_config](variables-serviceaccount.tf#L17) | Service account configurations. | object({…}) | | {} | | [trigger_config](variables.tf#L206) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | -| [vpc_connector](variables-vpcconnector.tf#L17) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | {} | -| [vpc_connector_create](variables-vpcconnector.tf#L28) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | +| [vpc_connector](variables.tf#L216) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | {} | +| [vpc_connector_create](variables-vpcconnector.tf#L17) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | ## Outputs diff --git a/modules/cloud-function-v1/main.tf b/modules/cloud-function-v1/main.tf index 4c6cc0f07..4df938b00 100644 --- a/modules/cloud-function-v1/main.tf +++ b/modules/cloud-function-v1/main.tf @@ -33,7 +33,7 @@ locals { location = lookup(local.ctx.locations, var.region, var.region) prefix = var.prefix == null ? "" : "${var.prefix}-" project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id) - vpc_connector = var.vpc_connector.create ? google_vpc_access_connector.connector[0].id : var.vpc_connector.name + vpc_connector = var.vpc_connector_create != null ? google_vpc_access_connector.connector[0].id : var.vpc_connector.name } resource "google_cloudfunctions_function" "function" { diff --git a/modules/cloud-function-v1/serviceaccount.tf b/modules/cloud-function-v1/serviceaccount.tf index f1fdec866..46c7499e4 100644 --- a/modules/cloud-function-v1/serviceaccount.tf +++ b/modules/cloud-function-v1/serviceaccount.tf @@ -17,11 +17,13 @@ locals { service_account_email = ( var.service_account_config.create - ? google_service_account.service_account[0].email - : lookup( - local.ctx.iam_principals, - var.service_account_config.email, - var.service_account_config.email + ? google_service_account.service_account[0].email # use managed SA, when creating + : (var.service_account_config.email == null ? null # set to null, if no email provided + : lookup( # lookup SA in context + local.ctx.iam_principals, + var.service_account_config.email, + var.service_account_config.email + ) ) ) service_account_roles = [ diff --git a/modules/cloud-function-v1/variables-vpcconnector.tf b/modules/cloud-function-v1/variables-vpcconnector.tf index 967ce6983..e45ebe20e 100644 --- a/modules/cloud-function-v1/variables-vpcconnector.tf +++ b/modules/cloud-function-v1/variables-vpcconnector.tf @@ -14,17 +14,6 @@ * limitations under the License. */ -variable "vpc_connector" { - description = "VPC connector configuration. Set create to 'true' if a new connector needs to be created." - type = object({ - create = optional(bool, false) - name = optional(string) - egress_settings = optional(string) - }) - nullable = false - default = {} -} - variable "vpc_connector_create" { description = "VPC connector network configuration. Must be provided if new VPC connector is being created." type = object({ @@ -50,7 +39,7 @@ variable "vpc_connector_create" { default = null validation { condition = ( - var.vpc_connector.create == false || + var.vpc_connector_create == null || try(var.vpc_connector_create.instances, null) != null || try(var.vpc_connector_create.throughput, null) != null ) diff --git a/modules/cloud-function-v1/variables.tf b/modules/cloud-function-v1/variables.tf index c089bbb07..8d0a1cc4c 100644 --- a/modules/cloud-function-v1/variables.tf +++ b/modules/cloud-function-v1/variables.tf @@ -212,3 +212,13 @@ variable "trigger_config" { }) default = null } + +variable "vpc_connector" { + description = "VPC connector configuration. Set create to 'true' if a new connector needs to be created." + type = object({ + name = optional(string) + egress_settings = optional(string) + }) + nullable = false + default = {} +} diff --git a/modules/cloud-function-v2/README.md b/modules/cloud-function-v2/README.md index 9f60b645f..c4ae121d3 100644 --- a/modules/cloud-function-v2/README.md +++ b/modules/cloud-function-v2/README.md @@ -432,8 +432,8 @@ module "cf_http" { | [secrets](variables.tf#L192) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) | | {} | | [service_account_config](variables-serviceaccount.tf#L17) | Service account configurations. | object({…}) | | {} | | [trigger_config](variables.tf#L204) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | -| [vpc_connector](variables-vpcconnector.tf#L17) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | {} | -| [vpc_connector_create](variables-vpcconnector.tf#L28) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | +| [vpc_connector](variables.tf#L222) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | {} | +| [vpc_connector_create](variables-vpcconnector.tf#L17) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | ## Outputs diff --git a/modules/cloud-function-v2/main.tf b/modules/cloud-function-v2/main.tf index 7905158ca..e1273aecc 100644 --- a/modules/cloud-function-v2/main.tf +++ b/modules/cloud-function-v2/main.tf @@ -41,7 +41,7 @@ locals { var.trigger_config.service_account_email, null ) - vpc_connector = var.vpc_connector.create ? google_vpc_access_connector.connector[0].id : var.vpc_connector.name + vpc_connector = var.vpc_connector_create != null ? google_vpc_access_connector.connector[0].id : var.vpc_connector.name } resource "google_cloudfunctions2_function" "function" { diff --git a/modules/cloud-function-v2/serviceaccount.tf b/modules/cloud-function-v2/serviceaccount.tf index f1fdec866..46c7499e4 100644 --- a/modules/cloud-function-v2/serviceaccount.tf +++ b/modules/cloud-function-v2/serviceaccount.tf @@ -17,11 +17,13 @@ locals { service_account_email = ( var.service_account_config.create - ? google_service_account.service_account[0].email - : lookup( - local.ctx.iam_principals, - var.service_account_config.email, - var.service_account_config.email + ? google_service_account.service_account[0].email # use managed SA, when creating + : (var.service_account_config.email == null ? null # set to null, if no email provided + : lookup( # lookup SA in context + local.ctx.iam_principals, + var.service_account_config.email, + var.service_account_config.email + ) ) ) service_account_roles = [ diff --git a/modules/cloud-function-v2/variables-vpcconnector.tf b/modules/cloud-function-v2/variables-vpcconnector.tf index 967ce6983..e45ebe20e 100644 --- a/modules/cloud-function-v2/variables-vpcconnector.tf +++ b/modules/cloud-function-v2/variables-vpcconnector.tf @@ -14,17 +14,6 @@ * limitations under the License. */ -variable "vpc_connector" { - description = "VPC connector configuration. Set create to 'true' if a new connector needs to be created." - type = object({ - create = optional(bool, false) - name = optional(string) - egress_settings = optional(string) - }) - nullable = false - default = {} -} - variable "vpc_connector_create" { description = "VPC connector network configuration. Must be provided if new VPC connector is being created." type = object({ @@ -50,7 +39,7 @@ variable "vpc_connector_create" { default = null validation { condition = ( - var.vpc_connector.create == false || + var.vpc_connector_create == null || try(var.vpc_connector_create.instances, null) != null || try(var.vpc_connector_create.throughput, null) != null ) diff --git a/modules/cloud-function-v2/variables.tf b/modules/cloud-function-v2/variables.tf index 0926f8fd5..d8a115e23 100644 --- a/modules/cloud-function-v2/variables.tf +++ b/modules/cloud-function-v2/variables.tf @@ -218,3 +218,13 @@ variable "trigger_config" { }) default = null } + +variable "vpc_connector" { + description = "VPC connector configuration. Set create to 'true' if a new connector needs to be created." + type = object({ + name = optional(string) + egress_settings = optional(string) + }) + nullable = false + default = {} +} diff --git a/modules/cloud-run-v2/README.md b/modules/cloud-run-v2/README.md index f33a3a13f..8b802edad 100644 --- a/modules/cloud-run-v2/README.md +++ b/modules/cloud-run-v2/README.md @@ -34,7 +34,7 @@ IAM bindings support the usual syntax. Container environment values can be decla module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id - name = "hello" + name = "example-hello" region = var.region containers = { hello = { @@ -65,7 +65,7 @@ module "cloud_run" { module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id - name = "hello" + name = "example-hello" region = var.region containers = { hello = { @@ -95,7 +95,7 @@ module "cloud_run" { module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id - name = "hello" + name = "example-hello" region = var.region containers = { hello = { @@ -132,7 +132,7 @@ module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id region = var.region - name = "hello" + name = "example-hello" containers = { hello = { image = "us-docker.pkg.dev/cloudrun/container/hello" @@ -157,7 +157,7 @@ module "cloud_run" { module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id - name = "hello" + name = "example-hello" region = var.region containers = { hello = { @@ -190,7 +190,7 @@ module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id region = var.regions.secondary - name = "hello" + name = "example-hello" containers = { hello = { image = "us-docker.pkg.dev/cloudrun/container/hello" @@ -204,7 +204,7 @@ module "cloud_run" { } deletion_protection = false } -# tftest modules=1 resources=2 fixtures=fixtures/vpc-connector.tf inventory=service-vpc-access-connector.yaml e2e +# tftest fixtures=fixtures/vpc-connector.tf inventory=service-vpc-access-connector.yaml e2e ``` If creation of the VPC Access Connector is required, use the `vpc_connector_create` variable which also supports optional attributes like number of instances, machine type, or throughput. The connector will be used automatically by Cloud Run Service and Job. Worker Pool does not support connector. @@ -214,7 +214,7 @@ module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id region = var.region - name = "hello" + name = "example-hello" containers = { hello = { image = "us-docker.pkg.dev/cloudrun/container/hello" @@ -240,7 +240,7 @@ module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = module.project-service.project_id region = var.region - name = "hello" + name = "example-hello" containers = { hello = { image = "us-docker.pkg.dev/cloudrun/container/hello" @@ -301,7 +301,7 @@ module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = module.project.project_id region = var.region - name = "hello" + name = "example-hello" encryption_key = module.kms.keys.key-regional.id containers = { hello = { @@ -310,7 +310,7 @@ module "cloud_run" { } deletion_protection = false } -# tftest modules=3 resources=11 e2e +# tftest inventory=cmek.yaml e2e ``` ## Deploying OpenTelemetry Collector sidecar @@ -509,7 +509,7 @@ module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id region = var.region - name = "hello" + name = "example-hello" containers = { hello = { image = "us-docker.pkg.dev/cloudrun/container/hello" @@ -568,7 +568,7 @@ module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id region = var.region - name = "hello" + name = "example-hello" containers = { hello = { image = "us-docker.pkg.dev/cloudrun/container/hello" @@ -595,7 +595,7 @@ module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id region = var.region - name = "hello" + name = "example-hello" containers = { hello = { image = "us-docker.pkg.dev/cloudrun/container/hello" @@ -636,7 +636,7 @@ module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id region = var.region - name = "hello" + name = "example-hello" containers = { hello = { image = "us-docker.pkg.dev/cloudrun/container/hello" @@ -684,7 +684,7 @@ module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id region = var.region - name = "hello" + name = "example-hello" containers = { hello = { image = "us-docker.pkg.dev/cloudrun/container/hello" @@ -700,42 +700,54 @@ module "cloud_run" { ## Cloud Run Service Account -To use a custom service account managed by the module, set `service_account_create` to `true` and leave `service_account` set to `null` (default). +The module by default creates a service account that is associated with the Cloud Run instance. It grants the service account `roles/logging.logWriter` and `roles/monitoring.metricWriter` roles. + +To assign non-default roles, pass them as `service_account_config.roles`. ```hcl module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id region = var.region - name = "hello" + name = "example-hello" containers = { hello = { image = "us-docker.pkg.dev/cloudrun/container/hello" } } - service_account_create = true - deletion_protection = false + service_account_config = { + roles = [ + "roles/logging.logWriter", + "roles/monitoring.metricWriter", + "roles/cloudsql.client", + "roles/cloudsql.instanceUser", + ] + } + deletion_protection = false } # tftest inventory=service-sa-create.yaml e2e ``` -To use an externally managed service account, use its email in `service_account` and leave `service_account_create` to `false` (default). +To use externally managed service account, pass its email in `service_account_config.email` and set `service_account_config.email` to `false`. ```hcl module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id region = var.region - name = "hello" + name = "example-hello" containers = { hello = { image = "us-docker.pkg.dev/cloudrun/container/hello" } } - service_account = module.iam-service-account.email + service_account_config = { + create = false + email = module.iam-service-account.email + } deletion_protection = false } -# tftest modules=2 resources=2 fixtures=fixtures/iam-service-account.tf inventory=service-external-sa.yaml e2e +# tftest fixtures=fixtures/iam-service-account.tf inventory=service-external-sa.yaml e2e ``` ## Creating Cloud Run Jobs @@ -762,7 +774,7 @@ Additional configuration can be passwed as `job_config`: module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id - name = "hello" + name = "example-hello" region = var.region type = "JOB" containers = { @@ -854,7 +866,7 @@ IAP is only supported for service. Refer to the [Configure IAP directly on cloud module "cloud_run" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id - name = "hello" + name = "example-hello" region = var.region launch_stage = "BETA" containers = { @@ -869,7 +881,7 @@ module "cloud_run" { } deletion_protection = false } -# tftest modules=1 resources=2 e2e +# tftest inventory=iap.yaml e2e ``` ## Adding GPUs @@ -880,7 +892,7 @@ GPU support is available for all types of Cloud Run resources: jobs, services an module "job" { source = "./fabric/modules/cloud-run-v2" project_id = var.project_id - name = "job" + name = "example-job" region = var.region launch_stage = "BETA" revision = { @@ -974,26 +986,26 @@ module "worker" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L160) | Name used for Cloud Run service. | string | ✓ | | -| [project_id](variables.tf#L165) | Project id used for all resources. | string | ✓ | | -| [region](variables.tf#L170) | Region used for all resources. | string | ✓ | | +| [name](variables.tf#L178) | Name used for Cloud Run service. | string | ✓ | | +| [project_id](variables.tf#L183) | Project id used for all resources. | string | ✓ | | +| [region](variables.tf#L188) | Region used for all resources. | string | ✓ | | | [containers](variables.tf#L17) | Containers in name => attributes format. | map(object({…})) | | {} | -| [deletion_protection](variables.tf#L97) | Deletion protection setting for this Cloud Run service. | string | | null | -| [encryption_key](variables.tf#L103) | The full resource name of the Cloud KMS CryptoKey. | string | | null | -| [iam](variables.tf#L109) | IAM bindings for Cloud Run service in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | -| [job_config](variables.tf#L115) | Cloud Run Job specific configuration. | object({…}) | | {} | -| [labels](variables.tf#L130) | Resource labels. | map(string) | | {} | -| [launch_stage](variables.tf#L136) | The launch stage as defined by Google Cloud Platform Launch Stages. | string | | null | -| [managed_revision](variables.tf#L153) | Whether the Terraform module should control the deployment of revisions. | bool | | true | -| [revision](variables.tf#L175) | Revision template configurations. | object({…}) | | {} | -| [service_account](variables.tf#L236) | Service account email. Unused if service account is auto-created. | string | | null | -| [service_account_create](variables.tf#L242) | Auto-create service account. | bool | | false | -| [service_config](variables.tf#L248) | Cloud Run service specific configuration options. | object({…}) | | {} | -| [tag_bindings](variables.tf#L311) | Tag bindings for this service, in key => tag value id format. | map(string) | | {} | -| [type](variables.tf#L318) | Type of Cloud Run resource to deploy: JOB, SERVICE or WORKERPOOL. | string | | "SERVICE" | -| [volumes](variables.tf#L328) | Named volumes in containers in name => attributes format. | map(object({…})) | | {} | -| [vpc_connector_create](variables-vpcconnector.tf#L17) | Populate this to create a Serverless VPC Access connector. | object({…}) | | null | -| [workerpool_config](variables.tf#L362) | Cloud Run Worker Pool specific configuration. | object({…}) | | {} | +| [context](variables.tf#L97) | Context-specific interpolations. | object({…}) | | {} | +| [deletion_protection](variables.tf#L115) | Deletion protection setting for this Cloud Run service. | string | | null | +| [encryption_key](variables.tf#L121) | The full resource name of the Cloud KMS CryptoKey. | string | | null | +| [iam](variables.tf#L127) | IAM bindings for Cloud Run service in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [job_config](variables.tf#L133) | Cloud Run Job specific configuration. | object({…}) | | {} | +| [labels](variables.tf#L148) | Resource labels. | map(string) | | {} | +| [launch_stage](variables.tf#L154) | The launch stage as defined by Google Cloud Platform Launch Stages. | string | | null | +| [managed_revision](variables.tf#L171) | Whether the Terraform module should control the deployment of revisions. | bool | | true | +| [revision](variables.tf#L193) | Revision template configurations. | object({…}) | | {} | +| [service_account_config](variables-serviceaccount.tf#L17) | Service account configurations. | object({…}) | | {} | +| [service_config](variables.tf#L260) | Cloud Run service specific configuration options. | object({…}) | | {} | +| [tag_bindings](variables.tf#L323) | Tag bindings for this service, in key => tag value id format. | map(string) | | {} | +| [type](variables.tf#L330) | Type of Cloud Run resource to deploy: JOB, SERVICE or WORKERPOOL. | string | | "SERVICE" | +| [volumes](variables.tf#L340) | Named volumes in containers in name => attributes format. | map(object({…})) | | {} | +| [vpc_connector_create](variables-vpcconnector.tf#L17) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | +| [workerpool_config](variables.tf#L374) | Cloud Run Worker Pool specific configuration. | object({…}) | | {} | ## Outputs diff --git a/modules/cloud-run-v2/job-managed.tf b/modules/cloud-run-v2/job-managed.tf new file mode 100644 index 000000000..c65578712 --- /dev/null +++ b/modules/cloud-run-v2/job-managed.tf @@ -0,0 +1,223 @@ +/** + * 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_cloud_run_v2_job" "job" { + count = var.type == "JOB" && var.managed_revision ? 1 : 0 + provider = google-beta + project = local.project_id + location = local.location + name = var.name + labels = var.labels + launch_stage = var.launch_stage + deletion_protection = var.deletion_protection + template { + labels = var.revision.labels + task_count = var.job_config.task_count + template { + encryption_key = var.encryption_key + gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled + dynamic "node_selector" { + for_each = var.revision.node_selector == null ? [] : [""] + content { + accelerator = var.revision.node_selector.accelerator + } + } + dynamic "vpc_access" { + for_each = local.connector == null ? [] : [""] + content { + connector = local.connector + egress = try(var.revision.vpc_access.egress, null) + } + } + dynamic "vpc_access" { + for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""] + content { + egress = var.revision.vpc_access.egress + network_interfaces { + subnetwork = var.revision.vpc_access.subnet == null ? null : lookup( + local.ctx.subnets, var.revision.vpc_access.subnet, + var.revision.vpc_access.subnet + ) + network = var.revision.vpc_access.network == null ? null : lookup( + local.ctx.networks, var.revision.vpc_access.network, + var.revision.vpc_access.network + ) + tags = var.revision.vpc_access.tags + } + } + } + max_retries = var.job_config.max_retries + timeout = var.job_config.timeout + service_account = local.service_account_email + dynamic "containers" { + for_each = var.containers + content { + name = containers.key + image = containers.value.image + depends_on = containers.value.depends_on + command = containers.value.command + args = containers.value.args + dynamic "env" { + for_each = coalesce(containers.value.env, tomap({})) + content { + name = env.key + value = env.value + } + } + dynamic "env" { + for_each = coalesce(containers.value.env_from_key, tomap({})) + content { + name = env.key + value_source { + secret_key_ref { + secret = env.value.secret + version = env.value.version + } + } + } + } + dynamic "resources" { + for_each = containers.value.resources == null ? [] : [""] + content { + limits = containers.value.resources.limits + } + } + dynamic "ports" { + for_each = coalesce(containers.value.ports, tomap({})) + content { + container_port = ports.value.container_port + name = ports.value.name + } + } + dynamic "volume_mounts" { + for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" } + content { + name = volume_mounts.key + mount_path = volume_mounts.value + } + } + # CloudSQL is the last mount in the list returned by API + dynamic "volume_mounts" { + for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" } + content { + name = volume_mounts.key + mount_path = volume_mounts.value + } + } + dynamic "startup_probe" { + for_each = containers.value.startup_probe == null ? [] : [""] + content { + initial_delay_seconds = containers.value.startup_probe.initial_delay_seconds + timeout_seconds = containers.value.startup_probe.timeout_seconds + period_seconds = containers.value.startup_probe.period_seconds + failure_threshold = containers.value.startup_probe.failure_threshold + dynamic "http_get" { + for_each = containers.value.startup_probe.http_get == null ? [] : [""] + content { + path = containers.value.startup_probe.http_get.path + port = containers.value.startup_probe.http_get.port + dynamic "http_headers" { + for_each = coalesce(containers.value.startup_probe.http_get.http_headers, tomap({})) + content { + name = http_headers.key + value = http_headers.value + } + } + } + } + dynamic "tcp_socket" { + for_each = containers.value.startup_probe.tcp_socket == null ? [] : [""] + content { + port = containers.value.startup_probe.tcp_socket.port + } + } + dynamic "grpc" { + for_each = containers.value.startup_probe.grpc == null ? [] : [""] + content { + port = containers.value.startup_probe.grpc.port + service = containers.value.startup_probe.grpc.service + } + } + } + } + } + } + dynamic "volumes" { + for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null } + content { + name = volumes.key + dynamic "secret" { + for_each = volumes.value.secret == null ? [] : [""] + content { + secret = volumes.value.secret.name + default_mode = volumes.value.secret.default_mode + dynamic "items" { + for_each = volumes.value.secret.path == null ? [] : [""] + content { + path = volumes.value.secret.path + version = volumes.value.secret.version + mode = volumes.value.secret.mode + } + } + } + } + + dynamic "empty_dir" { + for_each = volumes.value.empty_dir_size == null ? [] : [""] + content { + medium = "MEMORY" + size_limit = volumes.value.empty_dir_size + } + } + dynamic "gcs" { + for_each = volumes.value.gcs == null ? [] : [""] + content { + bucket = volumes.value.gcs.bucket + read_only = volumes.value.gcs.is_read_only + } + } + dynamic "nfs" { + for_each = volumes.value.nfs == null ? [] : [""] + content { + server = volumes.value.nfs.server + path = volumes.value.nfs.path + read_only = volumes.value.nfs.is_read_only + } + } + } + } + # CloudSQL is the last volume in the list returned by API + dynamic "volumes" { + for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null } + content { + name = volumes.key + dynamic "cloud_sql_instance" { + for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""] + content { + instances = volumes.value.cloud_sql_instances + } + } + } + } + } + } + + lifecycle { + ignore_changes = [ + template[0].annotations["run.googleapis.com/operation-id"], + ] + } +} diff --git a/modules/cloud-run-v2/job-unmanaged.tf b/modules/cloud-run-v2/job-unmanaged.tf new file mode 100644 index 000000000..1961acad6 --- /dev/null +++ b/modules/cloud-run-v2/job-unmanaged.tf @@ -0,0 +1,227 @@ +/** + * 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_cloud_run_v2_job" "job_unmanaged" { + count = var.type == "JOB" && !var.managed_revision ? 1 : 0 + provider = google-beta + project = local.project_id + location = local.location + name = var.name + labels = var.labels + launch_stage = var.launch_stage + deletion_protection = var.deletion_protection + template { + labels = var.revision.labels + task_count = var.job_config.task_count + template { + encryption_key = var.encryption_key + gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled + dynamic "node_selector" { + for_each = var.revision.node_selector == null ? [] : [""] + content { + accelerator = var.revision.node_selector.accelerator + } + } + dynamic "vpc_access" { + for_each = local.connector == null ? [] : [""] + content { + connector = local.connector + egress = try(var.revision.vpc_access.egress, null) + } + } + dynamic "vpc_access" { + for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""] + content { + egress = var.revision.vpc_access.egress + network_interfaces { + subnetwork = var.revision.vpc_access.subnet == null ? null : lookup( + local.ctx.subnets, var.revision.vpc_access.subnet, + var.revision.vpc_access.subnet + ) + network = var.revision.vpc_access.network == null ? null : lookup( + local.ctx.networks, var.revision.vpc_access.network, + var.revision.vpc_access.network + ) + tags = var.revision.vpc_access.tags + } + } + } + max_retries = var.job_config.max_retries + timeout = var.job_config.timeout + service_account = local.service_account_email + dynamic "containers" { + for_each = var.containers + content { + name = containers.key + image = containers.value.image + depends_on = containers.value.depends_on + command = containers.value.command + args = containers.value.args + dynamic "env" { + for_each = coalesce(containers.value.env, tomap({})) + content { + name = env.key + value = env.value + } + } + dynamic "env" { + for_each = coalesce(containers.value.env_from_key, tomap({})) + content { + name = env.key + value_source { + secret_key_ref { + secret = env.value.secret + version = env.value.version + } + } + } + } + dynamic "resources" { + for_each = containers.value.resources == null ? [] : [""] + content { + limits = containers.value.resources.limits + } + } + dynamic "ports" { + for_each = coalesce(containers.value.ports, tomap({})) + content { + container_port = ports.value.container_port + name = ports.value.name + } + } + dynamic "volume_mounts" { + for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" } + content { + name = volume_mounts.key + mount_path = volume_mounts.value + } + } + # CloudSQL is the last mount in the list returned by API + dynamic "volume_mounts" { + for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" } + content { + name = volume_mounts.key + mount_path = volume_mounts.value + } + } + dynamic "startup_probe" { + for_each = containers.value.startup_probe == null ? [] : [""] + content { + initial_delay_seconds = containers.value.startup_probe.initial_delay_seconds + timeout_seconds = containers.value.startup_probe.timeout_seconds + period_seconds = containers.value.startup_probe.period_seconds + failure_threshold = containers.value.startup_probe.failure_threshold + dynamic "http_get" { + for_each = containers.value.startup_probe.http_get == null ? [] : [""] + content { + path = containers.value.startup_probe.http_get.path + port = containers.value.startup_probe.http_get.port + dynamic "http_headers" { + for_each = coalesce(containers.value.startup_probe.http_get.http_headers, tomap({})) + content { + name = http_headers.key + value = http_headers.value + } + } + } + } + dynamic "tcp_socket" { + for_each = containers.value.startup_probe.tcp_socket == null ? [] : [""] + content { + port = containers.value.startup_probe.tcp_socket.port + } + } + dynamic "grpc" { + for_each = containers.value.startup_probe.grpc == null ? [] : [""] + content { + port = containers.value.startup_probe.grpc.port + service = containers.value.startup_probe.grpc.service + } + } + } + } + } + } + dynamic "volumes" { + for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null } + content { + name = volumes.key + dynamic "secret" { + for_each = volumes.value.secret == null ? [] : [""] + content { + secret = volumes.value.secret.name + default_mode = volumes.value.secret.default_mode + dynamic "items" { + for_each = volumes.value.secret.path == null ? [] : [""] + content { + path = volumes.value.secret.path + version = volumes.value.secret.version + mode = volumes.value.secret.mode + } + } + } + } + + dynamic "empty_dir" { + for_each = volumes.value.empty_dir_size == null ? [] : [""] + content { + medium = "MEMORY" + size_limit = volumes.value.empty_dir_size + } + } + dynamic "gcs" { + for_each = volumes.value.gcs == null ? [] : [""] + content { + bucket = volumes.value.gcs.bucket + read_only = volumes.value.gcs.is_read_only + } + } + dynamic "nfs" { + for_each = volumes.value.nfs == null ? [] : [""] + content { + server = volumes.value.nfs.server + path = volumes.value.nfs.path + read_only = volumes.value.nfs.is_read_only + } + } + } + } + # CloudSQL is the last volume in the list returned by API + dynamic "volumes" { + for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null } + content { + name = volumes.key + dynamic "cloud_sql_instance" { + for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""] + content { + instances = volumes.value.cloud_sql_instances + } + } + } + } + } + } + + lifecycle { + ignore_changes = [ + client, + client_version, + template[0].annotations["run.googleapis.com/operation-id"], + template[0].template, + template[0].labels + ] + } +} diff --git a/modules/cloud-run-v2/job.tf b/modules/cloud-run-v2/job.tf index 106482f0d..40ce76425 100644 --- a/modules/cloud-run-v2/job.tf +++ b/modules/cloud-run-v2/job.tf @@ -14,419 +14,11 @@ * limitations under the License. */ -resource "google_cloud_run_v2_job" "job" { - count = var.type == "JOB" && var.managed_revision ? 1 : 0 - provider = google-beta - project = var.project_id - location = var.region - name = var.name - labels = var.labels - launch_stage = var.launch_stage - deletion_protection = var.deletion_protection - template { - labels = var.revision.labels - task_count = var.job_config.task_count - template { - encryption_key = var.encryption_key - gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled - dynamic "node_selector" { - for_each = var.revision.node_selector == null ? [] : [""] - content { - accelerator = var.revision.node_selector.accelerator - } - } - dynamic "vpc_access" { - for_each = local.connector == null ? [] : [""] - content { - connector = local.connector - egress = try(var.revision.vpc_access.egress, null) - } - } - dynamic "vpc_access" { - for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""] - content { - egress = var.revision.vpc_access.egress - network_interfaces { - subnetwork = var.revision.vpc_access.subnet - network = var.revision.vpc_access.network - tags = var.revision.vpc_access.tags - } - } - } - max_retries = var.job_config.max_retries - timeout = var.job_config.timeout - service_account = local.service_account_email - dynamic "containers" { - for_each = var.containers - content { - name = containers.key - image = containers.value.image - depends_on = containers.value.depends_on - command = containers.value.command - args = containers.value.args - dynamic "env" { - for_each = coalesce(containers.value.env, tomap({})) - content { - name = env.key - value = env.value - } - } - dynamic "env" { - for_each = coalesce(containers.value.env_from_key, tomap({})) - content { - name = env.key - value_source { - secret_key_ref { - secret = env.value.secret - version = env.value.version - } - } - } - } - dynamic "resources" { - for_each = containers.value.resources == null ? [] : [""] - content { - limits = containers.value.resources.limits - } - } - dynamic "ports" { - for_each = coalesce(containers.value.ports, tomap({})) - content { - container_port = ports.value.container_port - name = ports.value.name - } - } - dynamic "volume_mounts" { - for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" } - content { - name = volume_mounts.key - mount_path = volume_mounts.value - } - } - # CloudSQL is the last mount in the list returned by API - dynamic "volume_mounts" { - for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" } - content { - name = volume_mounts.key - mount_path = volume_mounts.value - } - } - dynamic "startup_probe" { - for_each = containers.value.startup_probe == null ? [] : [""] - content { - initial_delay_seconds = containers.value.startup_probe.initial_delay_seconds - timeout_seconds = containers.value.startup_probe.timeout_seconds - period_seconds = containers.value.startup_probe.period_seconds - failure_threshold = containers.value.startup_probe.failure_threshold - dynamic "http_get" { - for_each = containers.value.startup_probe.http_get == null ? [] : [""] - content { - path = containers.value.startup_probe.http_get.path - port = containers.value.startup_probe.http_get.port - dynamic "http_headers" { - for_each = coalesce(containers.value.startup_probe.http_get.http_headers, tomap({})) - content { - name = http_headers.key - value = http_headers.value - } - } - } - } - dynamic "tcp_socket" { - for_each = containers.value.startup_probe.tcp_socket == null ? [] : [""] - content { - port = containers.value.startup_probe.tcp_socket.port - } - } - dynamic "grpc" { - for_each = containers.value.startup_probe.grpc == null ? [] : [""] - content { - port = containers.value.startup_probe.grpc.port - service = containers.value.startup_probe.grpc.service - } - } - } - } - } - } - dynamic "volumes" { - for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null } - content { - name = volumes.key - dynamic "secret" { - for_each = volumes.value.secret == null ? [] : [""] - content { - secret = volumes.value.secret.name - default_mode = volumes.value.secret.default_mode - dynamic "items" { - for_each = volumes.value.secret.path == null ? [] : [""] - content { - path = volumes.value.secret.path - version = volumes.value.secret.version - mode = volumes.value.secret.mode - } - } - } - } - - dynamic "empty_dir" { - for_each = volumes.value.empty_dir_size == null ? [] : [""] - content { - medium = "MEMORY" - size_limit = volumes.value.empty_dir_size - } - } - dynamic "gcs" { - for_each = volumes.value.gcs == null ? [] : [""] - content { - bucket = volumes.value.gcs.bucket - read_only = volumes.value.gcs.is_read_only - } - } - dynamic "nfs" { - for_each = volumes.value.nfs == null ? [] : [""] - content { - server = volumes.value.nfs.server - path = volumes.value.nfs.path - read_only = volumes.value.nfs.is_read_only - } - } - } - } - # CloudSQL is the last volume in the list returned by API - dynamic "volumes" { - for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null } - content { - name = volumes.key - dynamic "cloud_sql_instance" { - for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""] - content { - instances = volumes.value.cloud_sql_instances - } - } - } - } - } - } - - lifecycle { - ignore_changes = [ - template[0].annotations["run.googleapis.com/operation-id"], - ] - } -} - -resource "google_cloud_run_v2_job" "job_unmanaged" { - count = var.type == "JOB" && !var.managed_revision ? 1 : 0 - provider = google-beta - project = var.project_id - location = var.region - name = var.name - labels = var.labels - launch_stage = var.launch_stage - deletion_protection = var.deletion_protection - template { - labels = var.revision.labels - task_count = var.job_config.task_count - template { - encryption_key = var.encryption_key - gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled - dynamic "node_selector" { - for_each = var.revision.node_selector == null ? [] : [""] - content { - accelerator = var.revision.node_selector.accelerator - } - } - dynamic "vpc_access" { - for_each = local.connector == null ? [] : [""] - content { - connector = local.connector - egress = try(var.revision.vpc_access.egress, null) - } - } - dynamic "vpc_access" { - for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""] - content { - egress = var.revision.vpc_access.egress - network_interfaces { - subnetwork = var.revision.vpc_access.subnet - network = var.revision.vpc_access.network - tags = var.revision.vpc_access.tags - } - } - } - max_retries = var.job_config.max_retries - timeout = var.job_config.timeout - service_account = local.service_account_email - dynamic "containers" { - for_each = var.containers - content { - name = containers.key - image = containers.value.image - depends_on = containers.value.depends_on - command = containers.value.command - args = containers.value.args - dynamic "env" { - for_each = coalesce(containers.value.env, tomap({})) - content { - name = env.key - value = env.value - } - } - dynamic "env" { - for_each = coalesce(containers.value.env_from_key, tomap({})) - content { - name = env.key - value_source { - secret_key_ref { - secret = env.value.secret - version = env.value.version - } - } - } - } - dynamic "resources" { - for_each = containers.value.resources == null ? [] : [""] - content { - limits = containers.value.resources.limits - } - } - dynamic "ports" { - for_each = coalesce(containers.value.ports, tomap({})) - content { - container_port = ports.value.container_port - name = ports.value.name - } - } - dynamic "volume_mounts" { - for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" } - content { - name = volume_mounts.key - mount_path = volume_mounts.value - } - } - # CloudSQL is the last mount in the list returned by API - dynamic "volume_mounts" { - for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" } - content { - name = volume_mounts.key - mount_path = volume_mounts.value - } - } - dynamic "startup_probe" { - for_each = containers.value.startup_probe == null ? [] : [""] - content { - initial_delay_seconds = containers.value.startup_probe.initial_delay_seconds - timeout_seconds = containers.value.startup_probe.timeout_seconds - period_seconds = containers.value.startup_probe.period_seconds - failure_threshold = containers.value.startup_probe.failure_threshold - dynamic "http_get" { - for_each = containers.value.startup_probe.http_get == null ? [] : [""] - content { - path = containers.value.startup_probe.http_get.path - port = containers.value.startup_probe.http_get.port - dynamic "http_headers" { - for_each = coalesce(containers.value.startup_probe.http_get.http_headers, tomap({})) - content { - name = http_headers.key - value = http_headers.value - } - } - } - } - dynamic "tcp_socket" { - for_each = containers.value.startup_probe.tcp_socket == null ? [] : [""] - content { - port = containers.value.startup_probe.tcp_socket.port - } - } - dynamic "grpc" { - for_each = containers.value.startup_probe.grpc == null ? [] : [""] - content { - port = containers.value.startup_probe.grpc.port - service = containers.value.startup_probe.grpc.service - } - } - } - } - } - } - dynamic "volumes" { - for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null } - content { - name = volumes.key - dynamic "secret" { - for_each = volumes.value.secret == null ? [] : [""] - content { - secret = volumes.value.secret.name - default_mode = volumes.value.secret.default_mode - dynamic "items" { - for_each = volumes.value.secret.path == null ? [] : [""] - content { - path = volumes.value.secret.path - version = volumes.value.secret.version - mode = volumes.value.secret.mode - } - } - } - } - - dynamic "empty_dir" { - for_each = volumes.value.empty_dir_size == null ? [] : [""] - content { - medium = "MEMORY" - size_limit = volumes.value.empty_dir_size - } - } - dynamic "gcs" { - for_each = volumes.value.gcs == null ? [] : [""] - content { - bucket = volumes.value.gcs.bucket - read_only = volumes.value.gcs.is_read_only - } - } - dynamic "nfs" { - for_each = volumes.value.nfs == null ? [] : [""] - content { - server = volumes.value.nfs.server - path = volumes.value.nfs.path - read_only = volumes.value.nfs.is_read_only - } - } - } - } - # CloudSQL is the last volume in the list returned by API - dynamic "volumes" { - for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null } - content { - name = volumes.key - dynamic "cloud_sql_instance" { - for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""] - content { - instances = volumes.value.cloud_sql_instances - } - } - } - } - } - } - - lifecycle { - ignore_changes = [ - client, - client_version, - template[0].annotations["run.googleapis.com/operation-id"], - template[0].template, - template[0].labels - ] - } -} - resource "google_cloud_run_v2_job_iam_binding" "binding" { for_each = var.type == "JOB" ? var.iam : {} project = local.resource.project location = local.resource.location name = local.resource.name - role = each.key - members = each.value + role = lookup(local.ctx.custom_roles, each.key, each.key) + members = [for member in each.value : lookup(local.ctx.iam_principals, member, member)] } diff --git a/modules/cloud-run-v2/main.tf b/modules/cloud-run-v2/main.tf index f0aa07ff1..1ae0186f5 100644 --- a/modules/cloud-run-v2/main.tf +++ b/modules/cloud-run-v2/main.tf @@ -15,6 +15,12 @@ */ locals { + _ctx_p = "$" + ctx = { + for k, v in var.context : k => { + for kk, vv in v : "${local._ctx_p}${k}:${kk}" => vv + } if k != "condition_vars" + } connector = ( var.vpc_connector_create != null ? google_vpc_access_connector.connector[0].id @@ -37,14 +43,12 @@ locals { } invoke_command = local._invoke_command[var.type] + location = lookup(local.ctx.locations, var.region, var.region) + project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id) + revision_name = ( var.revision.name == null ? null : "${var.name}-${var.revision.name}" ) - service_account_email = ( - var.service_account_create - ? google_service_account.service_account[0].email - : var.service_account - ) _resource = { "JOB" : ( var.managed_revision ? @@ -67,82 +71,3 @@ locals { uri = var.type == "SERVICE" ? local._resource[var.type].uri : "" } } - -resource "google_service_account" "service_account" { - count = var.service_account_create ? 1 : 0 - project = var.project_id - account_id = "tf-cr-${var.name}" - display_name = "Terraform Cloud Run ${var.name}." -} - -resource "google_eventarc_trigger" "audit_log_triggers" { - for_each = coalesce(var.service_config.eventarc_triggers.audit_log, tomap({})) - name = "audit-log-${each.key}" - location = google_cloud_run_v2_service.service[0].location - project = google_cloud_run_v2_service.service[0].project - matching_criteria { - attribute = "type" - value = "google.cloud.audit.log.v1.written" - } - matching_criteria { - attribute = "serviceName" - value = each.value.service - } - matching_criteria { - attribute = "methodName" - value = each.value.method - } - destination { - cloud_run_service { - service = google_cloud_run_v2_service.service[0].name - region = google_cloud_run_v2_service.service[0].location - } - } - service_account = var.service_config.eventarc_triggers.service_account_email -} - -resource "google_eventarc_trigger" "pubsub_triggers" { - for_each = coalesce(var.service_config.eventarc_triggers.pubsub, tomap({})) - name = "pubsub-${each.key}" - location = google_cloud_run_v2_service.service[0].location - project = google_cloud_run_v2_service.service[0].project - matching_criteria { - attribute = "type" - value = "google.cloud.pubsub.topic.v1.messagePublished" - } - transport { - pubsub { - topic = each.value - } - } - destination { - cloud_run_service { - service = google_cloud_run_v2_service.service[0].name - region = google_cloud_run_v2_service.service[0].location - } - } - service_account = var.service_config.eventarc_triggers.service_account_email -} - -resource "google_eventarc_trigger" "storage_triggers" { - for_each = coalesce(var.service_config.eventarc_triggers.storage, tomap({})) - name = "storage-${each.key}" - location = google_cloud_run_v2_service.service[0].location - project = google_cloud_run_v2_service.service[0].project - matching_criteria { - attribute = "type" - value = "google.cloud.storage.object.v1.finalized" - } - matching_criteria { - attribute = "bucket" - value = each.value.bucket - } - destination { - cloud_run_service { - service = google_cloud_run_v2_service.service[0].name - region = google_cloud_run_v2_service.service[0].location - path = try(each.value.path, null) - } - } - service_account = var.service_config.eventarc_triggers.service_account_email -} diff --git a/modules/cloud-run-v2/recipes/cloudsql-iam-auth-proxy/README.md b/modules/cloud-run-v2/recipes/cloudsql-iam-auth-proxy/README.md index 788f75e25..04a5d95e0 100644 --- a/modules/cloud-run-v2/recipes/cloudsql-iam-auth-proxy/README.md +++ b/modules/cloud-run-v2/recipes/cloudsql-iam-auth-proxy/README.md @@ -89,13 +89,16 @@ module "database_run" { } } } - service_account = module.run-sa.email - deletion_protection = false + service_account_config = { + create = false + email = module.run-sa.email + } volumes = { custom_cloudsql = { empty_dir_size = "128k" } } + deletion_protection = false } # tftest inventory=recipe-cloudsql-iam-auth-proxy.yaml e2e ``` diff --git a/modules/cloud-run-v2/service-managed.tf b/modules/cloud-run-v2/service-managed.tf new file mode 100644 index 000000000..a6078c13d --- /dev/null +++ b/modules/cloud-run-v2/service-managed.tf @@ -0,0 +1,271 @@ +/** + * 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_cloud_run_v2_service" "service" { + count = var.type == "SERVICE" && var.managed_revision ? 1 : 0 + provider = google-beta + project = local.project_id + location = local.location + name = var.name + ingress = var.service_config.ingress + invoker_iam_disabled = var.service_config.invoker_iam_disabled + labels = var.labels + launch_stage = var.launch_stage + custom_audiences = var.service_config.custom_audiences + deletion_protection = var.deletion_protection + iap_enabled = var.service_config.iap_config != null + + template { + labels = var.revision.labels + encryption_key = var.encryption_key + revision = local.revision_name + execution_environment = ( + var.service_config.gen2_execution_environment + ? "EXECUTION_ENVIRONMENT_GEN2" : "EXECUTION_ENVIRONMENT_GEN1" + ) + gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled + max_instance_request_concurrency = var.service_config.max_concurrency + dynamic "node_selector" { + for_each = var.revision.node_selector == null ? [] : [""] + content { + accelerator = var.revision.node_selector.accelerator + } + } + dynamic "scaling" { + for_each = var.service_config.scaling == null ? [] : [""] + content { + max_instance_count = var.service_config.scaling.max_instance_count + min_instance_count = var.service_config.scaling.min_instance_count + } + } + dynamic "vpc_access" { + for_each = local.connector == null ? [] : [""] + content { + connector = local.connector + egress = try(var.revision.vpc_access.egress, null) + } + } + dynamic "vpc_access" { + for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""] + content { + egress = var.revision.vpc_access.egress + network_interfaces { + subnetwork = var.revision.vpc_access.subnet == null ? null : lookup( + local.ctx.subnets, var.revision.vpc_access.subnet, + var.revision.vpc_access.subnet + ) + network = var.revision.vpc_access.network == null ? null : lookup( + local.ctx.networks, var.revision.vpc_access.network, + var.revision.vpc_access.network + ) + tags = var.revision.vpc_access.tags + } + } + } + timeout = var.service_config.timeout + service_account = local.service_account_email + dynamic "containers" { + for_each = var.containers + content { + name = containers.key + image = containers.value.image + depends_on = containers.value.depends_on + command = containers.value.command + args = containers.value.args + dynamic "env" { + for_each = coalesce(containers.value.env, tomap({})) + content { + name = env.key + value = env.value + } + } + dynamic "env" { + for_each = coalesce(containers.value.env_from_key, tomap({})) + content { + name = env.key + value_source { + secret_key_ref { + secret = env.value.secret + version = env.value.version + } + } + } + } + dynamic "resources" { + for_each = containers.value.resources == null ? [] : [""] + content { + limits = containers.value.resources.limits + cpu_idle = containers.value.resources.cpu_idle + startup_cpu_boost = containers.value.resources.startup_cpu_boost + } + } + dynamic "ports" { + for_each = coalesce(containers.value.ports, tomap({})) + content { + container_port = ports.value.container_port + name = ports.value.name + } + } + dynamic "volume_mounts" { + for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" } + content { + name = volume_mounts.key + mount_path = volume_mounts.value + } + } + # CloudSQL is the last mount in the list returned by API + dynamic "volume_mounts" { + for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" } + content { + name = volume_mounts.key + mount_path = volume_mounts.value + } + } + dynamic "liveness_probe" { + for_each = containers.value.liveness_probe == null ? [] : [""] + content { + initial_delay_seconds = containers.value.liveness_probe.initial_delay_seconds + timeout_seconds = containers.value.liveness_probe.timeout_seconds + period_seconds = containers.value.liveness_probe.period_seconds + failure_threshold = containers.value.liveness_probe.failure_threshold + dynamic "http_get" { + for_each = containers.value.liveness_probe.http_get == null ? [] : [""] + content { + path = containers.value.liveness_probe.http_get.path + port = containers.value.liveness_probe.http_get.port + dynamic "http_headers" { + for_each = coalesce(containers.value.liveness_probe.http_get.http_headers, tomap({})) + content { + name = http_headers.key + value = http_headers.value + } + } + } + } + dynamic "grpc" { + for_each = containers.value.liveness_probe.grpc == null ? [] : [""] + content { + port = containers.value.liveness_probe.grpc.port + service = containers.value.liveness_probe.grpc.service + } + } + } + } + dynamic "startup_probe" { + for_each = containers.value.startup_probe == null ? [] : [""] + content { + initial_delay_seconds = containers.value.startup_probe.initial_delay_seconds + timeout_seconds = containers.value.startup_probe.timeout_seconds + period_seconds = containers.value.startup_probe.period_seconds + failure_threshold = containers.value.startup_probe.failure_threshold + dynamic "http_get" { + for_each = containers.value.startup_probe.http_get == null ? [] : [""] + content { + path = containers.value.startup_probe.http_get.path + port = containers.value.startup_probe.http_get.port + dynamic "http_headers" { + for_each = coalesce(containers.value.startup_probe.http_get.http_headers, tomap({})) + content { + name = http_headers.key + value = http_headers.value + } + } + } + } + dynamic "tcp_socket" { + for_each = containers.value.startup_probe.tcp_socket == null ? [] : [""] + content { + port = containers.value.startup_probe.tcp_socket.port + } + } + dynamic "grpc" { + for_each = containers.value.startup_probe.grpc == null ? [] : [""] + content { + port = containers.value.startup_probe.grpc.port + service = containers.value.startup_probe.grpc.service + } + } + } + } + } + } + dynamic "volumes" { + for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null } + content { + name = volumes.key + dynamic "secret" { + for_each = volumes.value.secret == null ? [] : [""] + content { + secret = volumes.value.secret.name + default_mode = volumes.value.secret.default_mode + dynamic "items" { + for_each = volumes.value.secret.path == null ? [] : [""] + content { + path = volumes.value.secret.path + version = volumes.value.secret.version + mode = volumes.value.secret.mode + } + } + } + } + + dynamic "empty_dir" { + for_each = volumes.value.empty_dir_size == null ? [] : [""] + content { + medium = "MEMORY" + size_limit = volumes.value.empty_dir_size + } + } + dynamic "gcs" { + for_each = volumes.value.gcs == null ? [] : [""] + content { + bucket = volumes.value.gcs.bucket + read_only = volumes.value.gcs.is_read_only + } + } + dynamic "nfs" { + for_each = volumes.value.nfs == null ? [] : [""] + content { + server = volumes.value.nfs.server + path = volumes.value.nfs.path + read_only = volumes.value.nfs.is_read_only + } + } + } + } + # CloudSQL is the last volume in the list returned by API + dynamic "volumes" { + for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null } + content { + name = volumes.key + dynamic "cloud_sql_instance" { + for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""] + content { + instances = volumes.value.cloud_sql_instances + } + } + } + } + } + + lifecycle { + ignore_changes = [ + client, + client_version, + template[0].annotations["run.googleapis.com/operation-id"], + ] + } +} diff --git a/modules/cloud-run-v2/service-unmanaged.tf b/modules/cloud-run-v2/service-unmanaged.tf new file mode 100644 index 000000000..5aaa50005 --- /dev/null +++ b/modules/cloud-run-v2/service-unmanaged.tf @@ -0,0 +1,274 @@ +/** + * 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_cloud_run_v2_service" "service_unmanaged" { + count = var.type == "SERVICE" && !var.managed_revision ? 1 : 0 + provider = google-beta + project = local.project_id + location = local.location + name = var.name + ingress = var.service_config.ingress + invoker_iam_disabled = var.service_config.invoker_iam_disabled + labels = var.labels + launch_stage = var.launch_stage + custom_audiences = var.service_config.custom_audiences + deletion_protection = var.deletion_protection + iap_enabled = var.service_config.iap_config != null + + template { + labels = var.revision.labels + encryption_key = var.encryption_key + revision = local.revision_name + execution_environment = ( + var.service_config.gen2_execution_environment + ? "EXECUTION_ENVIRONMENT_GEN2" : "EXECUTION_ENVIRONMENT_GEN1" + ) + gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled + max_instance_request_concurrency = var.service_config.max_concurrency + dynamic "node_selector" { + for_each = var.revision.node_selector == null ? [] : [""] + content { + accelerator = var.revision.node_selector.accelerator + } + } + dynamic "scaling" { + for_each = var.service_config.scaling == null ? [] : [""] + content { + max_instance_count = var.service_config.scaling.max_instance_count + min_instance_count = var.service_config.scaling.min_instance_count + } + } + dynamic "vpc_access" { + for_each = local.connector == null ? [] : [""] + content { + connector = local.connector + egress = try(var.revision.vpc_access.egress, null) + } + } + dynamic "vpc_access" { + for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""] + content { + egress = var.revision.vpc_access.egress + network_interfaces { + subnetwork = var.revision.vpc_access.subnet == null ? null : lookup( + local.ctx.subnets, var.revision.vpc_access.subnet, + var.revision.vpc_access.subnet + ) + network = var.revision.vpc_access.network == null ? null : lookup( + local.ctx.networks, var.revision.vpc_access.network, + var.revision.vpc_access.network + ) + tags = var.revision.vpc_access.tags + } + } + } + timeout = var.service_config.timeout + service_account = local.service_account_email + dynamic "containers" { + for_each = var.containers + content { + name = containers.key + image = containers.value.image + depends_on = containers.value.depends_on + command = containers.value.command + args = containers.value.args + dynamic "env" { + for_each = coalesce(containers.value.env, tomap({})) + content { + name = env.key + value = env.value + } + } + dynamic "env" { + for_each = coalesce(containers.value.env_from_key, tomap({})) + content { + name = env.key + value_source { + secret_key_ref { + secret = env.value.secret + version = env.value.version + } + } + } + } + dynamic "resources" { + for_each = containers.value.resources == null ? [] : [""] + content { + limits = containers.value.resources.limits + cpu_idle = containers.value.resources.cpu_idle + startup_cpu_boost = containers.value.resources.startup_cpu_boost + } + } + dynamic "ports" { + for_each = coalesce(containers.value.ports, tomap({})) + content { + container_port = ports.value.container_port + name = ports.value.name + } + } + dynamic "volume_mounts" { + for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" } + content { + name = volume_mounts.key + mount_path = volume_mounts.value + } + } + # CloudSQL is the last mount in the list returned by API + dynamic "volume_mounts" { + for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" } + content { + name = volume_mounts.key + mount_path = volume_mounts.value + } + } + dynamic "liveness_probe" { + for_each = containers.value.liveness_probe == null ? [] : [""] + content { + initial_delay_seconds = containers.value.liveness_probe.initial_delay_seconds + timeout_seconds = containers.value.liveness_probe.timeout_seconds + period_seconds = containers.value.liveness_probe.period_seconds + failure_threshold = containers.value.liveness_probe.failure_threshold + dynamic "http_get" { + for_each = containers.value.liveness_probe.http_get == null ? [] : [""] + content { + path = containers.value.liveness_probe.http_get.path + port = containers.value.liveness_probe.http_get.port + dynamic "http_headers" { + for_each = coalesce(containers.value.liveness_probe.http_get.http_headers, tomap({})) + content { + name = http_headers.key + value = http_headers.value + } + } + } + } + dynamic "grpc" { + for_each = containers.value.liveness_probe.grpc == null ? [] : [""] + content { + port = containers.value.liveness_probe.grpc.port + service = containers.value.liveness_probe.grpc.service + } + } + } + } + dynamic "startup_probe" { + for_each = containers.value.startup_probe == null ? [] : [""] + content { + initial_delay_seconds = containers.value.startup_probe.initial_delay_seconds + timeout_seconds = containers.value.startup_probe.timeout_seconds + period_seconds = containers.value.startup_probe.period_seconds + failure_threshold = containers.value.startup_probe.failure_threshold + dynamic "http_get" { + for_each = containers.value.startup_probe.http_get == null ? [] : [""] + content { + path = containers.value.startup_probe.http_get.path + port = containers.value.startup_probe.http_get.port + dynamic "http_headers" { + for_each = coalesce(containers.value.startup_probe.http_get.http_headers, tomap({})) + content { + name = http_headers.key + value = http_headers.value + } + } + } + } + dynamic "tcp_socket" { + for_each = containers.value.startup_probe.tcp_socket == null ? [] : [""] + content { + port = containers.value.startup_probe.tcp_socket.port + } + } + dynamic "grpc" { + for_each = containers.value.startup_probe.grpc == null ? [] : [""] + content { + port = containers.value.startup_probe.grpc.port + service = containers.value.startup_probe.grpc.service + } + } + } + } + } + } + dynamic "volumes" { + for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null } + content { + name = volumes.key + dynamic "secret" { + for_each = volumes.value.secret == null ? [] : [""] + content { + secret = volumes.value.secret.name + default_mode = volumes.value.secret.default_mode + dynamic "items" { + for_each = volumes.value.secret.path == null ? [] : [""] + content { + path = volumes.value.secret.path + version = volumes.value.secret.version + mode = volumes.value.secret.mode + } + } + } + } + + dynamic "empty_dir" { + for_each = volumes.value.empty_dir_size == null ? [] : [""] + content { + medium = "MEMORY" + size_limit = volumes.value.empty_dir_size + } + } + dynamic "gcs" { + for_each = volumes.value.gcs == null ? [] : [""] + content { + bucket = volumes.value.gcs.bucket + read_only = volumes.value.gcs.is_read_only + } + } + dynamic "nfs" { + for_each = volumes.value.nfs == null ? [] : [""] + content { + server = volumes.value.nfs.server + path = volumes.value.nfs.path + read_only = volumes.value.nfs.is_read_only + } + } + } + } + # CloudSQL is the last volume in the list returned by API + dynamic "volumes" { + for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null } + content { + name = volumes.key + dynamic "cloud_sql_instance" { + for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""] + content { + instances = volumes.value.cloud_sql_instances + } + } + } + } + } + + lifecycle { + ignore_changes = [ + build_config, + client, + client_version, + template[0].annotations["run.googleapis.com/operation-id"], + template[0].containers, + template[0].labels + ] + } +} diff --git a/modules/cloud-run-v2/service.tf b/modules/cloud-run-v2/service.tf index 0d0de99e4..bb15a9ee9 100644 --- a/modules/cloud-run-v2/service.tf +++ b/modules/cloud-run-v2/service.tf @@ -14,516 +14,13 @@ * limitations under the License. */ -resource "google_cloud_run_v2_service" "service" { - count = var.type == "SERVICE" && var.managed_revision ? 1 : 0 - provider = google-beta - project = var.project_id - location = var.region - name = var.name - ingress = var.service_config.ingress - invoker_iam_disabled = var.service_config.invoker_iam_disabled - labels = var.labels - launch_stage = var.launch_stage - custom_audiences = var.service_config.custom_audiences - deletion_protection = var.deletion_protection - iap_enabled = var.service_config.iap_config != null - - template { - labels = var.revision.labels - encryption_key = var.encryption_key - revision = local.revision_name - execution_environment = ( - var.service_config.gen2_execution_environment - ? "EXECUTION_ENVIRONMENT_GEN2" : "EXECUTION_ENVIRONMENT_GEN1" - ) - gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled - max_instance_request_concurrency = var.service_config.max_concurrency - dynamic "node_selector" { - for_each = var.revision.node_selector == null ? [] : [""] - content { - accelerator = var.revision.node_selector.accelerator - } - } - dynamic "scaling" { - for_each = var.service_config.scaling == null ? [] : [""] - content { - max_instance_count = var.service_config.scaling.max_instance_count - min_instance_count = var.service_config.scaling.min_instance_count - } - } - dynamic "vpc_access" { - for_each = local.connector == null ? [] : [""] - content { - connector = local.connector - egress = try(var.revision.vpc_access.egress, null) - } - } - dynamic "vpc_access" { - for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""] - content { - egress = var.revision.vpc_access.egress - network_interfaces { - subnetwork = var.revision.vpc_access.subnet - network = var.revision.vpc_access.network - tags = var.revision.vpc_access.tags - } - } - } - timeout = var.service_config.timeout - service_account = local.service_account_email - dynamic "containers" { - for_each = var.containers - content { - name = containers.key - image = containers.value.image - depends_on = containers.value.depends_on - command = containers.value.command - args = containers.value.args - dynamic "env" { - for_each = coalesce(containers.value.env, tomap({})) - content { - name = env.key - value = env.value - } - } - dynamic "env" { - for_each = coalesce(containers.value.env_from_key, tomap({})) - content { - name = env.key - value_source { - secret_key_ref { - secret = env.value.secret - version = env.value.version - } - } - } - } - dynamic "resources" { - for_each = containers.value.resources == null ? [] : [""] - content { - limits = containers.value.resources.limits - cpu_idle = containers.value.resources.cpu_idle - startup_cpu_boost = containers.value.resources.startup_cpu_boost - } - } - dynamic "ports" { - for_each = coalesce(containers.value.ports, tomap({})) - content { - container_port = ports.value.container_port - name = ports.value.name - } - } - dynamic "volume_mounts" { - for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" } - content { - name = volume_mounts.key - mount_path = volume_mounts.value - } - } - # CloudSQL is the last mount in the list returned by API - dynamic "volume_mounts" { - for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" } - content { - name = volume_mounts.key - mount_path = volume_mounts.value - } - } - dynamic "liveness_probe" { - for_each = containers.value.liveness_probe == null ? [] : [""] - content { - initial_delay_seconds = containers.value.liveness_probe.initial_delay_seconds - timeout_seconds = containers.value.liveness_probe.timeout_seconds - period_seconds = containers.value.liveness_probe.period_seconds - failure_threshold = containers.value.liveness_probe.failure_threshold - dynamic "http_get" { - for_each = containers.value.liveness_probe.http_get == null ? [] : [""] - content { - path = containers.value.liveness_probe.http_get.path - port = containers.value.liveness_probe.http_get.port - dynamic "http_headers" { - for_each = coalesce(containers.value.liveness_probe.http_get.http_headers, tomap({})) - content { - name = http_headers.key - value = http_headers.value - } - } - } - } - dynamic "grpc" { - for_each = containers.value.liveness_probe.grpc == null ? [] : [""] - content { - port = containers.value.liveness_probe.grpc.port - service = containers.value.liveness_probe.grpc.service - } - } - } - } - dynamic "startup_probe" { - for_each = containers.value.startup_probe == null ? [] : [""] - content { - initial_delay_seconds = containers.value.startup_probe.initial_delay_seconds - timeout_seconds = containers.value.startup_probe.timeout_seconds - period_seconds = containers.value.startup_probe.period_seconds - failure_threshold = containers.value.startup_probe.failure_threshold - dynamic "http_get" { - for_each = containers.value.startup_probe.http_get == null ? [] : [""] - content { - path = containers.value.startup_probe.http_get.path - port = containers.value.startup_probe.http_get.port - dynamic "http_headers" { - for_each = coalesce(containers.value.startup_probe.http_get.http_headers, tomap({})) - content { - name = http_headers.key - value = http_headers.value - } - } - } - } - dynamic "tcp_socket" { - for_each = containers.value.startup_probe.tcp_socket == null ? [] : [""] - content { - port = containers.value.startup_probe.tcp_socket.port - } - } - dynamic "grpc" { - for_each = containers.value.startup_probe.grpc == null ? [] : [""] - content { - port = containers.value.startup_probe.grpc.port - service = containers.value.startup_probe.grpc.service - } - } - } - } - } - } - dynamic "volumes" { - for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null } - content { - name = volumes.key - dynamic "secret" { - for_each = volumes.value.secret == null ? [] : [""] - content { - secret = volumes.value.secret.name - default_mode = volumes.value.secret.default_mode - dynamic "items" { - for_each = volumes.value.secret.path == null ? [] : [""] - content { - path = volumes.value.secret.path - version = volumes.value.secret.version - mode = volumes.value.secret.mode - } - } - } - } - - dynamic "empty_dir" { - for_each = volumes.value.empty_dir_size == null ? [] : [""] - content { - medium = "MEMORY" - size_limit = volumes.value.empty_dir_size - } - } - dynamic "gcs" { - for_each = volumes.value.gcs == null ? [] : [""] - content { - bucket = volumes.value.gcs.bucket - read_only = volumes.value.gcs.is_read_only - } - } - dynamic "nfs" { - for_each = volumes.value.nfs == null ? [] : [""] - content { - server = volumes.value.nfs.server - path = volumes.value.nfs.path - read_only = volumes.value.nfs.is_read_only - } - } - } - } - # CloudSQL is the last volume in the list returned by API - dynamic "volumes" { - for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null } - content { - name = volumes.key - dynamic "cloud_sql_instance" { - for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""] - content { - instances = volumes.value.cloud_sql_instances - } - } - } - } - } - - lifecycle { - ignore_changes = [ - client, - client_version, - template[0].annotations["run.googleapis.com/operation-id"], - ] - } -} - -resource "google_cloud_run_v2_service" "service_unmanaged" { - count = var.type == "SERVICE" && !var.managed_revision ? 1 : 0 - provider = google-beta - project = var.project_id - location = var.region - name = var.name - ingress = var.service_config.ingress - invoker_iam_disabled = var.service_config.invoker_iam_disabled - labels = var.labels - launch_stage = var.launch_stage - custom_audiences = var.service_config.custom_audiences - deletion_protection = var.deletion_protection - iap_enabled = var.service_config.iap_config != null - - template { - labels = var.revision.labels - encryption_key = var.encryption_key - revision = local.revision_name - execution_environment = ( - var.service_config.gen2_execution_environment - ? "EXECUTION_ENVIRONMENT_GEN2" : "EXECUTION_ENVIRONMENT_GEN1" - ) - gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled - max_instance_request_concurrency = var.service_config.max_concurrency - dynamic "node_selector" { - for_each = var.revision.node_selector == null ? [] : [""] - content { - accelerator = var.revision.node_selector.accelerator - } - } - dynamic "scaling" { - for_each = var.service_config.scaling == null ? [] : [""] - content { - max_instance_count = var.service_config.scaling.max_instance_count - min_instance_count = var.service_config.scaling.min_instance_count - } - } - dynamic "vpc_access" { - for_each = local.connector == null ? [] : [""] - content { - connector = local.connector - egress = try(var.revision.vpc_access.egress, null) - } - } - dynamic "vpc_access" { - for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""] - content { - egress = var.revision.vpc_access.egress - network_interfaces { - subnetwork = var.revision.vpc_access.subnet - network = var.revision.vpc_access.network - tags = var.revision.vpc_access.tags - } - } - } - timeout = var.service_config.timeout - service_account = local.service_account_email - dynamic "containers" { - for_each = var.containers - content { - name = containers.key - image = containers.value.image - depends_on = containers.value.depends_on - command = containers.value.command - args = containers.value.args - dynamic "env" { - for_each = coalesce(containers.value.env, tomap({})) - content { - name = env.key - value = env.value - } - } - dynamic "env" { - for_each = coalesce(containers.value.env_from_key, tomap({})) - content { - name = env.key - value_source { - secret_key_ref { - secret = env.value.secret - version = env.value.version - } - } - } - } - dynamic "resources" { - for_each = containers.value.resources == null ? [] : [""] - content { - limits = containers.value.resources.limits - cpu_idle = containers.value.resources.cpu_idle - startup_cpu_boost = containers.value.resources.startup_cpu_boost - } - } - dynamic "ports" { - for_each = coalesce(containers.value.ports, tomap({})) - content { - container_port = ports.value.container_port - name = ports.value.name - } - } - dynamic "volume_mounts" { - for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" } - content { - name = volume_mounts.key - mount_path = volume_mounts.value - } - } - # CloudSQL is the last mount in the list returned by API - dynamic "volume_mounts" { - for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" } - content { - name = volume_mounts.key - mount_path = volume_mounts.value - } - } - dynamic "liveness_probe" { - for_each = containers.value.liveness_probe == null ? [] : [""] - content { - initial_delay_seconds = containers.value.liveness_probe.initial_delay_seconds - timeout_seconds = containers.value.liveness_probe.timeout_seconds - period_seconds = containers.value.liveness_probe.period_seconds - failure_threshold = containers.value.liveness_probe.failure_threshold - dynamic "http_get" { - for_each = containers.value.liveness_probe.http_get == null ? [] : [""] - content { - path = containers.value.liveness_probe.http_get.path - port = containers.value.liveness_probe.http_get.port - dynamic "http_headers" { - for_each = coalesce(containers.value.liveness_probe.http_get.http_headers, tomap({})) - content { - name = http_headers.key - value = http_headers.value - } - } - } - } - dynamic "grpc" { - for_each = containers.value.liveness_probe.grpc == null ? [] : [""] - content { - port = containers.value.liveness_probe.grpc.port - service = containers.value.liveness_probe.grpc.service - } - } - } - } - dynamic "startup_probe" { - for_each = containers.value.startup_probe == null ? [] : [""] - content { - initial_delay_seconds = containers.value.startup_probe.initial_delay_seconds - timeout_seconds = containers.value.startup_probe.timeout_seconds - period_seconds = containers.value.startup_probe.period_seconds - failure_threshold = containers.value.startup_probe.failure_threshold - dynamic "http_get" { - for_each = containers.value.startup_probe.http_get == null ? [] : [""] - content { - path = containers.value.startup_probe.http_get.path - port = containers.value.startup_probe.http_get.port - dynamic "http_headers" { - for_each = coalesce(containers.value.startup_probe.http_get.http_headers, tomap({})) - content { - name = http_headers.key - value = http_headers.value - } - } - } - } - dynamic "tcp_socket" { - for_each = containers.value.startup_probe.tcp_socket == null ? [] : [""] - content { - port = containers.value.startup_probe.tcp_socket.port - } - } - dynamic "grpc" { - for_each = containers.value.startup_probe.grpc == null ? [] : [""] - content { - port = containers.value.startup_probe.grpc.port - service = containers.value.startup_probe.grpc.service - } - } - } - } - } - } - dynamic "volumes" { - for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null } - content { - name = volumes.key - dynamic "secret" { - for_each = volumes.value.secret == null ? [] : [""] - content { - secret = volumes.value.secret.name - default_mode = volumes.value.secret.default_mode - dynamic "items" { - for_each = volumes.value.secret.path == null ? [] : [""] - content { - path = volumes.value.secret.path - version = volumes.value.secret.version - mode = volumes.value.secret.mode - } - } - } - } - - dynamic "empty_dir" { - for_each = volumes.value.empty_dir_size == null ? [] : [""] - content { - medium = "MEMORY" - size_limit = volumes.value.empty_dir_size - } - } - dynamic "gcs" { - for_each = volumes.value.gcs == null ? [] : [""] - content { - bucket = volumes.value.gcs.bucket - read_only = volumes.value.gcs.is_read_only - } - } - dynamic "nfs" { - for_each = volumes.value.nfs == null ? [] : [""] - content { - server = volumes.value.nfs.server - path = volumes.value.nfs.path - read_only = volumes.value.nfs.is_read_only - } - } - } - } - # CloudSQL is the last volume in the list returned by API - dynamic "volumes" { - for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null } - content { - name = volumes.key - dynamic "cloud_sql_instance" { - for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""] - content { - instances = volumes.value.cloud_sql_instances - } - } - } - } - } - - lifecycle { - ignore_changes = [ - build_config, - client, - client_version, - template[0].annotations["run.googleapis.com/operation-id"], - template[0].containers, - template[0].labels - ] - } -} - resource "google_cloud_run_v2_service_iam_binding" "binding" { for_each = var.type == "SERVICE" ? var.iam : {} project = local.resource.project location = local.resource.location name = local.resource.name - role = each.key - members = each.value + role = lookup(local.ctx.custom_roles, each.key, each.key) + members = [for member in each.value : lookup(local.ctx.iam_principals, member, member)] } resource "google_iap_web_cloud_run_service_iam_member" "member" { @@ -532,7 +29,7 @@ resource "google_iap_web_cloud_run_service_iam_member" "member" { location = local.resource.location cloud_run_service_name = local.resource.name role = "roles/iap.httpsResourceAccessor" - member = each.key + member = lookup(local.ctx.iam_principals, each.key, each.key) } resource "google_iap_web_cloud_run_service_iam_binding" "binding" { @@ -544,5 +41,78 @@ resource "google_iap_web_cloud_run_service_iam_binding" "binding" { location = local.resource.location cloud_run_service_name = local.resource.name role = "roles/iap.httpsResourceAccessor" - members = var.service_config.iap_config.iam + members = [for member in var.service_config.iap_config.iam : lookup(local.ctx.iam_principals, member, member)] +} + +# Event ARC for Cloud Run services +resource "google_eventarc_trigger" "audit_log_triggers" { + for_each = coalesce(var.service_config.eventarc_triggers.audit_log, tomap({})) + name = "audit-log-${each.key}" + location = local.resource.location + project = local.resource.project + matching_criteria { + attribute = "type" + value = "google.cloud.audit.log.v1.written" + } + matching_criteria { + attribute = "serviceName" + value = each.value.service + } + matching_criteria { + attribute = "methodName" + value = each.value.method + } + destination { + cloud_run_service { + service = local.resource.name + region = local.resource.location + } + } + service_account = var.service_config.eventarc_triggers.service_account_email +} + +resource "google_eventarc_trigger" "pubsub_triggers" { + for_each = coalesce(var.service_config.eventarc_triggers.pubsub, tomap({})) + name = "pubsub-${each.key}" + location = local.resource.location + project = local.resource.project + matching_criteria { + attribute = "type" + value = "google.cloud.pubsub.topic.v1.messagePublished" + } + transport { + pubsub { + topic = each.value + } + } + destination { + cloud_run_service { + service = local.resource.name + region = local.resource.location + } + } + service_account = var.service_config.eventarc_triggers.service_account_email +} + +resource "google_eventarc_trigger" "storage_triggers" { + for_each = coalesce(var.service_config.eventarc_triggers.storage, tomap({})) + name = "storage-${each.key}" + location = local.resource.location + project = local.resource.project + matching_criteria { + attribute = "type" + value = "google.cloud.storage.object.v1.finalized" + } + matching_criteria { + attribute = "bucket" + value = each.value.bucket + } + destination { + cloud_run_service { + service = local.resource.name + region = local.resource.location + path = try(each.value.path, null) + } + } + service_account = var.service_config.eventarc_triggers.service_account_email } diff --git a/modules/cloud-run-v2/serviceaccount.tf b/modules/cloud-run-v2/serviceaccount.tf new file mode 100644 index 000000000..46c7499e4 --- /dev/null +++ b/modules/cloud-run-v2/serviceaccount.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 { + service_account_email = ( + var.service_account_config.create + ? google_service_account.service_account[0].email # use managed SA, when creating + : (var.service_account_config.email == null ? null # set to null, if no email provided + : lookup( # lookup SA in context + local.ctx.iam_principals, + var.service_account_config.email, + var.service_account_config.email + ) + ) + ) + service_account_roles = [ + for role in var.service_account_config.roles + : lookup(local.ctx.custom_roles, role, role) + ] +} + +resource "google_service_account" "service_account" { + count = var.service_account_config.create ? 1 : 0 + project = local.project_id + account_id = coalesce(var.service_account_config.name, var.name) + display_name = coalesce( + var.service_account_config.display_name, + var.service_account_config.name, + var.name + ) +} + +resource "google_project_iam_member" "default" { + for_each = ( + var.service_account_config.create + ? toset(local.service_account_roles) + : toset([]) + ) + role = each.key + project = local.project_id + member = google_service_account.service_account[0].member +} diff --git a/modules/cloud-run-v2/tags.tf b/modules/cloud-run-v2/tags.tf index 67b3c68a4..b461d1388 100644 --- a/modules/cloud-run-v2/tags.tf +++ b/modules/cloud-run-v2/tags.tf @@ -25,8 +25,8 @@ locals { resource "google_tags_location_tag_binding" "binding" { for_each = var.tag_bindings parent = ( - "//run.googleapis.com/projects/${var.project_id}/locations/${var.region}/${local.resource_types[var.type]}/${local.resource.name}" + "//run.googleapis.com/projects/${local.project_id}/locations/${local.location}/${local.resource_types[var.type]}/${local.resource.name}" ) - tag_value = each.value - location = var.region + tag_value = lookup(local.ctx.tag_values, each.value, each.value) + location = local.location } diff --git a/modules/cloud-run-v2/variables-serviceaccount.tf b/modules/cloud-run-v2/variables-serviceaccount.tf new file mode 100644 index 000000000..878feed9f --- /dev/null +++ b/modules/cloud-run-v2/variables-serviceaccount.tf @@ -0,0 +1,31 @@ +/** + * 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 "service_account_config" { + description = "Service account configurations." + type = object({ + create = optional(bool, true) + display_name = optional(string) + email = optional(string) + name = optional(string) + roles = optional(list(string), [ + "roles/logging.logWriter", + "roles/monitoring.metricWriter" + ]) + }) + nullable = false + default = {} +} diff --git a/modules/cloud-run-v2/variables-vpcconnector.tf b/modules/cloud-run-v2/variables-vpcconnector.tf index 0ca5ea2cd..e45ebe20e 100644 --- a/modules/cloud-run-v2/variables-vpcconnector.tf +++ b/modules/cloud-run-v2/variables-vpcconnector.tf @@ -15,7 +15,7 @@ */ variable "vpc_connector_create" { - description = "Populate this to create a Serverless VPC Access connector." + description = "VPC connector network configuration. Must be provided if new VPC connector is being created." type = object({ ip_cidr_range = optional(string) machine_type = optional(string) @@ -37,4 +37,12 @@ variable "vpc_connector_create" { }), {}) }) default = null + validation { + condition = ( + var.vpc_connector_create == null || + try(var.vpc_connector_create.instances, null) != null || + try(var.vpc_connector_create.throughput, null) != null + ) + error_message = "VPC connector must specify either instances or throughput." + } } diff --git a/modules/cloud-run-v2/variables.tf b/modules/cloud-run-v2/variables.tf index 4472e94d3..2f78c9ee2 100644 --- a/modules/cloud-run-v2/variables.tf +++ b/modules/cloud-run-v2/variables.tf @@ -94,6 +94,24 @@ variable "containers" { } } +variable "context" { + description = "Context-specific interpolations." + type = object({ + condition_vars = optional(map(map(string)), {}) # not needed here? + cidr_ranges = optional(map(string), {}) + custom_roles = optional(map(string), {}) + iam_principals = optional(map(string), {}) + kms_keys = optional(map(string), {}) + locations = optional(map(string), {}) + networks = optional(map(string), {}) + project_ids = optional(map(string), {}) + subnets = optional(map(string), {}) + tag_values = optional(map(string), {}) + }) + nullable = false + default = {} +} + variable "deletion_protection" { description = "Deletion protection setting for this Cloud Run service." type = string @@ -231,18 +249,12 @@ variable "revision" { ) error_message = "When providing vpc_access.network provide also vpc_access.subnet." } -} - -variable "service_account" { - description = "Service account email. Unused if service account is auto-created." - type = string - default = null -} - -variable "service_account_create" { - description = "Auto-create service account." - type = bool - default = false + validation { + condition = try(var.revision.vpc_access.connector, null) == null || ( + try(var.revision.vpc_access.connector, null) != null && var.vpc_connector_create == null + ) + error_message = "Either provide connector to create in var.vpc_connector_create or provide externally managed connector in var.revision.vpc_access.connector" + } } variable "service_config" { diff --git a/modules/cloud-run-v2/vpcconnector.tf b/modules/cloud-run-v2/vpcconnector.tf index ea135c6d6..2c449132a 100644 --- a/modules/cloud-run-v2/vpcconnector.tf +++ b/modules/cloud-run-v2/vpcconnector.tf @@ -14,17 +14,51 @@ * limitations under the License. */ +locals { + _connector_subnet_name_ctx = ( + try(var.vpc_connector_create.subnet.name, null) == null ? false : + contains(keys(local.ctx.subnets), var.vpc_connector_create.subnet.name) + ) + # if you pass the subnet, you must pass only the name, not the whole id + _connector_subnet_name = ( + local._connector_subnet_name_ctx + ? reverse(split("/", local.ctx.subnets[var.vpc_connector_create.subnet.name]))[0] + : try(var.vpc_connector_create.subnet.name, null) + ) + # if project is not provided, but subnet is coming from context, use project from subnet id in context + # and avoid lookups using null project + _connector_subnet_project_input = try(var.vpc_connector_create.subnet.project_id, null) + _connector_subnet_project = ( + local._connector_subnet_project_input == null + ? ( + local._connector_subnet_name_ctx + ? split("/", local.ctx.subnets[var.vpc_connector_create.subnet.name])[1] + : null + ) + : lookup( + local.ctx.project_ids, local._connector_subnet_project_input, + local._connector_subnet_project_input + ) + ) +} + resource "google_vpc_access_connector" "connector" { count = var.vpc_connector_create != null ? 1 : 0 - project = var.project_id + project = local.project_id name = ( var.vpc_connector_create.name != null ? var.vpc_connector_create.name : var.name ) - region = var.region - ip_cidr_range = var.vpc_connector_create.ip_cidr_range - network = var.vpc_connector_create.network + region = local.location + ip_cidr_range = var.vpc_connector_create.ip_cidr_range == null ? null : lookup( + local.ctx.cidr_ranges, var.vpc_connector_create.ip_cidr_range, + var.vpc_connector_create.ip_cidr_range + ) + network = var.vpc_connector_create.network == null ? null : lookup( + local.ctx.networks, var.vpc_connector_create.network, + var.vpc_connector_create.network + ) machine_type = var.vpc_connector_create.machine_type max_instances = var.vpc_connector_create.instances.max max_throughput = var.vpc_connector_create.throughput.max @@ -33,9 +67,8 @@ resource "google_vpc_access_connector" "connector" { dynamic "subnet" { for_each = var.vpc_connector_create.subnet.name == null ? [] : [""] content { - name = var.vpc_connector_create.subnet.name - project_id = var.vpc_connector_create.subnet.project_id + name = local._connector_subnet_name + project_id = local._connector_subnet_project } } } - diff --git a/modules/cloud-run-v2/workerpool-managed.tf b/modules/cloud-run-v2/workerpool-managed.tf new file mode 100644 index 000000000..19bacb034 --- /dev/null +++ b/modules/cloud-run-v2/workerpool-managed.tf @@ -0,0 +1,190 @@ +/** + * 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_cloud_run_v2_worker_pool" "default_managed" { + count = var.type == "WORKERPOOL" && var.managed_revision ? 1 : 0 + provider = google-beta + project = local.project_id + location = local.location + name = var.name + labels = var.labels + launch_stage = var.launch_stage + deletion_protection = var.deletion_protection + + dynamic "scaling" { + for_each = var.workerpool_config.scaling == null ? [] : [""] + content { + scaling_mode = var.workerpool_config.scaling.mode + max_instance_count = var.workerpool_config.scaling.max_instance_count + min_instance_count = var.workerpool_config.scaling.min_instance_count + manual_instance_count = var.workerpool_config.scaling.manual_instance_count + } + } + + template { + labels = var.revision.labels + encryption_key = var.encryption_key + revision = local.revision_name + + gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled + dynamic "node_selector" { + for_each = var.revision.node_selector == null ? [] : [""] + content { + accelerator = var.revision.node_selector.accelerator + } + } + # Serverless VPC connector is not supported + # dynamic "vpc_access" { + # for_each = local.connector == null ? [] : [""] + # content { + # connector = local.connector + # egress = try(var.revision.vpc_access.egress, null) + # } + # } + dynamic "vpc_access" { + for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""] + content { + egress = var.revision.vpc_access.egress + network_interfaces { + subnetwork = var.revision.vpc_access.subnet == null ? null : lookup( + local.ctx.subnets, var.revision.vpc_access.subnet, + var.revision.vpc_access.subnet + ) + network = var.revision.vpc_access.network == null ? null : lookup( + local.ctx.networks, var.revision.vpc_access.network, + var.revision.vpc_access.network + ) + tags = var.revision.vpc_access.tags + } + } + } + service_account = local.service_account_email + dynamic "containers" { + for_each = var.containers + content { + name = containers.key + image = containers.value.image + command = containers.value.command + args = containers.value.args + dynamic "env" { + for_each = coalesce(containers.value.env, tomap({})) + content { + name = env.key + value = env.value + } + } + dynamic "env" { + for_each = coalesce(containers.value.env_from_key, tomap({})) + content { + name = env.key + value_source { + secret_key_ref { + secret = env.value.secret + version = env.value.version + } + } + } + } + dynamic "resources" { + for_each = containers.value.resources == null ? [] : [""] + content { + limits = containers.value.resources.limits + } + } + dynamic "volume_mounts" { + for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" } + content { + name = volume_mounts.key + mount_path = volume_mounts.value + } + } + # CloudSQL is the last mount in the list returned by API + dynamic "volume_mounts" { + for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" } + content { + name = volume_mounts.key + mount_path = volume_mounts.value + } + } + } + } + dynamic "volumes" { + for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null } + content { + name = volumes.key + dynamic "secret" { + for_each = volumes.value.secret == null ? [] : [""] + content { + secret = volumes.value.secret.name + default_mode = volumes.value.secret.default_mode + dynamic "items" { + for_each = volumes.value.secret.path == null ? [] : [""] + content { + path = volumes.value.secret.path + version = volumes.value.secret.version + mode = volumes.value.secret.mode + } + } + } + } + + dynamic "empty_dir" { + for_each = volumes.value.empty_dir_size == null ? [] : [""] + content { + medium = "MEMORY" + size_limit = volumes.value.empty_dir_size + } + } + dynamic "gcs" { + for_each = volumes.value.gcs == null ? [] : [""] + content { + bucket = volumes.value.gcs.bucket + read_only = volumes.value.gcs.is_read_only + } + } + dynamic "nfs" { + for_each = volumes.value.nfs == null ? [] : [""] + content { + server = volumes.value.nfs.server + path = volumes.value.nfs.path + read_only = volumes.value.nfs.is_read_only + } + } + } + } + # CloudSQL is the last volume in the list returned by API + dynamic "volumes" { + for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null } + content { + name = volumes.key + dynamic "cloud_sql_instance" { + for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""] + content { + instances = volumes.value.cloud_sql_instances + } + } + } + } + } + + lifecycle { + ignore_changes = [ + client, + client_version, + template[0].annotations["run.googleapis.com/operation-id"], + ] + } +} diff --git a/modules/cloud-run-v2/workerpool-unmanaged.tf b/modules/cloud-run-v2/workerpool-unmanaged.tf new file mode 100644 index 000000000..5f05eca58 --- /dev/null +++ b/modules/cloud-run-v2/workerpool-unmanaged.tf @@ -0,0 +1,193 @@ +/** + * 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_cloud_run_v2_worker_pool" "default_unmanaged" { + count = var.type == "WORKERPOOL" && !var.managed_revision ? 1 : 0 + provider = google-beta + project = local.project_id + location = local.location + name = var.name + labels = var.labels + launch_stage = var.launch_stage + deletion_protection = var.deletion_protection + + dynamic "scaling" { + for_each = var.workerpool_config.scaling == null ? [] : [""] + content { + scaling_mode = var.workerpool_config.scaling.mode + max_instance_count = var.workerpool_config.scaling.max_instance_count + min_instance_count = var.workerpool_config.scaling.min_instance_count + manual_instance_count = var.workerpool_config.scaling.manual_instance_count + } + } + + template { + labels = var.revision.labels + encryption_key = var.encryption_key + revision = local.revision_name + + gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled + dynamic "node_selector" { + for_each = var.revision.node_selector == null ? [] : [""] + content { + accelerator = var.revision.node_selector.accelerator + } + } + + # Serverless VPC connector is not supported + # dynamic "vpc_access" { + # for_each = local.connector == null ? [] : [""] + # content { + # connector = local.connector + # egress = try(var.revision.vpc_access.egress, null) + # } + # } + dynamic "vpc_access" { + for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""] + content { + egress = var.revision.vpc_access.egress + network_interfaces { + subnetwork = var.revision.vpc_access.subnet == null ? null : lookup( + local.ctx.subnets, var.revision.vpc_access.subnet, + var.revision.vpc_access.subnet + ) + network = var.revision.vpc_access.network == null ? null : lookup( + local.ctx.networks, var.revision.vpc_access.network, + var.revision.vpc_access.network + ) + tags = var.revision.vpc_access.tags + } + } + } + service_account = local.service_account_email + dynamic "containers" { + for_each = var.containers + content { + name = containers.key + image = containers.value.image + command = containers.value.command + args = containers.value.args + dynamic "env" { + for_each = coalesce(containers.value.env, tomap({})) + content { + name = env.key + value = env.value + } + } + dynamic "env" { + for_each = coalesce(containers.value.env_from_key, tomap({})) + content { + name = env.key + value_source { + secret_key_ref { + secret = env.value.secret + version = env.value.version + } + } + } + } + dynamic "resources" { + for_each = containers.value.resources == null ? [] : [""] + content { + limits = containers.value.resources.limits + } + } + dynamic "volume_mounts" { + for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" } + content { + name = volume_mounts.key + mount_path = volume_mounts.value + } + } + # CloudSQL is the last mount in the list returned by API + dynamic "volume_mounts" { + for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" } + content { + name = volume_mounts.key + mount_path = volume_mounts.value + } + } + } + } + dynamic "volumes" { + for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null } + content { + name = volumes.key + dynamic "secret" { + for_each = volumes.value.secret == null ? [] : [""] + content { + secret = volumes.value.secret.name + default_mode = volumes.value.secret.default_mode + dynamic "items" { + for_each = volumes.value.secret.path == null ? [] : [""] + content { + path = volumes.value.secret.path + version = volumes.value.secret.version + mode = volumes.value.secret.mode + } + } + } + } + + dynamic "empty_dir" { + for_each = volumes.value.empty_dir_size == null ? [] : [""] + content { + medium = "MEMORY" + size_limit = volumes.value.empty_dir_size + } + } + dynamic "gcs" { + for_each = volumes.value.gcs == null ? [] : [""] + content { + bucket = volumes.value.gcs.bucket + read_only = volumes.value.gcs.is_read_only + } + } + dynamic "nfs" { + for_each = volumes.value.nfs == null ? [] : [""] + content { + server = volumes.value.nfs.server + path = volumes.value.nfs.path + read_only = volumes.value.nfs.is_read_only + } + } + } + } + # CloudSQL is the last volume in the list returned by API + dynamic "volumes" { + for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null } + content { + name = volumes.key + dynamic "cloud_sql_instance" { + for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""] + content { + instances = volumes.value.cloud_sql_instances + } + } + } + } + } + + lifecycle { + ignore_changes = [ + client, + client_version, + template[0].annotations["run.googleapis.com/operation-id"], + template[0].containers, + template[0].labels + ] + } +} diff --git a/modules/cloud-run-v2/workerpool.tf b/modules/cloud-run-v2/workerpool.tf index 121305640..22f85feee 100644 --- a/modules/cloud-run-v2/workerpool.tf +++ b/modules/cloud-run-v2/workerpool.tf @@ -14,351 +14,11 @@ * limitations under the License. */ -resource "google_cloud_run_v2_worker_pool" "default_managed" { - count = var.type == "WORKERPOOL" && var.managed_revision ? 1 : 0 - provider = google-beta - project = var.project_id - location = var.region - name = var.name - labels = var.labels - launch_stage = var.launch_stage - deletion_protection = var.deletion_protection - - dynamic "scaling" { - for_each = var.workerpool_config.scaling == null ? [] : [""] - content { - scaling_mode = var.workerpool_config.scaling.mode - max_instance_count = var.workerpool_config.scaling.max_instance_count - min_instance_count = var.workerpool_config.scaling.min_instance_count - manual_instance_count = var.workerpool_config.scaling.manual_instance_count - } - } - - template { - labels = var.revision.labels - encryption_key = var.encryption_key - revision = local.revision_name - - gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled - dynamic "node_selector" { - for_each = var.revision.node_selector == null ? [] : [""] - content { - accelerator = var.revision.node_selector.accelerator - } - } - # Serverless VPC connector is not supported - # dynamic "vpc_access" { - # for_each = local.connector == null ? [] : [""] - # content { - # connector = local.connector - # egress = try(var.revision.vpc_access.egress, null) - # } - # } - dynamic "vpc_access" { - for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""] - content { - egress = var.revision.vpc_access.egress - network_interfaces { - subnetwork = var.revision.vpc_access.subnet - network = var.revision.vpc_access.network - tags = var.revision.vpc_access.tags - } - } - } - service_account = local.service_account_email - dynamic "containers" { - for_each = var.containers - content { - name = containers.key - image = containers.value.image - command = containers.value.command - args = containers.value.args - dynamic "env" { - for_each = coalesce(containers.value.env, tomap({})) - content { - name = env.key - value = env.value - } - } - dynamic "env" { - for_each = coalesce(containers.value.env_from_key, tomap({})) - content { - name = env.key - value_source { - secret_key_ref { - secret = env.value.secret - version = env.value.version - } - } - } - } - dynamic "resources" { - for_each = containers.value.resources == null ? [] : [""] - content { - limits = containers.value.resources.limits - } - } - dynamic "volume_mounts" { - for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" } - content { - name = volume_mounts.key - mount_path = volume_mounts.value - } - } - # CloudSQL is the last mount in the list returned by API - dynamic "volume_mounts" { - for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" } - content { - name = volume_mounts.key - mount_path = volume_mounts.value - } - } - } - } - dynamic "volumes" { - for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null } - content { - name = volumes.key - dynamic "secret" { - for_each = volumes.value.secret == null ? [] : [""] - content { - secret = volumes.value.secret.name - default_mode = volumes.value.secret.default_mode - dynamic "items" { - for_each = volumes.value.secret.path == null ? [] : [""] - content { - path = volumes.value.secret.path - version = volumes.value.secret.version - mode = volumes.value.secret.mode - } - } - } - } - - dynamic "empty_dir" { - for_each = volumes.value.empty_dir_size == null ? [] : [""] - content { - medium = "MEMORY" - size_limit = volumes.value.empty_dir_size - } - } - dynamic "gcs" { - for_each = volumes.value.gcs == null ? [] : [""] - content { - bucket = volumes.value.gcs.bucket - read_only = volumes.value.gcs.is_read_only - } - } - dynamic "nfs" { - for_each = volumes.value.nfs == null ? [] : [""] - content { - server = volumes.value.nfs.server - path = volumes.value.nfs.path - read_only = volumes.value.nfs.is_read_only - } - } - } - } - # CloudSQL is the last volume in the list returned by API - dynamic "volumes" { - for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null } - content { - name = volumes.key - dynamic "cloud_sql_instance" { - for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""] - content { - instances = volumes.value.cloud_sql_instances - } - } - } - } - } - - lifecycle { - ignore_changes = [ - client, - client_version, - template[0].annotations["run.googleapis.com/operation-id"], - ] - } -} - -resource "google_cloud_run_v2_worker_pool" "default_unmanaged" { - count = var.type == "WORKERPOOL" && !var.managed_revision ? 1 : 0 - provider = google-beta - project = var.project_id - location = var.region - name = var.name - labels = var.labels - launch_stage = var.launch_stage - deletion_protection = var.deletion_protection - - dynamic "scaling" { - for_each = var.workerpool_config.scaling == null ? [] : [""] - content { - scaling_mode = var.workerpool_config.scaling.mode - max_instance_count = var.workerpool_config.scaling.max_instance_count - min_instance_count = var.workerpool_config.scaling.min_instance_count - manual_instance_count = var.workerpool_config.scaling.manual_instance_count - } - } - - template { - labels = var.revision.labels - encryption_key = var.encryption_key - revision = local.revision_name - - gpu_zonal_redundancy_disabled = var.revision.gpu_zonal_redundancy_disabled - dynamic "node_selector" { - for_each = var.revision.node_selector == null ? [] : [""] - content { - accelerator = var.revision.node_selector.accelerator - } - } - - # Serverless VPC connector is not supported - # dynamic "vpc_access" { - # for_each = local.connector == null ? [] : [""] - # content { - # connector = local.connector - # egress = try(var.revision.vpc_access.egress, null) - # } - # } - dynamic "vpc_access" { - for_each = var.revision.vpc_access.subnet == null && var.revision.vpc_access.network == null ? [] : [""] - content { - egress = var.revision.vpc_access.egress - network_interfaces { - subnetwork = var.revision.vpc_access.subnet - network = var.revision.vpc_access.network - tags = var.revision.vpc_access.tags - } - } - } - service_account = local.service_account_email - dynamic "containers" { - for_each = var.containers - content { - name = containers.key - image = containers.value.image - command = containers.value.command - args = containers.value.args - dynamic "env" { - for_each = coalesce(containers.value.env, tomap({})) - content { - name = env.key - value = env.value - } - } - dynamic "env" { - for_each = coalesce(containers.value.env_from_key, tomap({})) - content { - name = env.key - value_source { - secret_key_ref { - secret = env.value.secret - version = env.value.version - } - } - } - } - dynamic "resources" { - for_each = containers.value.resources == null ? [] : [""] - content { - limits = containers.value.resources.limits - } - } - dynamic "volume_mounts" { - for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k != "cloudsql" } - content { - name = volume_mounts.key - mount_path = volume_mounts.value - } - } - # CloudSQL is the last mount in the list returned by API - dynamic "volume_mounts" { - for_each = { for k, v in coalesce(containers.value.volume_mounts, tomap({})) : k => v if k == "cloudsql" } - content { - name = volume_mounts.key - mount_path = volume_mounts.value - } - } - } - } - dynamic "volumes" { - for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances == null } - content { - name = volumes.key - dynamic "secret" { - for_each = volumes.value.secret == null ? [] : [""] - content { - secret = volumes.value.secret.name - default_mode = volumes.value.secret.default_mode - dynamic "items" { - for_each = volumes.value.secret.path == null ? [] : [""] - content { - path = volumes.value.secret.path - version = volumes.value.secret.version - mode = volumes.value.secret.mode - } - } - } - } - - dynamic "empty_dir" { - for_each = volumes.value.empty_dir_size == null ? [] : [""] - content { - medium = "MEMORY" - size_limit = volumes.value.empty_dir_size - } - } - dynamic "gcs" { - for_each = volumes.value.gcs == null ? [] : [""] - content { - bucket = volumes.value.gcs.bucket - read_only = volumes.value.gcs.is_read_only - } - } - dynamic "nfs" { - for_each = volumes.value.nfs == null ? [] : [""] - content { - server = volumes.value.nfs.server - path = volumes.value.nfs.path - read_only = volumes.value.nfs.is_read_only - } - } - } - } - # CloudSQL is the last volume in the list returned by API - dynamic "volumes" { - for_each = { for k, v in var.volumes : k => v if v.cloud_sql_instances != null } - content { - name = volumes.key - dynamic "cloud_sql_instance" { - for_each = length(coalesce(volumes.value.cloud_sql_instances, [])) == 0 ? [] : [""] - content { - instances = volumes.value.cloud_sql_instances - } - } - } - } - } - lifecycle { - ignore_changes = [ - client, - client_version, - template[0].annotations["run.googleapis.com/operation-id"], - template[0].containers, - template[0].labels - ] - } -} - resource "google_cloud_run_v2_worker_pool_iam_binding" "binding" { for_each = var.type == "WORKERPOOL" ? var.iam : {} project = local.resource.project location = local.resource.location name = local.resource.name - role = each.key - members = each.value + role = lookup(local.ctx.custom_roles, each.key, each.key) + members = [for member in each.value : lookup(local.ctx.iam_principals, member, member)] } diff --git a/modules/gke-cluster-autopilot/README.md b/modules/gke-cluster-autopilot/README.md index bf74f9c88..179eedce7 100644 --- a/modules/gke-cluster-autopilot/README.md +++ b/modules/gke-cluster-autopilot/README.md @@ -268,25 +268,26 @@ module "cluster-1" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [location](variables.tf#L162) | Autopilot clusters are always regional. | string | ✓ | | -| [name](variables.tf#L241) | Cluster name. | string | ✓ | | -| [project_id](variables.tf#L274) | Cluster project ID. | string | ✓ | | -| [vpc_config](variables.tf#L290) | VPC-level configuration. | object({…}) | ✓ | | +| [location](variables.tf#L168) | Autopilot clusters are always regional. | string | ✓ | | +| [name](variables.tf#L247) | Cluster name. | string | ✓ | | +| [project_id](variables.tf#L280) | Cluster project ID. | string | ✓ | | +| [vpc_config](variables.tf#L296) | VPC-level configuration. | object({…}) | ✓ | | | [access_config](variables.tf#L17) | Control plane endpoint and nodes access configurations. | object({…}) | | {} | | [backup_configs](variables.tf#L45) | Configuration for Backup for GKE. | object({…}) | | {} | | [deletion_protection](variables.tf#L67) | Whether or not to allow Terraform to destroy the cluster. Unless this field is set to false in Terraform state, a terraform destroy or terraform apply that would delete the cluster will fail. | bool | | true | | [description](variables.tf#L74) | Cluster description. | string | | null | | [enable_addons](variables.tf#L80) | Addons enabled in the cluster (true means enabled). | object({…}) | | {} | | [enable_features](variables.tf#L94) | Enable cluster-level features. Certain features allow configuration. | object({…}) | | {} | -| [issue_client_certificate](variables.tf#L150) | Enable issuing client certificate. | bool | | false | -| [labels](variables.tf#L156) | Cluster resource labels. | map(string) | | null | -| [logging_config](variables.tf#L167) | Logging configuration. | object({…}) | | {} | -| [maintenance_config](variables.tf#L178) | Maintenance window configuration. | object({…}) | | {…} | -| [min_master_version](variables.tf#L201) | Minimum version of the master, defaults to the version of the most recent official release. | string | | null | -| [monitoring_config](variables.tf#L207) | Monitoring configuration. System metrics collection cannot be disabled. Control plane metrics are optional. Kube state metrics are optional. Google Cloud Managed Service for Prometheus is enabled by default. | object({…}) | | {} | -| [node_config](variables.tf#L246) | Configuration for nodes and nodepools. | object({…}) | | {} | -| [node_locations](variables.tf#L267) | Zones in which the cluster's nodes are located. | list(string) | | [] | -| [release_channel](variables.tf#L279) | Release channel for GKE upgrades. Clusters created in the Autopilot mode must use a release channel. Choose between \"RAPID\", \"REGULAR\", and \"STABLE\". | string | | "REGULAR" | +| [fleet_project](variables.tf#L150) | The name of the fleet host project where this cluster will be registered. | string | | null | +| [issue_client_certificate](variables.tf#L156) | Enable issuing client certificate. | bool | | false | +| [labels](variables.tf#L162) | Cluster resource labels. | map(string) | | null | +| [logging_config](variables.tf#L173) | Logging configuration. | object({…}) | | {} | +| [maintenance_config](variables.tf#L184) | Maintenance window configuration. | object({…}) | | {…} | +| [min_master_version](variables.tf#L207) | Minimum version of the master, defaults to the version of the most recent official release. | string | | null | +| [monitoring_config](variables.tf#L213) | Monitoring configuration. System metrics collection cannot be disabled. Control plane metrics are optional. Kube state metrics are optional. Google Cloud Managed Service for Prometheus is enabled by default. | object({…}) | | {} | +| [node_config](variables.tf#L252) | Configuration for nodes and nodepools. | object({…}) | | {} | +| [node_locations](variables.tf#L273) | Zones in which the cluster's nodes are located. | list(string) | | [] | +| [release_channel](variables.tf#L285) | Release channel for GKE upgrades. Clusters created in the Autopilot mode must use a release channel. Choose between \"RAPID\", \"REGULAR\", and \"STABLE\". | string | | "REGULAR" | ## Outputs @@ -296,11 +297,12 @@ module "cluster-1" { | [cluster](outputs.tf#L23) | Cluster resource. | ✓ | | [dns_endpoint](outputs.tf#L29) | Control plane DNS endpoint. | | | [endpoint](outputs.tf#L37) | Cluster endpoint. | | -| [id](outputs.tf#L42) | Fully qualified cluster ID. | | -| [location](outputs.tf#L47) | Cluster location. | | -| [master_version](outputs.tf#L52) | Master version. | | -| [name](outputs.tf#L57) | Cluster name. | | -| [notifications](outputs.tf#L62) | GKE Pub/Sub notifications topic. | | -| [self_link](outputs.tf#L67) | Cluster self link. | ✓ | -| [workload_identity_pool](outputs.tf#L73) | Workload identity pool. | | +| [fleet](outputs.tf#L42) | GKE Fleet Membership. | | +| [id](outputs.tf#L47) | Fully qualified cluster ID. | | +| [location](outputs.tf#L52) | Cluster location. | | +| [master_version](outputs.tf#L57) | Master version. | | +| [name](outputs.tf#L62) | Cluster name. | | +| [notifications](outputs.tf#L67) | GKE Pub/Sub notifications topic. | | +| [self_link](outputs.tf#L72) | Cluster self link. | ✓ | +| [workload_identity_pool](outputs.tf#L78) | Workload identity pool. | | diff --git a/modules/gke-cluster-autopilot/main.tf b/modules/gke-cluster-autopilot/main.tf index 9a03176f2..a55ef7ba7 100644 --- a/modules/gke-cluster-autopilot/main.tf +++ b/modules/gke-cluster-autopilot/main.tf @@ -118,6 +118,12 @@ resource "google_container_cluster" "cluster" { enabled_apis = var.enable_features.beta_apis } } + dynamic "fleet" { + for_each = var.fleet_project != null ? [""] : [] + content { + project = var.fleet_project + } + } dynamic "gateway_api_config" { for_each = var.enable_features.gateway_api ? [""] : [] content { diff --git a/modules/gke-cluster-autopilot/outputs.tf b/modules/gke-cluster-autopilot/outputs.tf index 81bdc2a0c..f6890b391 100644 --- a/modules/gke-cluster-autopilot/outputs.tf +++ b/modules/gke-cluster-autopilot/outputs.tf @@ -39,6 +39,11 @@ output "endpoint" { value = google_container_cluster.cluster.endpoint } +output "fleet" { + description = "GKE Fleet Membership." + value = google_container_cluster.cluster.endpoint +} + output "id" { description = "Fully qualified cluster ID." value = google_container_cluster.cluster.id diff --git a/modules/gke-cluster-autopilot/variables.tf b/modules/gke-cluster-autopilot/variables.tf index b24ddccdf..5fdfbcfed 100644 --- a/modules/gke-cluster-autopilot/variables.tf +++ b/modules/gke-cluster-autopilot/variables.tf @@ -147,6 +147,12 @@ variable "enable_features" { } } +variable "fleet_project" { + description = "The name of the fleet host project where this cluster will be registered." + type = string + default = null +} + variable "issue_client_certificate" { description = "Enable issuing client certificate." type = bool diff --git a/modules/gke-cluster-standard/README.md b/modules/gke-cluster-standard/README.md index 47176032d..12b8aa294 100644 --- a/modules/gke-cluster-standard/README.md +++ b/modules/gke-cluster-standard/README.md @@ -510,10 +510,10 @@ module "cluster-1" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [location](variables.tf#L282) | Cluster zone or region. | string | ✓ | | -| [name](variables.tf#L397) | Cluster name. | string | ✓ | | -| [project_id](variables.tf#L449) | Cluster project id. | string | ✓ | | -| [vpc_config](variables.tf#L460) | VPC-level configuration. | object({…}) | ✓ | | +| [location](variables.tf#L288) | Cluster zone or region. | string | ✓ | | +| [name](variables.tf#L403) | Cluster name. | string | ✓ | | +| [project_id](variables.tf#L455) | Cluster project id. | string | ✓ | | +| [vpc_config](variables.tf#L466) | VPC-level configuration. | object({…}) | ✓ | | | [access_config](variables.tf#L17) | Control plane endpoint and nodes access configurations. | object({…}) | | {} | | [backup_configs](variables.tf#L45) | Configuration for Backup for GKE. | object({…}) | | {} | | [cluster_autoscaling](variables.tf#L68) | Enable and configure limits for Node Auto-Provisioning with Cluster Autoscaler. | object({…}) | | null | @@ -522,17 +522,18 @@ module "cluster-1" { | [description](variables.tf#L173) | Cluster description. | string | | null | | [enable_addons](variables.tf#L179) | Addons enabled in the cluster (true means enabled). | object({…}) | | {} | | [enable_features](variables.tf#L201) | Enable cluster-level features. Certain features allow configuration. | object({…}) | | {} | -| [issue_client_certificate](variables.tf#L269) | Enable issuing client certificate. | bool | | false | -| [labels](variables.tf#L275) | Cluster resource labels. | map(string) | | {} | -| [logging_config](variables.tf#L287) | Logging configuration. | object({…}) | | {} | -| [maintenance_config](variables.tf#L308) | Maintenance window configuration. | object({…}) | | {…} | -| [max_pods_per_node](variables.tf#L331) | Maximum number of pods per node in this cluster. | number | | 110 | -| [min_master_version](variables.tf#L337) | Minimum version of the master, defaults to the version of the most recent official release. | string | | null | -| [monitoring_config](variables.tf#L343) | Monitoring configuration. Google Cloud Managed Service for Prometheus is enabled by default. | object({…}) | | {} | -| [node_config](variables.tf#L402) | Node-level configuration. | object({…}) | | {} | -| [node_locations](variables.tf#L425) | Zones in which the cluster's nodes are located. | list(string) | | [] | -| [node_pool_auto_config](variables.tf#L432) | Node pool configs that apply to auto-provisioned node pools in autopilot clusters and node auto-provisioning-enabled clusters. | object({…}) | | {} | -| [release_channel](variables.tf#L454) | Release channel for GKE upgrades. | string | | null | +| [fleet_project](variables.tf#L269) | The name of the fleet host project where this cluster will be registered. | string | | null | +| [issue_client_certificate](variables.tf#L275) | Enable issuing client certificate. | bool | | false | +| [labels](variables.tf#L281) | Cluster resource labels. | map(string) | | {} | +| [logging_config](variables.tf#L293) | Logging configuration. | object({…}) | | {} | +| [maintenance_config](variables.tf#L314) | Maintenance window configuration. | object({…}) | | {…} | +| [max_pods_per_node](variables.tf#L337) | Maximum number of pods per node in this cluster. | number | | 110 | +| [min_master_version](variables.tf#L343) | Minimum version of the master, defaults to the version of the most recent official release. | string | | null | +| [monitoring_config](variables.tf#L349) | Monitoring configuration. Google Cloud Managed Service for Prometheus is enabled by default. | object({…}) | | {} | +| [node_config](variables.tf#L408) | Node-level configuration. | object({…}) | | {} | +| [node_locations](variables.tf#L431) | Zones in which the cluster's nodes are located. | list(string) | | [] | +| [node_pool_auto_config](variables.tf#L438) | Node pool configs that apply to auto-provisioned node pools in autopilot clusters and node auto-provisioning-enabled clusters. | object({…}) | | {} | +| [release_channel](variables.tf#L460) | Release channel for GKE upgrades. | string | | null | ## Outputs @@ -542,11 +543,12 @@ module "cluster-1" { | [cluster](outputs.tf#L25) | Cluster resource. | ✓ | | [dns_endpoint](outputs.tf#L31) | Control plane DNS endpoint. | | | [endpoint](outputs.tf#L39) | Cluster endpoint. | | -| [id](outputs.tf#L44) | FUlly qualified cluster id. | | -| [location](outputs.tf#L49) | Cluster location. | | -| [master_version](outputs.tf#L54) | Master version. | | -| [name](outputs.tf#L59) | Cluster name. | | -| [notifications](outputs.tf#L64) | GKE PubSub notifications topic. | | -| [self_link](outputs.tf#L69) | Cluster self link. | ✓ | -| [workload_identity_pool](outputs.tf#L75) | Workload identity pool. | | +| [fleet](outputs.tf#L44) | GKE Fleet Membership. | | +| [id](outputs.tf#L49) | FUlly qualified cluster id. | | +| [location](outputs.tf#L54) | Cluster location. | | +| [master_version](outputs.tf#L59) | Master version. | | +| [name](outputs.tf#L64) | Cluster name. | | +| [notifications](outputs.tf#L69) | GKE PubSub notifications topic. | | +| [self_link](outputs.tf#L74) | Cluster self link. | ✓ | +| [workload_identity_pool](outputs.tf#L80) | Workload identity pool. | | diff --git a/modules/gke-cluster-standard/main.tf b/modules/gke-cluster-standard/main.tf index 56e2990ba..c50c6c55f 100644 --- a/modules/gke-cluster-standard/main.tf +++ b/modules/gke-cluster-standard/main.tf @@ -298,6 +298,12 @@ resource "google_container_cluster" "cluster" { enabled_apis = var.enable_features.beta_apis } } + dynamic "fleet" { + for_each = var.fleet_project != null ? [""] : [] + content { + project = var.fleet_project + } + } dynamic "gateway_api_config" { for_each = var.enable_features.gateway_api ? [""] : [] content { diff --git a/modules/gke-cluster-standard/outputs.tf b/modules/gke-cluster-standard/outputs.tf index bc6c8e9a4..b6829fd27 100644 --- a/modules/gke-cluster-standard/outputs.tf +++ b/modules/gke-cluster-standard/outputs.tf @@ -41,6 +41,11 @@ output "endpoint" { value = google_container_cluster.cluster.endpoint } +output "fleet" { + description = "GKE Fleet Membership." + value = google_container_cluster.cluster.endpoint +} + output "id" { description = "FUlly qualified cluster id." value = google_container_cluster.cluster.id diff --git a/modules/gke-cluster-standard/variables.tf b/modules/gke-cluster-standard/variables.tf index 09a0765c4..57ebf16c5 100644 --- a/modules/gke-cluster-standard/variables.tf +++ b/modules/gke-cluster-standard/variables.tf @@ -266,6 +266,12 @@ variable "enable_features" { } } +variable "fleet_project" { + description = "The name of the fleet host project where this cluster will be registered." + type = string + default = null +} + variable "issue_client_certificate" { description = "Enable issuing client certificate." type = bool diff --git a/modules/net-lb-app-ext/recipe-cloud-run-iap/README.md b/modules/net-lb-app-ext/recipe-cloud-run-iap/README.md index 19e99f2c7..13fc06d72 100644 --- a/modules/net-lb-app-ext/recipe-cloud-run-iap/README.md +++ b/modules/net-lb-app-ext/recipe-cloud-run-iap/README.md @@ -64,5 +64,5 @@ module "test" { "group:mygroup3@myorg.com" ] } -# tftest modules=6 resources=24 +# tftest modules=6 resources=26 ``` diff --git a/modules/net-lb-app-ext/recipe-cloud-run-iap/main.tf b/modules/net-lb-app-ext/recipe-cloud-run-iap/main.tf index a6ba4e3f9..2f4eb8d37 100644 --- a/modules/net-lb-app-ext/recipe-cloud-run-iap/main.tf +++ b/modules/net-lb-app-ext/recipe-cloud-run-iap/main.tf @@ -68,8 +68,7 @@ module "backend_service" { module.project.service_agents.iap.iam_email ] } - deletion_protection = false - service_account_create = true + deletion_protection = false } module "addresses" { diff --git a/tests/fast/stages/s0_org_setup/not-simple.tfvars b/tests/fast/stages/s0_org_setup/simple.tfvars similarity index 100% rename from tests/fast/stages/s0_org_setup/not-simple.tfvars rename to tests/fast/stages/s0_org_setup/simple.tfvars diff --git a/tests/fast/stages/s0_org_setup/not-simple.yaml b/tests/fast/stages/s0_org_setup/simple.yaml similarity index 100% rename from tests/fast/stages/s0_org_setup/not-simple.yaml rename to tests/fast/stages/s0_org_setup/simple.yaml diff --git a/tests/fast/stages/s0_org_setup/tftest.yaml b/tests/fast/stages/s0_org_setup/tftest.yaml index 3637945ae..67480c49a 100644 --- a/tests/fast/stages/s0_org_setup/tftest.yaml +++ b/tests/fast/stages/s0_org_setup/tftest.yaml @@ -15,9 +15,8 @@ module: fast/stages/0-org-setup tests: - # TODO: rename to simple once fast lint setup accepts extra dirs - not-simple: + simple: inventory: - - not-simple.yaml + - simple.yaml extra_dirs: - ../../../tests/fast/stages/s0_org_setup/data-simple diff --git a/tests/modules/agent_engine/examples/encryption.yaml b/tests/modules/agent_engine/examples/encryption.yaml index 1a4394c0b..d0c02a74c 100644 --- a/tests/modules/agent_engine/examples/encryption.yaml +++ b/tests/modules/agent_engine/examples/encryption.yaml @@ -28,12 +28,12 @@ values: member: serviceAccount:my-agent@project-id.iam.gserviceaccount.com project: project-id role: roles/viewer - module.agent_engine.google_service_account.default[0]: + module.agent_engine.google_service_account.service_account[0]: account_id: my-agent create_ignore_already_exists: null description: null disabled: false - display_name: Agent Engine my-agent. + display_name: my-agent email: my-agent@project-id.iam.gserviceaccount.com member: serviceAccount:my-agent@project-id.iam.gserviceaccount.com project: project-id diff --git a/tests/modules/agent_engine/examples/environment.yaml b/tests/modules/agent_engine/examples/environment.yaml index 03e012a0d..a92d3d1e6 100644 --- a/tests/modules/agent_engine/examples/environment.yaml +++ b/tests/modules/agent_engine/examples/environment.yaml @@ -28,12 +28,12 @@ values: member: serviceAccount:my-agent@project-id.iam.gserviceaccount.com project: project-id role: roles/viewer - module.agent_engine.google_service_account.default[0]: + module.agent_engine.google_service_account.service_account[0]: account_id: my-agent create_ignore_already_exists: null description: null disabled: false - display_name: Agent Engine my-agent. + display_name: my-agent email: my-agent@project-id.iam.gserviceaccount.com member: serviceAccount:my-agent@project-id.iam.gserviceaccount.com project: project-id diff --git a/tests/modules/agent_engine/examples/minimal-pickle.yaml b/tests/modules/agent_engine/examples/minimal-pickle.yaml index aef4d90f9..8cb0cd285 100644 --- a/tests/modules/agent_engine/examples/minimal-pickle.yaml +++ b/tests/modules/agent_engine/examples/minimal-pickle.yaml @@ -28,12 +28,12 @@ values: member: serviceAccount:my-agent@project-id.iam.gserviceaccount.com project: project-id role: roles/viewer - module.agent_engine.google_service_account.default[0]: + module.agent_engine.google_service_account.service_account[0]: account_id: my-agent create_ignore_already_exists: null description: null disabled: false - display_name: Agent Engine my-agent. + display_name: my-agent email: my-agent@project-id.iam.gserviceaccount.com member: serviceAccount:my-agent@project-id.iam.gserviceaccount.com project: project-id diff --git a/tests/modules/agent_engine/examples/minimal.yaml b/tests/modules/agent_engine/examples/minimal.yaml index 658bbe93b..ba1a04600 100644 --- a/tests/modules/agent_engine/examples/minimal.yaml +++ b/tests/modules/agent_engine/examples/minimal.yaml @@ -28,12 +28,12 @@ values: member: serviceAccount:my-agent@project-id.iam.gserviceaccount.com project: project-id role: roles/viewer - module.agent_engine.google_service_account.default[0]: + module.agent_engine.google_service_account.service_account[0]: account_id: my-agent create_ignore_already_exists: null description: null disabled: false - display_name: Agent Engine my-agent. + display_name: my-agent email: my-agent@project-id.iam.gserviceaccount.com member: serviceAccount:my-agent@project-id.iam.gserviceaccount.com project: project-id diff --git a/tests/modules/cloud_function_v1/context-subnet-project.tfvars b/tests/modules/cloud_function_v1/context-subnet-project.tfvars index 6428c97e1..937f899f5 100644 --- a/tests/modules/cloud_function_v1/context-subnet-project.tfvars +++ b/tests/modules/cloud_function_v1/context-subnet-project.tfvars @@ -43,8 +43,7 @@ service_account_config = { ] } vpc_connector = { - create = true - name = "connector_name" + name = "connector_name" } vpc_connector_create = { instances = { diff --git a/tests/modules/cloud_function_v1/context-subnet.tfvars b/tests/modules/cloud_function_v1/context-subnet.tfvars index 7512e3ab3..0fbe0ee6c 100644 --- a/tests/modules/cloud_function_v1/context-subnet.tfvars +++ b/tests/modules/cloud_function_v1/context-subnet.tfvars @@ -43,8 +43,7 @@ service_account_config = { ] } vpc_connector = { - create = true - name = "connector_name" + name = "connector_name" } vpc_connector_create = { instances = { diff --git a/tests/modules/cloud_function_v1/context.tfvars b/tests/modules/cloud_function_v1/context.tfvars index f6eb5e3e3..9f937b04a 100644 --- a/tests/modules/cloud_function_v1/context.tfvars +++ b/tests/modules/cloud_function_v1/context.tfvars @@ -43,8 +43,7 @@ service_account_config = { ] } vpc_connector = { - create = true - name = "connector_name" + name = "connector_name" } vpc_connector_create = { ip_cidr_range = "$cidr_ranges:test" diff --git a/tests/modules/cloud_function_v2/context-subnet-project.tfvars b/tests/modules/cloud_function_v2/context-subnet-project.tfvars index 6428c97e1..937f899f5 100644 --- a/tests/modules/cloud_function_v2/context-subnet-project.tfvars +++ b/tests/modules/cloud_function_v2/context-subnet-project.tfvars @@ -43,8 +43,7 @@ service_account_config = { ] } vpc_connector = { - create = true - name = "connector_name" + name = "connector_name" } vpc_connector_create = { instances = { diff --git a/tests/modules/cloud_function_v2/context-subnet.tfvars b/tests/modules/cloud_function_v2/context-subnet.tfvars index 7512e3ab3..0fbe0ee6c 100644 --- a/tests/modules/cloud_function_v2/context-subnet.tfvars +++ b/tests/modules/cloud_function_v2/context-subnet.tfvars @@ -43,8 +43,7 @@ service_account_config = { ] } vpc_connector = { - create = true - name = "connector_name" + name = "connector_name" } vpc_connector_create = { instances = { diff --git a/tests/modules/cloud_function_v2/context.tfvars b/tests/modules/cloud_function_v2/context.tfvars index f6eb5e3e3..9f937b04a 100644 --- a/tests/modules/cloud_function_v2/context.tfvars +++ b/tests/modules/cloud_function_v2/context.tfvars @@ -43,8 +43,7 @@ service_account_config = { ] } vpc_connector = { - create = true - name = "connector_name" + name = "connector_name" } vpc_connector_create = { ip_cidr_range = "$cidr_ranges:test" diff --git a/tests/modules/cloud_run_v2/context-subnet-project.tfvars b/tests/modules/cloud_run_v2/context-subnet-project.tfvars new file mode 100644 index 000000000..3fe9e76ca --- /dev/null +++ b/tests/modules/cloud_run_v2/context-subnet-project.tfvars @@ -0,0 +1,60 @@ +name = "test-cf-kms" +bucket_name = "bucket" +bundle_config = { + path = "gs://assets/sample-function.zip" +} +context = { + cidr_ranges = { + test = "10.10.20.0/28" + } + custom_roles = { + myrole_one = "organizations/366118655033/roles/myRoleOne" + } + iam_principals = { + mygroup = "group:test-group@example.com" + } + kms_keys = { + test = "projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute" + } + locations = { + ew8 = "europe-west8" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } + subnets = { + test = "projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce" + } +} +kms_key = "$kms_keys:test" +iam = { + "$custom_roles:myrole_one" = [ + "$iam_principals:mygroup" + ] +} +project_id = "$project_ids:test" +region = "$locations:ew8" +service_account_config = { + roles = [ + "$custom_roles:myrole_one" + ] +} +revision = { + vpc_access = { + egress_settings = "ALL_TRAFFIC" + } +} + +vpc_connector_create = { + instances = { + max = 10 + min = 3 + } + subnet = { + name = "$subnets:test" + project_id = "$project_ids:test" + } +} diff --git a/tests/modules/cloud_run_v2/context-subnet-project.yaml b/tests/modules/cloud_run_v2/context-subnet-project.yaml new file mode 100644 index 000000000..6ba64800a --- /dev/null +++ b/tests/modules/cloud_run_v2/context-subnet-project.yaml @@ -0,0 +1,88 @@ +# 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. + +values: + google_cloud_run_v2_service.service[0]: + annotations: null + binary_authorization: [] + build_config: [] + client: null + client_version: null + custom_audiences: null + default_uri_disabled: null + deletion_protection: true + description: null + effective_labels: + goog-terraform-provisioned: 'true' + iap_enabled: false + invoker_iam_disabled: false + labels: null + location: europe-west8 + multi_region_settings: [] + name: test-cf-kms + project: foo-test-0 + scaling: [] + template: + - annotations: null + containers: [] + encryption_key: null + execution_environment: EXECUTION_ENVIRONMENT_GEN1 + gpu_zonal_redundancy_disabled: null + labels: null + node_selector: [] + revision: null + service_account: test-cf-kms@foo-test-0.iam.gserviceaccount.com + service_mesh: [] + session_affinity: null + volumes: [] + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + google_cloud_run_v2_service_iam_binding.binding["$custom_roles:myrole_one"]: + condition: [] + members: + - group:test-group@example.com + role: organizations/366118655033/roles/myRoleOne + google_project_iam_member.default["organizations/366118655033/roles/myRoleOne"]: + condition: [] + member: serviceAccount:test-cf-kms@foo-test-0.iam.gserviceaccount.com + project: foo-test-0 + role: organizations/366118655033/roles/myRoleOne + google_service_account.service_account[0]: + account_id: test-cf-kms + create_ignore_already_exists: null + description: null + disabled: false + display_name: test-cf-kms + email: test-cf-kms@foo-test-0.iam.gserviceaccount.com + member: serviceAccount:test-cf-kms@foo-test-0.iam.gserviceaccount.com + project: foo-test-0 + timeouts: null + google_vpc_access_connector.connector[0]: + ip_cidr_range: null + machine_type: e2-micro + max_instances: 10 + min_instances: 3 + name: test-cf-kms + project: foo-test-0 + region: europe-west8 + subnet: + - name: gce + project_id: foo-test-0 + timeouts: null + +counts: + google_cloud_run_v2_service: 1 + google_cloud_run_v2_service_iam_binding: 1 + google_vpc_access_connector: 1 diff --git a/tests/modules/cloud_run_v2/context-subnet.tfvars b/tests/modules/cloud_run_v2/context-subnet.tfvars new file mode 100644 index 000000000..e32a42b0e --- /dev/null +++ b/tests/modules/cloud_run_v2/context-subnet.tfvars @@ -0,0 +1,58 @@ +name = "test-cf-kms" +bucket_name = "bucket" +bundle_config = { + path = "gs://assets/sample-function.zip" +} +context = { + cidr_ranges = { + test = "10.10.20.0/28" + } + custom_roles = { + myrole_one = "organizations/366118655033/roles/myRoleOne" + } + iam_principals = { + mygroup = "group:test-group@example.com" + } + kms_keys = { + test = "projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute" + } + locations = { + ew8 = "europe-west8" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } + subnets = { + test = "projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce" + } +} +kms_key = "$kms_keys:test" +iam = { + "$custom_roles:myrole_one" = [ + "$iam_principals:mygroup" + ] +} +project_id = "$project_ids:test" +region = "$locations:ew8" +service_account_config = { + roles = [ + "$custom_roles:myrole_one" + ] +} +revision = { + vpc_access = { + egress_settings = "ALL_TRAFFIC" + } +} +vpc_connector_create = { + instances = { + max = 10 + min = 3 + } + subnet = { + name = "$subnets:test" + } +} diff --git a/tests/modules/cloud_run_v2/context-subnet.yaml b/tests/modules/cloud_run_v2/context-subnet.yaml new file mode 100644 index 000000000..85449a176 --- /dev/null +++ b/tests/modules/cloud_run_v2/context-subnet.yaml @@ -0,0 +1,78 @@ +# 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. + +values: + google_cloud_run_v2_service.service[0]: + annotations: null + binary_authorization: [] + build_config: [] + client: null + client_version: null + custom_audiences: null + default_uri_disabled: null + deletion_protection: true + description: null + effective_labels: + goog-terraform-provisioned: 'true' + iap_enabled: false + invoker_iam_disabled: false + labels: null + location: europe-west8 + multi_region_settings: [] + name: test-cf-kms + project: foo-test-0 + scaling: [] + template: + - annotations: null + containers: [] + encryption_key: null + execution_environment: EXECUTION_ENVIRONMENT_GEN1 + gpu_zonal_redundancy_disabled: null + labels: null + node_selector: [] + revision: null + service_account: test-cf-kms@foo-test-0.iam.gserviceaccount.com + service_mesh: [] + session_affinity: null + volumes: [] + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + google_cloud_run_v2_service_iam_binding.binding["$custom_roles:myrole_one"]: + condition: [] + members: + - group:test-group@example.com + role: organizations/366118655033/roles/myRoleOne + google_project_iam_member.default["organizations/366118655033/roles/myRoleOne"]: + condition: [] + member: serviceAccount:test-cf-kms@foo-test-0.iam.gserviceaccount.com + project: foo-test-0 + role: organizations/366118655033/roles/myRoleOne + google_vpc_access_connector.connector[0]: + ip_cidr_range: null + machine_type: e2-micro + max_instances: 10 + min_instances: 3 + name: test-cf-kms + project: foo-test-0 + region: europe-west8 + subnet: + - name: gce + project_id: foo-dev-net-spoke-0 + timeouts: null + +counts: + google_cloud_run_v2_service: 1 + google_cloud_run_v2_service_iam_binding: 1 + google_vpc_access_connector: 1 diff --git a/tests/modules/cloud_run_v2/context.tfvars b/tests/modules/cloud_run_v2/context.tfvars new file mode 100644 index 000000000..16d5bb097 --- /dev/null +++ b/tests/modules/cloud_run_v2/context.tfvars @@ -0,0 +1,54 @@ +name = "test-run-context" +context = { + cidr_ranges = { + test = "10.10.20.0/28" + } + custom_roles = { + myrole_one = "organizations/366118655033/roles/myRoleOne" + } + iam_principals = { + mygroup = "group:test-group@example.com" + } + kms_keys = { + test = "projects/foo-prod-sec-core/locations/global/keyRings/prod-global-default/cryptoKeys/compute" + } + locations = { + ew8 = "europe-west8" + } + networks = { + test = "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + project_ids = { + test = "foo-test-0" + } + subnets = { + test = "projects/foo-dev-net-spoke-0/regions/europe-west1/subnetworks/gce" + } +} +kms_key = "$kms_keys:test" +iam = { + "$custom_roles:myrole_one" = [ + "$iam_principals:mygroup" + ] +} +project_id = "$project_ids:test" +region = "$locations:ew8" +service_account_config = { + roles = [ + "$custom_roles:myrole_one" + ] +} +revision = { + vpc_access = { + egress_settings = "ALL_TRAFFIC" + } +} +vpc_connector_create = { + ip_cidr_range = "$cidr_ranges:test" + name = "connector_name" + network = "$networks:test" + instances = { + max = 10 + min = 3 + } +} diff --git a/tests/modules/cloud_run_v2/context.yaml b/tests/modules/cloud_run_v2/context.yaml new file mode 100644 index 000000000..cb82fd436 --- /dev/null +++ b/tests/modules/cloud_run_v2/context.yaml @@ -0,0 +1,87 @@ +# 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. + +values: + google_cloud_run_v2_service.service[0]: + annotations: null + binary_authorization: [] + build_config: [] + client: null + client_version: null + custom_audiences: null + default_uri_disabled: null + deletion_protection: true + description: null + effective_labels: + goog-terraform-provisioned: 'true' + iap_enabled: false + invoker_iam_disabled: false + labels: null + location: europe-west8 + multi_region_settings: [] + name: test-run-context + project: foo-test-0 + scaling: [] + template: + - annotations: null + containers: [] + encryption_key: null + execution_environment: EXECUTION_ENVIRONMENT_GEN1 + gpu_zonal_redundancy_disabled: null + labels: null + node_selector: [] + revision: null + service_account: test-run-context@foo-test-0.iam.gserviceaccount.com + service_mesh: [] + session_affinity: null + volumes: [] + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + google_cloud_run_v2_service_iam_binding.binding["$custom_roles:myrole_one"]: + condition: [] + members: + - group:test-group@example.com + role: organizations/366118655033/roles/myRoleOne + google_project_iam_member.default["organizations/366118655033/roles/myRoleOne"]: + condition: [] + member: serviceAccount:test-run-context@foo-test-0.iam.gserviceaccount.com + project: foo-test-0 + role: organizations/366118655033/roles/myRoleOne + google_service_account.service_account[0]: + account_id: test-run-context + create_ignore_already_exists: null + description: null + disabled: false + display_name: test-run-context + email: test-run-context@foo-test-0.iam.gserviceaccount.com + member: serviceAccount:test-run-context@foo-test-0.iam.gserviceaccount.com + project: foo-test-0 + timeouts: null + google_vpc_access_connector.connector[0]: + ip_cidr_range: 10.10.20.0/28 + machine_type: e2-micro + max_instances: 10 + min_instances: 3 + name: connector_name + network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + project: foo-test-0 + region: europe-west8 + subnet: [] + timeouts: null + +counts: + google_cloud_run_v2_service: 1 + google_cloud_run_v2_service_iam_binding: 1 + google_vpc_access_connector: 1 diff --git a/tests/modules/cloud_run_v2/examples/cloudsql.yaml b/tests/modules/cloud_run_v2/examples/cloudsql.yaml index d34e887a8..548b31bd3 100644 --- a/tests/modules/cloud_run_v2/examples/cloudsql.yaml +++ b/tests/modules/cloud_run_v2/examples/cloudsql.yaml @@ -15,7 +15,7 @@ values: module.cloud_run.google_cloud_run_v2_service.service[0]: location: europe-west8 - name: hello + name: example-hello project: project-id template: - containers: diff --git a/tests/modules/cloud_run_v2/examples/cmek.yaml b/tests/modules/cloud_run_v2/examples/cmek.yaml new file mode 100644 index 000000000..dd39aacf2 --- /dev/null +++ b/tests/modules/cloud_run_v2/examples/cmek.yaml @@ -0,0 +1,90 @@ +# 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. + +values: + module.cloud_run.google_cloud_run_v2_service.service[0]: + annotations: null + binary_authorization: [] + build_config: [] + client: null + client_version: null + custom_audiences: null + default_uri_disabled: null + deletion_protection: false + description: null + effective_labels: + goog-terraform-provisioned: 'true' + iap_enabled: false + invoker_iam_disabled: false + labels: null + location: europe-west8 + multi_region_settings: [] + name: example-hello + project: test-cloudrun + scaling: [] + template: + - annotations: null + containers: + - args: null + base_image_uri: null + command: null + depends_on: null + env: [] + image: us-docker.pkg.dev/cloudrun/container/hello + liveness_probe: [] + name: hello + volume_mounts: [] + working_dir: null + execution_environment: EXECUTION_ENVIRONMENT_GEN1 + gpu_zonal_redundancy_disabled: null + labels: null + node_selector: [] + revision: null + service_account: example-hello@test-cloudrun.iam.gserviceaccount.com + service_mesh: [] + session_affinity: null + volumes: [] + vpc_access: [] + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + module.kms.google_kms_crypto_key.default["key-regional"]: + effective_labels: + goog-terraform-provisioned: 'true' + labels: null + name: key-regional + purpose: ENCRYPT_DECRYPT + rotation_period: null + skip_initial_version_creation: false + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + module.kms.google_kms_key_ring.default[0]: + location: europe-west8 + name: test-keyring + project: test-cloudrun + timeouts: null + module.kms.google_kms_key_ring_iam_binding.authoritative["roles/cloudkms.cryptoKeyEncrypterDecrypter"]: + condition: [] + role: roles/cloudkms.cryptoKeyEncrypterDecrypter + module.project.google_project.project[0]: + + name: test-cloudrun + +counts: + google_cloud_run_v2_service: 1 + google_kms_crypto_key: 1 + google_kms_key_ring: 1 + google_kms_key_ring_iam_binding: 1 + google_project: 1 diff --git a/tests/modules/cloud_run_v2/examples/gcs-mount.yaml b/tests/modules/cloud_run_v2/examples/gcs-mount.yaml index ee462e4b6..b2ef34d9a 100644 --- a/tests/modules/cloud_run_v2/examples/gcs-mount.yaml +++ b/tests/modules/cloud_run_v2/examples/gcs-mount.yaml @@ -15,7 +15,7 @@ values: module.cloud_run.google_cloud_run_v2_service.service[0]: location: europe-west8 - name: hello + name: example-hello project: project-id template: - containers: diff --git a/tests/modules/cloud_run_v2/examples/gpu-job.yaml b/tests/modules/cloud_run_v2/examples/gpu-job.yaml index da21bdf32..5624956e6 100644 --- a/tests/modules/cloud_run_v2/examples/gpu-job.yaml +++ b/tests/modules/cloud_run_v2/examples/gpu-job.yaml @@ -23,7 +23,7 @@ values: goog-terraform-provisioned: 'true' labels: null location: europe-west8 - name: job + name: example-job project: project-id run_execution_token: null start_execution_token: null @@ -57,5 +57,3 @@ values: counts: google_cloud_run_v2_job: 1 - modules: 1 - resources: 1 diff --git a/tests/modules/cloud_run_v2/examples/gpu-service.yaml b/tests/modules/cloud_run_v2/examples/gpu-service.yaml index 2467695e1..6ef57b5ab 100644 --- a/tests/modules/cloud_run_v2/examples/gpu-service.yaml +++ b/tests/modules/cloud_run_v2/examples/gpu-service.yaml @@ -67,5 +67,3 @@ values: counts: google_cloud_run_v2_service: 1 - modules: 1 - resources: 1 diff --git a/tests/modules/cloud_run_v2/examples/gpu-workerpool.yaml b/tests/modules/cloud_run_v2/examples/gpu-workerpool.yaml index 68c65492f..7aa710416 100644 --- a/tests/modules/cloud_run_v2/examples/gpu-workerpool.yaml +++ b/tests/modules/cloud_run_v2/examples/gpu-workerpool.yaml @@ -56,5 +56,3 @@ values: counts: google_cloud_run_v2_worker_pool: 1 - modules: 1 - resources: 1 diff --git a/tests/modules/cloud_run_v2/examples/iap.yaml b/tests/modules/cloud_run_v2/examples/iap.yaml new file mode 100644 index 000000000..7940d5861 --- /dev/null +++ b/tests/modules/cloud_run_v2/examples/iap.yaml @@ -0,0 +1,46 @@ +# 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. + +values: + module.cloud_run.google_cloud_run_v2_service.service[0]: + iap_enabled: true + invoker_iam_disabled: false + launch_stage: BETA + location: europe-west8 + name: example-hello + project: project-id + scaling: [] + template: + - annotations: null + containers: + - args: null + base_image_uri: null + command: null + depends_on: null + env: [] + image: us-docker.pkg.dev/cloudrun/container/hello + liveness_probe: [] + name: hello + volume_mounts: [] + working_dir: null + service_account: example-hello@project-id.iam.gserviceaccount.com + module.cloud_run.google_iap_web_cloud_run_service_iam_binding.binding["1"]: + condition: [] + members: + - group:organization-admins@example.org + role: roles/iap.httpsResourceAccessor + +counts: + google_cloud_run_v2_service: 1 + google_iap_web_cloud_run_service_iam_binding: 1 diff --git a/tests/modules/cloud_run_v2/examples/job-iam-env.yaml b/tests/modules/cloud_run_v2/examples/job-iam-env.yaml index daedaeb9b..097099496 100644 --- a/tests/modules/cloud_run_v2/examples/job-iam-env.yaml +++ b/tests/modules/cloud_run_v2/examples/job-iam-env.yaml @@ -15,7 +15,7 @@ values: module.cloud_run.google_cloud_run_v2_job.job[0]: location: europe-west8 - name: hello + name: example-hello project: project-id template: - template: @@ -46,7 +46,5 @@ values: counts: google_cloud_run_v2_job: 1 google_cloud_run_v2_job_iam_binding: 1 - modules: 1 - resources: 2 outputs: {} diff --git a/tests/modules/cloud_run_v2/examples/service-direct-vpc.yaml b/tests/modules/cloud_run_v2/examples/service-direct-vpc.yaml index 871806b5a..8a90186fa 100644 --- a/tests/modules/cloud_run_v2/examples/service-direct-vpc.yaml +++ b/tests/modules/cloud_run_v2/examples/service-direct-vpc.yaml @@ -15,7 +15,7 @@ values: module.cloud_run.google_cloud_run_v2_service.service[0]: location: europe-west8 - name: hello + name: example-hello project: project-id template: - annotations: null @@ -42,7 +42,5 @@ values: counts: google_cloud_run_v2_service: 1 - modules: 1 - resources: 1 outputs: {} diff --git a/tests/modules/cloud_run_v2/examples/service-eventarc-auditlogs-external-sa.yaml b/tests/modules/cloud_run_v2/examples/service-eventarc-auditlogs-external-sa.yaml index 6eecd6c4b..a99400285 100644 --- a/tests/modules/cloud_run_v2/examples/service-eventarc-auditlogs-external-sa.yaml +++ b/tests/modules/cloud_run_v2/examples/service-eventarc-auditlogs-external-sa.yaml @@ -15,7 +15,7 @@ values: module.cloud_run.google_cloud_run_v2_service.service[0]: location: europe-west8 - name: hello + name: example-hello project: project-id template: - containers: @@ -34,9 +34,6 @@ values: destination: - cloud_run_service: - path: null - region: europe-west8 - service: hello - location: europe-west8 matching_criteria: - attribute: methodName operator: '' @@ -48,15 +45,12 @@ values: operator: '' value: google.cloud.audit.log.v1.written name: audit-log-setiampolicy - project: project-id service_account: fixture-service-account@project-id.iam.gserviceaccount.com counts: google_cloud_run_v2_service: 1 google_cloud_run_v2_service_iam_binding: 1 google_eventarc_trigger: 1 - google_service_account: 1 - modules: 2 - resources: 5 + google_service_account: 2 outputs: {} diff --git a/tests/modules/cloud_run_v2/examples/service-eventarc-pubsub.yaml b/tests/modules/cloud_run_v2/examples/service-eventarc-pubsub.yaml index 125fecbea..af9d6110f 100644 --- a/tests/modules/cloud_run_v2/examples/service-eventarc-pubsub.yaml +++ b/tests/modules/cloud_run_v2/examples/service-eventarc-pubsub.yaml @@ -15,7 +15,7 @@ values: module.cloud_run.google_cloud_run_v2_service.service[0]: location: europe-west8 - name: hello + name: example-hello project: project-id template: - containers: @@ -35,15 +35,11 @@ values: destination: - cloud_run_service: - path: null - region: europe-west8 - service: hello - location: europe-west8 matching_criteria: - attribute: type operator: '' value: google.cloud.pubsub.topic.v1.messagePublished name: pubsub-topic-1 - project: project-id service_account: null transport: - pubsub: @@ -52,7 +48,5 @@ values: counts: google_cloud_run_v2_service: 1 google_eventarc_trigger: 1 - modules: 2 - resources: 4 outputs: {} diff --git a/tests/modules/cloud_run_v2/examples/service-eventarc-storage.yaml b/tests/modules/cloud_run_v2/examples/service-eventarc-storage.yaml index 34c549968..e2b4dd2de 100644 --- a/tests/modules/cloud_run_v2/examples/service-eventarc-storage.yaml +++ b/tests/modules/cloud_run_v2/examples/service-eventarc-storage.yaml @@ -15,7 +15,7 @@ values: module.cloud_run.google_cloud_run_v2_service.service[0]: location: europe-west8 - name: hello + name: example-hello project: project-id template: - containers: @@ -35,9 +35,6 @@ values: destination: - cloud_run_service: - path: /webhook - region: europe-west8 - service: hello - location: europe-west8 matching_criteria: - attribute: bucket operator: '' @@ -46,13 +43,10 @@ values: operator: '' value: google.cloud.storage.object.v1.finalized name: storage-bucket-upload - project: project-id service_account: fixture-service-account@project-id.iam.gserviceaccount.com counts: google_cloud_run_v2_service: 1 google_eventarc_trigger: 1 - modules: 3 - resources: 7 outputs: {} diff --git a/tests/modules/cloud_run_v2/examples/service-external-sa.yaml b/tests/modules/cloud_run_v2/examples/service-external-sa.yaml index a9d65dd42..27567e9bd 100644 --- a/tests/modules/cloud_run_v2/examples/service-external-sa.yaml +++ b/tests/modules/cloud_run_v2/examples/service-external-sa.yaml @@ -15,7 +15,7 @@ values: module.cloud_run.google_cloud_run_v2_service.service[0]: location: europe-west8 - name: hello + name: example-hello project: project-id template: - containers: diff --git a/tests/modules/cloud_run_v2/examples/service-iam-env.yaml b/tests/modules/cloud_run_v2/examples/service-iam-env.yaml index 09c9a2c0a..a6de07f0a 100644 --- a/tests/modules/cloud_run_v2/examples/service-iam-env.yaml +++ b/tests/modules/cloud_run_v2/examples/service-iam-env.yaml @@ -30,7 +30,7 @@ values: invoker_iam_disabled: false labels: null location: europe-west8 - name: hello + name: example-hello project: project-id scaling: [] template: @@ -113,8 +113,8 @@ values: counts: google_cloud_run_v2_service: 1 google_cloud_run_v2_service_iam_binding: 1 + google_project_iam_member: 2 google_secret_manager_secret: 1 google_secret_manager_secret_iam_binding: 1 google_secret_manager_secret_version: 1 - modules: 2 - resources: 5 + google_service_account: 1 diff --git a/tests/modules/cloud_run_v2/examples/service-invoker-iam-disable.yaml b/tests/modules/cloud_run_v2/examples/service-invoker-iam-disable.yaml index 7b1dddffd..fa6dfe4a6 100644 --- a/tests/modules/cloud_run_v2/examples/service-invoker-iam-disable.yaml +++ b/tests/modules/cloud_run_v2/examples/service-invoker-iam-disable.yaml @@ -15,7 +15,7 @@ values: module.cloud_run.google_cloud_run_v2_service.service[0]: location: europe-west8 - name: hello + name: example-hello project: project-id template: - containers: @@ -34,7 +34,5 @@ values: counts: google_cloud_run_v2_service: 1 - modules: 1 - resources: 1 outputs: {} diff --git a/tests/modules/cloud_run_v2/examples/service-otel-sidecar.yaml b/tests/modules/cloud_run_v2/examples/service-otel-sidecar.yaml index f313ec963..0e8029b1e 100644 --- a/tests/modules/cloud_run_v2/examples/service-otel-sidecar.yaml +++ b/tests/modules/cloud_run_v2/examples/service-otel-sidecar.yaml @@ -52,7 +52,5 @@ counts: google_secret_manager_secret: 1 google_secret_manager_secret_iam_binding: 1 google_secret_manager_secret_version: 1 - modules: 2 - resources: 4 outputs: {} diff --git a/tests/modules/cloud_run_v2/examples/service-sa-create.yaml b/tests/modules/cloud_run_v2/examples/service-sa-create.yaml index cdc3699a3..802373962 100644 --- a/tests/modules/cloud_run_v2/examples/service-sa-create.yaml +++ b/tests/modules/cloud_run_v2/examples/service-sa-create.yaml @@ -14,34 +14,81 @@ values: module.cloud_run.google_cloud_run_v2_service.service[0]: + annotations: null + binary_authorization: [] + build_config: [] + client: null + client_version: null + custom_audiences: null + default_uri_disabled: null + deletion_protection: false + description: null + effective_labels: + goog-terraform-provisioned: 'true' + iap_enabled: false + invoker_iam_disabled: false + labels: null location: europe-west8 - name: hello + multi_region_settings: [] + name: example-hello project: project-id + scaling: [] template: - - containers: - - args: null - command: null - depends_on: null - env: [] - image: us-docker.pkg.dev/cloudrun/container/hello - name: hello - volume_mounts: [] - working_dir: null - execution_environment: EXECUTION_ENVIRONMENT_GEN1 - volumes: [] - vpc_access: [] + - annotations: null + containers: + - args: null + base_image_uri: null + command: null + depends_on: null + env: [] + image: us-docker.pkg.dev/cloudrun/container/hello + liveness_probe: [] + name: hello + volume_mounts: [] + working_dir: null + encryption_key: null + execution_environment: EXECUTION_ENVIRONMENT_GEN1 + gpu_zonal_redundancy_disabled: null + labels: null + node_selector: [] + revision: null + service_account: example-hello@project-id.iam.gserviceaccount.com + service_mesh: [] + session_affinity: null + volumes: [] + vpc_access: [] + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + module.cloud_run.google_project_iam_member.default["roles/cloudsql.client"]: + condition: [] + member: serviceAccount:example-hello@project-id.iam.gserviceaccount.com + project: project-id + role: roles/cloudsql.client + module.cloud_run.google_project_iam_member.default["roles/cloudsql.instanceUser"]: + condition: [] + member: serviceAccount:example-hello@project-id.iam.gserviceaccount.com + project: project-id + role: roles/cloudsql.instanceUser + module.cloud_run.google_project_iam_member.default["roles/logging.logWriter"]: + condition: [] + member: serviceAccount:example-hello@project-id.iam.gserviceaccount.com + project: project-id + role: roles/logging.logWriter + module.cloud_run.google_project_iam_member.default["roles/monitoring.metricWriter"]: + condition: [] + member: serviceAccount:example-hello@project-id.iam.gserviceaccount.com + project: project-id + role: roles/monitoring.metricWriter module.cloud_run.google_service_account.service_account[0]: - account_id: tf-cr-hello + account_id: example-hello + create_ignore_already_exists: null description: null disabled: false - display_name: Terraform Cloud Run hello. + display_name: example-hello + email: example-hello@project-id.iam.gserviceaccount.com + member: serviceAccount:example-hello@project-id.iam.gserviceaccount.com project: project-id timeouts: null -counts: - google_cloud_run_v2_service: 1 - google_service_account: 1 - modules: 1 - resources: 2 - outputs: {} diff --git a/tests/modules/cloud_run_v2/examples/service-volume-secretes.yaml b/tests/modules/cloud_run_v2/examples/service-volume-secretes.yaml index cace77eff..ff0fe90ab 100644 --- a/tests/modules/cloud_run_v2/examples/service-volume-secretes.yaml +++ b/tests/modules/cloud_run_v2/examples/service-volume-secretes.yaml @@ -22,7 +22,7 @@ values: description: null labels: null location: europe-west8 - name: hello + name: example-hello project: project-id template: - annotations: null @@ -56,7 +56,6 @@ values: counts: google_cloud_run_v2_service: 1 - modules: 2 - resources: 4 + outputs: {} diff --git a/tests/modules/cloud_run_v2/examples/service-vpc-access-connector-create-sharedvpc.yaml b/tests/modules/cloud_run_v2/examples/service-vpc-access-connector-create-sharedvpc.yaml index 16efec979..38f13a7b6 100644 --- a/tests/modules/cloud_run_v2/examples/service-vpc-access-connector-create-sharedvpc.yaml +++ b/tests/modules/cloud_run_v2/examples/service-vpc-access-connector-create-sharedvpc.yaml @@ -15,7 +15,7 @@ values: module.cloud_run.google_cloud_run_v2_service.service[0]: location: europe-west8 - name: hello + name: example-hello project: test-service template: - containers: @@ -33,7 +33,7 @@ values: machine_type: e2-standard-4 max_throughput: 300 min_throughput: 200 - name: hello + name: example-hello project: test-service region: europe-west8 subnet: @@ -52,7 +52,6 @@ values: counts: google_cloud_run_v2_service: 1 google_vpc_access_connector: 1 - modules: 4 - resources: 59 + outputs: {} diff --git a/tests/modules/cloud_run_v2/examples/service-vpc-access-connector-create.yaml b/tests/modules/cloud_run_v2/examples/service-vpc-access-connector-create.yaml index 5fd4c74e2..96fe2d83d 100644 --- a/tests/modules/cloud_run_v2/examples/service-vpc-access-connector-create.yaml +++ b/tests/modules/cloud_run_v2/examples/service-vpc-access-connector-create.yaml @@ -26,7 +26,7 @@ values: goog-terraform-provisioned: 'true' labels: null location: europe-west8 - name: hello + name: example-hello project: project-id scaling: [] template: @@ -55,7 +55,7 @@ values: machine_type: e2-micro max_instances: 10 min_instances: 3 - name: hello + name: example-hello network: projects/xxx/global/networks/aaa project: project-id region: europe-west8 @@ -65,5 +65,3 @@ values: counts: google_cloud_run_v2_service: 1 google_vpc_access_connector: 1 - modules: 1 - resources: 2 diff --git a/tests/modules/cloud_run_v2/examples/service-vpc-access-connector.yaml b/tests/modules/cloud_run_v2/examples/service-vpc-access-connector.yaml index bee8ea643..2e2398225 100644 --- a/tests/modules/cloud_run_v2/examples/service-vpc-access-connector.yaml +++ b/tests/modules/cloud_run_v2/examples/service-vpc-access-connector.yaml @@ -15,7 +15,7 @@ values: module.cloud_run.google_cloud_run_v2_service.service[0]: location: europe-west9 - name: hello + name: example-hello project: project-id template: - annotations: null @@ -38,7 +38,6 @@ values: counts: google_cloud_run_v2_service: 1 - modules: 1 - resources: 2 + outputs: {} diff --git a/tests/modules/cloud_run_v2/examples/tags.yaml b/tests/modules/cloud_run_v2/examples/tags.yaml index 8948624b6..dbc0631ed 100644 --- a/tests/modules/cloud_run_v2/examples/tags.yaml +++ b/tests/modules/cloud_run_v2/examples/tags.yaml @@ -124,7 +124,5 @@ counts: google_tags_location_tag_binding: 2 google_tags_tag_key: 1 google_tags_tag_value: 3 - modules: 3 - resources: 8 outputs: {} diff --git a/tests/modules/cloud_run_v2/tftest.yaml b/tests/modules/cloud_run_v2/tftest.yaml new file mode 100644 index 000000000..cf724f5f0 --- /dev/null +++ b/tests/modules/cloud_run_v2/tftest.yaml @@ -0,0 +1,20 @@ +# 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: modules/cloud-run-v2 +tests: + context: + context-subnet: + context-subnet-project: + vpcconnector: diff --git a/tests/modules/cloud_run_v2/vpcconnector.tfvars b/tests/modules/cloud_run_v2/vpcconnector.tfvars new file mode 100644 index 000000000..04d098b38 --- /dev/null +++ b/tests/modules/cloud_run_v2/vpcconnector.tfvars @@ -0,0 +1,9 @@ +project_id = "test-project" +region = "region" +name = "test-run-vpc" +revision = { + vpc_access = { + connector = "projects/test-project/locations/region/connectors/vpc-connector" + egress_settings = "ALL_TRAFFIC" + } +} diff --git a/tests/modules/cloud_run_v2/vpcconnector.yaml b/tests/modules/cloud_run_v2/vpcconnector.yaml new file mode 100644 index 000000000..b87df380c --- /dev/null +++ b/tests/modules/cloud_run_v2/vpcconnector.yaml @@ -0,0 +1,71 @@ +# 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. + +values: + google_cloud_run_v2_service.service[0]: + annotations: null + binary_authorization: [] + build_config: [] + client: null + client_version: null + custom_audiences: null + default_uri_disabled: null + deletion_protection: true + description: null + effective_labels: + goog-terraform-provisioned: 'true' + iap_enabled: false + invoker_iam_disabled: false + labels: null + location: region + multi_region_settings: [] + name: test-run-vpc + project: test-project + scaling: [] + template: + - annotations: null + containers: [] + encryption_key: null + execution_environment: EXECUTION_ENVIRONMENT_GEN1 + gpu_zonal_redundancy_disabled: null + labels: null + node_selector: [] + revision: null + service_account: test-run-vpc@test-project.iam.gserviceaccount.com + service_mesh: [] + session_affinity: null + volumes: [] + vpc_access: + - connector: projects/test-project/locations/region/connectors/vpc-connector + network_interfaces: [] + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + +counts: + google_cloud_run_v2_service: 1 + +outputs: + id: __missing__ + invoke_command: __missing__ + job: null + resource: __missing__ + resource_name: __missing__ + service: __missing__ + service_account: __missing__ + service_account_email: test-run-vpc@test-project.iam.gserviceaccount.com + service_account_iam_email: serviceAccount:test-run-vpc@test-project.iam.gserviceaccount.com + service_name: __missing__ + service_uri: __missing__ + vpc_connector: null diff --git a/tools/duplicate-diff.py b/tools/duplicate-diff.py index 390081bdd..e3847045a 100755 --- a/tools/duplicate-diff.py +++ b/tools/duplicate-diff.py @@ -122,20 +122,25 @@ duplicates = [ "modules/cloud-function-v2/bundle.tf", ], [ + "modules/agent-engine/serviceaccount.tf", "modules/cloud-function-v1/serviceaccount.tf", "modules/cloud-function-v2/serviceaccount.tf", + "modules/cloud-run-v2/serviceaccount.tf", ], [ "modules/cloud-function-v1/variables-serviceaccount.tf", "modules/cloud-function-v2/variables-serviceaccount.tf", + "modules/cloud-run-v2/variables-serviceaccount.tf", ], [ "modules/cloud-function-v1/variables-vpcconnector.tf", "modules/cloud-function-v2/variables-vpcconnector.tf", + "modules/cloud-run-v2/variables-vpcconnector.tf", ], [ "modules/cloud-function-v1/vpcconnector.tf", "modules/cloud-function-v2/vpcconnector.tf", + "modules/cloud-run-v2/vpcconnector.tf", ], ] diff --git a/tools/tflint-fast.py b/tools/tflint-fast.py index e8bb1919a..56f6e4691 100755 --- a/tools/tflint-fast.py +++ b/tools/tflint-fast.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 - -# Copyright 2024 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. @@ -20,45 +19,106 @@ import subprocess import yaml from pathlib import Path +import os +import shutil +import tempfile BASEDIR = Path(__file__).parents[1] +# if we're copying the module, we might as well ignore files and +# directories that are automatically read by terraform. Useful +# to avoid surprises if, for example, you have an active fast +# deployment with links to configs) +ignore_patterns = shutil.ignore_patterns('*.auto.tfvars', '*.auto.tfvars.json', + '[0-9]-*-providers.tf', + 'terraform.tfstate*', + '.terraform.lock.hcl', + 'terraform.tfvars', '.terraform') + + +def tflint_module(module_path, var_path, extra_dirs, junit): + with tempfile.TemporaryDirectory(dir=module_path.parent) as tmp_path: + tmp_path = Path(tmp_path) + + # Running tests in a copy made with symlinks=True makes them run + # ~20% slower than when run in a copy made with symlinks=False. + shutil.copytree(BASEDIR / module_path, tmp_path, dirs_exist_ok=True, + symlinks=False, ignore=ignore_patterns) + + for extra_dir in extra_dirs: + os.symlink(extra_dir, tmp_path / extra_dir.name) + args = ['tflint'] + if junit: + args += ['--format=junit'] + args += [ + '--chdir', + str(tmp_path.absolute()), + '--var-file', + str((BASEDIR / var_path).absolute()), + '--config', + str((BASEDIR / ".tflint.hcl").absolute()), + ] + if junit: + with open(f'tflint-fast-{str(module_path).replace("/", "_")}.xml', + 'w+') as output: + return subprocess.run(args, stderr=subprocess.STDOUT, + stdout=output).returncode + else: + return subprocess.run(args, stderr=subprocess.STDOUT).returncode + + +def is_affected(files, module_path, tftest_path, extra_dirs): + # no files provided, run all tftests + if not files: + return True + absolute_files = [Path(x).absolute() for x in files] + # check if the files modified the module + ret = any(x.is_relative_to(module_path.absolute()) for x in absolute_files) + if ret: + return ret + # check if the files modified the test definition + ret = any(x.is_relative_to(tftest_path.absolute()) for x in absolute_files) + if ret: + return ret + # check if the files modified extra dirs + for extra_dir in extra_dirs: + ret = any(x.is_relative_to(extra_dir.absolute()) for x in absolute_files) + if ret: + return ret + return False + @click.option('--junit', default=False, is_flag=True) +@click.argument('files', nargs=-1, type=click.Path(), required=False) @click.command() -def main(junit): +def main(junit, files): ret = 0 - for tftest_path in sorted( + for tftest_yaml in sorted( glob.glob(f'{BASEDIR}/tests/fast/**/tftest.yaml', recursive=True)): - with open(tftest_path, 'r') as f: + with open(tftest_yaml, 'r') as f: tftest = yaml.safe_load(f) module_path = Path(tftest['module']) - var_path = (Path(tftest_path).parent / 'simple.tfvars') + tftest_path = Path(tftest_yaml).parent + simple_test = tftest['tests'].get('simple', {}) + if not simple_test: + simple_test = {} + relative_extra_dirs = simple_test.get('extra_dirs') + extra_dirs = [ + (module_path.absolute() / Path(x)) for x in relative_extra_dirs + ] if relative_extra_dirs else [] + var_path = (tftest_path / 'simple.tfvars') - print(f'## {Path(tftest_path).relative_to(BASEDIR)}') - if var_path.exists(): - args = ['tflint'] - if junit: - args += ['--format=junit'] - args += [ - '--chdir', - str((BASEDIR / module_path).absolute()), - '--var-file', - str((BASEDIR / var_path).absolute()), - '--config', - str((BASEDIR / ".tflint.hcl").absolute()), - ] - print(' '.join(args)) - if junit: - with open(f'tflint-fast-{str(module_path).replace("/", "_")}.xml', - 'w+') as output: - ret |= subprocess.run(args, stderr=subprocess.STDOUT, - stdout=output).returncode - else: - ret |= subprocess.run(args, stderr=subprocess.STDOUT).returncode - else: - print(f'Skipping stage: {tftest_path} as no simple.tfvars found there') - # end for + if not var_path.exists(): + print(f'## {module_path}: skipping stage as no simple.tfvars found there') + continue + if not is_affected(files, module_path, tftest_path, extra_dirs): + print( + f'## {module_path}: skipping stage as it is not affected by provided files' + ) + continue + click.echo(f'## {module_path}') + ret |= tflint_module(module_path, var_path, extra_dirs, junit) + # end for exit(ret)