From e99e122fdd41257e022d8af91a1831e18a967147 Mon Sep 17 00:00:00 2001 From: The Magician Date: Thu, 11 Jan 2024 06:34:42 -0800 Subject: [PATCH] Add support for NFS and GCS mounts in Cloud Run v2 service (#9728) (#16972) * feat: add support for NFS and GCS mounts in Cloud Run v2 service ```release-note:enhancement cloudrunv2: added `nfs` field to `google_cloud_run_v2_service.template.volumes` cloudrunv2: added `gcs` field to `google_cloud_run_v2_service.template.volumes` cloudrunv2: adding tcpSocket field to `google_cloud_run_v2.template.containers.livenessProbe` ``` * fix: add required to relevant properties [upstream:1de3c302c346c96d1313a4f3ec7e3c815bd8de59] Signed-off-by: Modular Magician --- .changelog/9728.txt | 5 + .../resource_cloud_run_v2_service.go | 271 ++++++++++++++++++ ...rce_cloud_run_v2_service_generated_test.go | 140 +++++++++ .../docs/r/cloud_run_v2_service.html.markdown | 147 ++++++++++ 4 files changed, 563 insertions(+) create mode 100644 .changelog/9728.txt diff --git a/.changelog/9728.txt b/.changelog/9728.txt new file mode 100644 index 00000000000..37518d235b4 --- /dev/null +++ b/.changelog/9728.txt @@ -0,0 +1,5 @@ +```release-note:enhancement +cloudrunv2: added `nfs` field to `google_cloud_run_v2_service.template.volumes` +cloudrunv2: added `gcs` field to `google_cloud_run_v2_service.template.volumes` +cloudrunv2: adding `tcpSocket` field to `google_cloud_run_v2.template.containers.liveness_probe` +``` diff --git a/google/services/cloudrunv2/resource_cloud_run_v2_service.go b/google/services/cloudrunv2/resource_cloud_run_v2_service.go index 19721e9b933..e87adc747bd 100644 --- a/google/services/cloudrunv2/resource_cloud_run_v2_service.go +++ b/google/services/cloudrunv2/resource_cloud_run_v2_service.go @@ -263,6 +263,23 @@ If not specified, defaults to the same value as container.ports[0].containerPort Description: `How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1. Maximum value for liveness probe is 3600. Maximum value for startup probe is 240. Must be greater or equal than timeoutSeconds`, Default: 10, }, + "tcp_socket": { + Type: schema.TypeList, + Optional: true, + Description: `TCPSocketAction describes an action based on opening a socket`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "port": { + Type: schema.TypeInt, + Required: true, + Description: `Port number to access on the container. Must be in the range 1 to 65535. +If not specified, defaults to the exposed port of the container, which +is the value of container.ports[0].containerPort.`, + }, + }, + }, + }, "timeout_seconds": { Type: schema.TypeInt, Optional: true, @@ -576,6 +593,51 @@ A duration in seconds with up to nine fractional digits, ending with 's'. Exampl }, }, }, + "gcs": { + Type: schema.TypeList, + Optional: true, + Description: `Represents a GCS Bucket mounted as a volume.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "bucket": { + Type: schema.TypeString, + Required: true, + Description: `GCS Bucket name`, + }, + "read_only": { + Type: schema.TypeBool, + Optional: true, + Description: `If true, mount the GCS bucket as read-only`, + }, + }, + }, + }, + "nfs": { + Type: schema.TypeList, + Optional: true, + Description: `Represents an NFS mount.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "path": { + Type: schema.TypeString, + Required: true, + Description: `Path that is exported by the NFS server.`, + }, + "server": { + Type: schema.TypeString, + Required: true, + Description: `Hostname or IP address of the NFS server`, + }, + "read_only": { + Type: schema.TypeBool, + Optional: true, + Description: `If true, mount the NFS volume as read only`, + }, + }, + }, + }, "secret": { Type: schema.TypeList, Optional: true, @@ -2014,6 +2076,8 @@ func flattenCloudRunV2ServiceTemplateContainersLivenessProbe(v interface{}, d *s flattenCloudRunV2ServiceTemplateContainersLivenessProbeHttpGet(original["httpGet"], d, config) transformed["grpc"] = flattenCloudRunV2ServiceTemplateContainersLivenessProbeGrpc(original["grpc"], d, config) + transformed["tcp_socket"] = + flattenCloudRunV2ServiceTemplateContainersLivenessProbeTcpSocket(original["tcpSocket"], d, config) return []interface{}{transformed} } func flattenCloudRunV2ServiceTemplateContainersLivenessProbeInitialDelaySeconds(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { @@ -2179,6 +2243,36 @@ func flattenCloudRunV2ServiceTemplateContainersLivenessProbeGrpcService(v interf return v } +func flattenCloudRunV2ServiceTemplateContainersLivenessProbeTcpSocket(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["port"] = + flattenCloudRunV2ServiceTemplateContainersLivenessProbeTcpSocketPort(original["port"], d, config) + return []interface{}{transformed} +} +func flattenCloudRunV2ServiceTemplateContainersLivenessProbeTcpSocketPort(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := tpgresource.StringToFixed64(strVal); err == nil { + return intVal + } + } + + // number values are represented as float64 + if floatVal, ok := v.(float64); ok { + intVal := int(floatVal) + return intVal + } + + return v // let terraform core handle it otherwise +} + func flattenCloudRunV2ServiceTemplateContainersStartupProbe(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { if v == nil { return nil @@ -2414,6 +2508,8 @@ func flattenCloudRunV2ServiceTemplateVolumes(v interface{}, d *schema.ResourceDa "name": flattenCloudRunV2ServiceTemplateVolumesName(original["name"], d, config), "secret": flattenCloudRunV2ServiceTemplateVolumesSecret(original["secret"], d, config), "cloud_sql_instance": flattenCloudRunV2ServiceTemplateVolumesCloudSqlInstance(original["cloudSqlInstance"], d, config), + "gcs": flattenCloudRunV2ServiceTemplateVolumesGcs(original["gcs"], d, config), + "nfs": flattenCloudRunV2ServiceTemplateVolumesNfs(original["nfs"], d, config), }) } return transformed @@ -2525,6 +2621,58 @@ func flattenCloudRunV2ServiceTemplateVolumesCloudSqlInstanceInstances(v interfac return schema.NewSet(schema.HashString, v.([]interface{})) } +func flattenCloudRunV2ServiceTemplateVolumesGcs(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["bucket"] = + flattenCloudRunV2ServiceTemplateVolumesGcsBucket(original["bucket"], d, config) + transformed["read_only"] = + flattenCloudRunV2ServiceTemplateVolumesGcsReadOnly(original["readOnly"], d, config) + return []interface{}{transformed} +} +func flattenCloudRunV2ServiceTemplateVolumesGcsBucket(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenCloudRunV2ServiceTemplateVolumesGcsReadOnly(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenCloudRunV2ServiceTemplateVolumesNfs(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["server"] = + flattenCloudRunV2ServiceTemplateVolumesNfsServer(original["server"], d, config) + transformed["path"] = + flattenCloudRunV2ServiceTemplateVolumesNfsPath(original["path"], d, config) + transformed["read_only"] = + flattenCloudRunV2ServiceTemplateVolumesNfsReadOnly(original["readOnly"], d, config) + return []interface{}{transformed} +} +func flattenCloudRunV2ServiceTemplateVolumesNfsServer(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenCloudRunV2ServiceTemplateVolumesNfsPath(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenCloudRunV2ServiceTemplateVolumesNfsReadOnly(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + func flattenCloudRunV2ServiceTemplateExecutionEnvironment(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { return v } @@ -3533,6 +3681,13 @@ func expandCloudRunV2ServiceTemplateContainersLivenessProbe(v interface{}, d tpg transformed["grpc"] = transformedGrpc } + transformedTcpSocket, err := expandCloudRunV2ServiceTemplateContainersLivenessProbeTcpSocket(original["tcp_socket"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedTcpSocket); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["tcpSocket"] = transformedTcpSocket + } + return transformed, nil } @@ -3674,6 +3829,29 @@ func expandCloudRunV2ServiceTemplateContainersLivenessProbeGrpcService(v interfa return v, nil } +func expandCloudRunV2ServiceTemplateContainersLivenessProbeTcpSocket(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedPort, err := expandCloudRunV2ServiceTemplateContainersLivenessProbeTcpSocketPort(original["port"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedPort); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["port"] = transformedPort + } + + return transformed, nil +} + +func expandCloudRunV2ServiceTemplateContainersLivenessProbeTcpSocketPort(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + func expandCloudRunV2ServiceTemplateContainersStartupProbe(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { l := v.([]interface{}) if len(l) == 0 || l[0] == nil { @@ -3936,6 +4114,20 @@ func expandCloudRunV2ServiceTemplateVolumes(v interface{}, d tpgresource.Terrafo transformed["cloudSqlInstance"] = transformedCloudSqlInstance } + transformedGcs, err := expandCloudRunV2ServiceTemplateVolumesGcs(original["gcs"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedGcs); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["gcs"] = transformedGcs + } + + transformedNfs, err := expandCloudRunV2ServiceTemplateVolumesNfs(original["nfs"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedNfs); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["nfs"] = transformedNfs + } + req = append(req, transformed) } return req, nil @@ -4058,6 +4250,85 @@ func expandCloudRunV2ServiceTemplateVolumesCloudSqlInstanceInstances(v interface return v, nil } +func expandCloudRunV2ServiceTemplateVolumesGcs(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedBucket, err := expandCloudRunV2ServiceTemplateVolumesGcsBucket(original["bucket"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedBucket); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["bucket"] = transformedBucket + } + + transformedReadOnly, err := expandCloudRunV2ServiceTemplateVolumesGcsReadOnly(original["read_only"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedReadOnly); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["readOnly"] = transformedReadOnly + } + + return transformed, nil +} + +func expandCloudRunV2ServiceTemplateVolumesGcsBucket(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandCloudRunV2ServiceTemplateVolumesGcsReadOnly(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandCloudRunV2ServiceTemplateVolumesNfs(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedServer, err := expandCloudRunV2ServiceTemplateVolumesNfsServer(original["server"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedServer); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["server"] = transformedServer + } + + transformedPath, err := expandCloudRunV2ServiceTemplateVolumesNfsPath(original["path"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedPath); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["path"] = transformedPath + } + + transformedReadOnly, err := expandCloudRunV2ServiceTemplateVolumesNfsReadOnly(original["read_only"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedReadOnly); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["readOnly"] = transformedReadOnly + } + + return transformed, nil +} + +func expandCloudRunV2ServiceTemplateVolumesNfsServer(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandCloudRunV2ServiceTemplateVolumesNfsPath(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandCloudRunV2ServiceTemplateVolumesNfsReadOnly(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + func expandCloudRunV2ServiceTemplateExecutionEnvironment(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { return v, nil } diff --git a/google/services/cloudrunv2/resource_cloud_run_v2_service_generated_test.go b/google/services/cloudrunv2/resource_cloud_run_v2_service_generated_test.go index 1fa3f27eb01..391a79f42a9 100644 --- a/google/services/cloudrunv2/resource_cloud_run_v2_service_generated_test.go +++ b/google/services/cloudrunv2/resource_cloud_run_v2_service_generated_test.go @@ -474,6 +474,146 @@ resource "google_secret_manager_secret_iam_member" "secret-access" { `, context) } +func TestAccCloudRunV2Service_cloudrunv2ServiceMountGcsExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckCloudRunV2ServiceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccCloudRunV2Service_cloudrunv2ServiceMountGcsExample(context), + }, + { + ResourceName: "google_cloud_run_v2_service.default", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name", "location", "labels", "annotations", "terraform_labels"}, + }, + }, + }) +} + +func testAccCloudRunV2Service_cloudrunv2ServiceMountGcsExample(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_cloud_run_v2_service" "default" { + name = "tf-test-cloudrun-service%{random_suffix}" + + location = "us-central1" + launch_stage = "BETA" + + template { + execution_environment = "EXECUTION_ENVIRONMENT_GEN2" + + containers { + image = "us-docker.pkg.dev/cloudrun/container/hello" + volume_mounts { + name = "bucket" + mount_path = "/var/www" + } + } + + volumes { + name = "bucket" + gcs { + bucket = google_storage_bucket.default.name + read_only = false + } + } + } +} + +resource "google_storage_bucket" "default" { + name = "tf-test-cloudrun-service%{random_suffix}" + location = "US" +} +`, context) +} + +func TestAccCloudRunV2Service_cloudrunv2ServiceMountNfsExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckCloudRunV2ServiceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccCloudRunV2Service_cloudrunv2ServiceMountNfsExample(context), + }, + { + ResourceName: "google_cloud_run_v2_service.default", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"name", "location", "labels", "annotations", "terraform_labels"}, + }, + }, + }) +} + +func testAccCloudRunV2Service_cloudrunv2ServiceMountNfsExample(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_cloud_run_v2_service" "default" { + name = "tf-test-cloudrun-service%{random_suffix}" + + location = "us-central1" + ingress = "INGRESS_TRAFFIC_ALL" + launch_stage = "BETA" + + template { + execution_environment = "EXECUTION_ENVIRONMENT_GEN2" + containers { + image = "us-docker.pkg.dev/cloudrun/container/hello:latest" + volume_mounts { + name = "nfs" + mount_path = "/mnt/nfs/filestore" + } + } + vpc_access { + network_interfaces { + network = "default" + subnetwork = "default" + } + } + + volumes { + name = "nfs" + nfs { + server = google_filestore_instance.default.networks[0].ip_addresses[0] + path = "/share1" + read_only = false + } + } + } +} + +resource "google_filestore_instance" "default" { + name = "tf-test-cloudrun-service%{random_suffix}" + location = "us-central1-b" + tier = "BASIC_HDD" + + file_shares { + capacity_gb = 1024 + name = "share1" + } + + networks { + network = "default" + modes = ["MODE_IPV4"] + } +} +`, context) +} + func testAccCheckCloudRunV2ServiceDestroyProducer(t *testing.T) func(s *terraform.State) error { return func(s *terraform.State) error { for name, rs := range s.RootModule().Resources { diff --git a/website/docs/r/cloud_run_v2_service.html.markdown b/website/docs/r/cloud_run_v2_service.html.markdown index c9fd91715bd..dd5368bb450 100644 --- a/website/docs/r/cloud_run_v2_service.html.markdown +++ b/website/docs/r/cloud_run_v2_service.html.markdown @@ -373,6 +373,106 @@ resource "google_cloud_run_v2_service" "default" { } } ``` + +## Example Usage - Cloudrunv2 Service Mount Gcs + + +```hcl +resource "google_cloud_run_v2_service" "default" { + name = "cloudrun-service" + + location = "us-central1" + launch_stage = "BETA" + + template { + execution_environment = "EXECUTION_ENVIRONMENT_GEN2" + + containers { + image = "us-docker.pkg.dev/cloudrun/container/hello" + volume_mounts { + name = "bucket" + mount_path = "/var/www" + } + } + + volumes { + name = "bucket" + gcs { + bucket = google_storage_bucket.default.name + read_only = false + } + } + } +} + +resource "google_storage_bucket" "default" { + name = "cloudrun-service" + location = "US" +} +``` + +## Example Usage - Cloudrunv2 Service Mount Nfs + + +```hcl +resource "google_cloud_run_v2_service" "default" { + name = "cloudrun-service" + + location = "us-central1" + ingress = "INGRESS_TRAFFIC_ALL" + launch_stage = "BETA" + + template { + execution_environment = "EXECUTION_ENVIRONMENT_GEN2" + containers { + image = "us-docker.pkg.dev/cloudrun/container/hello:latest" + volume_mounts { + name = "nfs" + mount_path = "/mnt/nfs/filestore" + } + } + vpc_access { + network_interfaces { + network = "default" + subnetwork = "default" + } + } + + volumes { + name = "nfs" + nfs { + server = google_filestore_instance.default.networks[0].ip_addresses[0] + path = "/share1" + read_only = false + } + } + } +} + +resource "google_filestore_instance" "default" { + name = "cloudrun-service" + location = "us-central1-b" + tier = "BASIC_HDD" + + file_shares { + capacity_gb = 1024 + name = "share1" + } + + networks { + network = "default" + modes = ["MODE_IPV4"] + } +} +``` ## Argument Reference @@ -659,6 +759,11 @@ The following arguments are supported: GRPC specifies an action involving a GRPC port. Structure is [documented below](#nested_grpc). +* `tcp_socket` - + (Optional) + TCPSocketAction describes an action based on opening a socket + Structure is [documented below](#nested_tcp_socket). + The `http_get` block supports: @@ -700,6 +805,14 @@ The following arguments are supported: (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). If this is not specified, the default behavior is defined by gRPC. +The `tcp_socket` block supports: + +* `port` - + (Required) + Port number to access on the container. Must be in the range 1 to 65535. + If not specified, defaults to the exposed port of the container, which + is the value of container.ports[0].containerPort. + The `startup_probe` block supports: * `initial_delay_seconds` - @@ -802,6 +915,16 @@ The following arguments are supported: Ephemeral storage used as a shared volume. Structure is [documented below](#nested_empty_dir). +* `gcs` - + (Optional) + Represents a GCS Bucket mounted as a volume. + Structure is [documented below](#nested_gcs). + +* `nfs` - + (Optional) + Represents an NFS mount. + Structure is [documented below](#nested_nfs). + The `secret` block supports: @@ -851,6 +974,30 @@ The following arguments are supported: (Optional) Limit on the storage usable by this EmptyDir volume. The size limit is also applicable for memory medium. The maximum usage on memory medium EmptyDir would be the minimum value between the SizeLimit specified here and the sum of memory limits of all containers in a pod. This field's values are of the 'Quantity' k8s type: https://kubernetes.io/docs/reference/kubernetes-api/common-definitions/quantity/. The default is nil which means that the limit is undefined. More info: https://kubernetes.io/docs/concepts/storage/volumes/#emptydir. +The `gcs` block supports: + +* `bucket` - + (Required) + GCS Bucket name + +* `read_only` - + (Optional) + If true, mount the GCS bucket as read-only + +The `nfs` block supports: + +* `server` - + (Required) + Hostname or IP address of the NFS server + +* `path` - + (Required) + Path that is exported by the NFS server. + +* `read_only` - + (Optional) + If true, mount the NFS volume as read only + - - -