From fa16bcd7a1f541d9476de3fe30f76c19bb01790d Mon Sep 17 00:00:00 2001 From: Ryan Oaks Date: Fri, 11 Sep 2026 20:31:20 +0000 Subject: [PATCH 1/2] Send 0 values for client_ttl and default_ttl in backend service --- mmv1/products/compute/BackendService.yaml | 2 + .../compute/RegionBackendService.yaml | 2 + ...ource_compute_backend_service_test.go.tmpl | 62 +++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/mmv1/products/compute/BackendService.yaml b/mmv1/products/compute/BackendService.yaml index 4d2cf6077102..c1585c72e08e 100644 --- a/mmv1/products/compute/BackendService.yaml +++ b/mmv1/products/compute/BackendService.yaml @@ -775,6 +775,7 @@ properties: Specifies the default TTL for cached content served by this origin for responses that do not have an existing valid TTL (max-age or s-max-age). default_from_api: true + send_empty_value: true - name: 'maxTtl' type: Integer description: | @@ -785,6 +786,7 @@ properties: description: | Specifies the maximum allowed TTL for cached content served by this origin. default_from_api: true + send_empty_value: true - name: 'negativeCaching' type: Boolean description: | diff --git a/mmv1/products/compute/RegionBackendService.yaml b/mmv1/products/compute/RegionBackendService.yaml index 4ff2bf72e371..e1563f949607 100644 --- a/mmv1/products/compute/RegionBackendService.yaml +++ b/mmv1/products/compute/RegionBackendService.yaml @@ -761,6 +761,7 @@ properties: Specifies the default TTL for cached content served by this origin for responses that do not have an existing valid TTL (max-age or s-max-age). default_from_api: true + send_empty_value: true - name: 'maxTtl' type: Integer description: | @@ -771,6 +772,7 @@ properties: description: | Specifies the maximum allowed TTL for cached content served by this origin. default_from_api: true + send_empty_value: true - name: 'negativeCaching' type: Boolean description: | diff --git a/mmv1/third_party/terraform/services/compute/resource_compute_backend_service_test.go.tmpl b/mmv1/third_party/terraform/services/compute/resource_compute_backend_service_test.go.tmpl index e12f7cc3b798..afca84d6d809 100644 --- a/mmv1/third_party/terraform/services/compute/resource_compute_backend_service_test.go.tmpl +++ b/mmv1/third_party/terraform/services/compute/resource_compute_backend_service_test.go.tmpl @@ -450,6 +450,45 @@ func TestAccComputeBackendService_withCdnPolicy(t *testing.T) { }) } +func TestAccComputeBackendService_cdnPolicyZeroTtl(t *testing.T) { + t.Parallel() + + serviceName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + checkName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckComputeBackendServiceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccComputeBackendService_withCdnPolicyZeroTtl(serviceName, checkName, 0, 0), + }, + { + ResourceName: "google_compute_backend_service.foobar", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccComputeBackendService_withCdnPolicyZeroTtl(serviceName, checkName, 3600, 1800), + }, + { + ResourceName: "google_compute_backend_service.foobar", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccComputeBackendService_withCdnPolicyZeroTtl(serviceName, checkName, 0, 0), + }, + { + ResourceName: "google_compute_backend_service.foobar", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccComputeBackendService_withSecurityPolicy(t *testing.T) { t.Parallel() @@ -2066,6 +2105,29 @@ resource "google_compute_http_health_check" "zero" { `, serviceName, checkName) } +func testAccComputeBackendService_withCdnPolicyZeroTtl(serviceName, checkName string, clientTtl, defaultTtl int) string { + return fmt.Sprintf(` +resource "google_compute_backend_service" "foobar" { + name = "%s" + health_checks = [google_compute_http_health_check.zero.self_link] + enable_cdn = true + + cdn_policy { + cache_mode = "CACHE_ALL_STATIC" + client_ttl = %d + default_ttl = %d + } +} + +resource "google_compute_http_health_check" "zero" { + name = "%s" + request_path = "/" + check_interval_sec = 1 + timeout_sec = 1 +} +`, serviceName, clientTtl, defaultTtl, checkName) +} + func testAccComputeBackendService_withSecurityPolicy(serviceName, checkName, polName, edgePolName, polLink string, edgePolLink string) string { return fmt.Sprintf(` resource "google_compute_backend_service" "foobar" { From cbd4a1cf4d57abb47ba40ab0179e51521de2a6cd Mon Sep 17 00:00:00 2001 From: Ryan Oaks Date: Tue, 15 Sep 2026 20:48:05 +0000 Subject: [PATCH 2/2] Modify approach to consider api returning 0 when ttls are unset --- mmv1/products/compute/BackendService.yaml | 4 +- .../compute/RegionBackendService.yaml | 4 +- ...ute_backend_service_cdn_policy_ttl.go.tmpl | 56 +++++++++++++++ ...ource_compute_backend_service_test.go.tmpl | 72 +++---------------- 4 files changed, 69 insertions(+), 67 deletions(-) create mode 100644 mmv1/templates/terraform/custom_expand/compute_backend_service_cdn_policy_ttl.go.tmpl diff --git a/mmv1/products/compute/BackendService.yaml b/mmv1/products/compute/BackendService.yaml index c1585c72e08e..4b3aa5b00b01 100644 --- a/mmv1/products/compute/BackendService.yaml +++ b/mmv1/products/compute/BackendService.yaml @@ -775,7 +775,7 @@ properties: Specifies the default TTL for cached content served by this origin for responses that do not have an existing valid TTL (max-age or s-max-age). default_from_api: true - send_empty_value: true + custom_expand: 'templates/terraform/custom_expand/compute_backend_service_cdn_policy_ttl.go.tmpl' - name: 'maxTtl' type: Integer description: | @@ -786,7 +786,7 @@ properties: description: | Specifies the maximum allowed TTL for cached content served by this origin. default_from_api: true - send_empty_value: true + custom_expand: 'templates/terraform/custom_expand/compute_backend_service_cdn_policy_ttl.go.tmpl' - name: 'negativeCaching' type: Boolean description: | diff --git a/mmv1/products/compute/RegionBackendService.yaml b/mmv1/products/compute/RegionBackendService.yaml index e1563f949607..a2fc2d5ff44d 100644 --- a/mmv1/products/compute/RegionBackendService.yaml +++ b/mmv1/products/compute/RegionBackendService.yaml @@ -761,7 +761,7 @@ properties: Specifies the default TTL for cached content served by this origin for responses that do not have an existing valid TTL (max-age or s-max-age). default_from_api: true - send_empty_value: true + custom_expand: 'templates/terraform/custom_expand/compute_backend_service_cdn_policy_ttl.go.tmpl' - name: 'maxTtl' type: Integer description: | @@ -772,7 +772,7 @@ properties: description: | Specifies the maximum allowed TTL for cached content served by this origin. default_from_api: true - send_empty_value: true + custom_expand: 'templates/terraform/custom_expand/compute_backend_service_cdn_policy_ttl.go.tmpl' - name: 'negativeCaching' type: Boolean description: | diff --git a/mmv1/templates/terraform/custom_expand/compute_backend_service_cdn_policy_ttl.go.tmpl b/mmv1/templates/terraform/custom_expand/compute_backend_service_cdn_policy_ttl.go.tmpl new file mode 100644 index 000000000000..91aeecc4023d --- /dev/null +++ b/mmv1/templates/terraform/custom_expand/compute_backend_service_cdn_policy_ttl.go.tmpl @@ -0,0 +1,56 @@ +{{/* + The license inside this block applies to this file + Copyright 2024 Google LLC. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ -}} + +// expand{{$.GetPrefix}}{{$.TitlelizeProperty}} uses raw config inspection so that an explicitly +// configured `{{ underscore $.Name }} = 0` reaches the API, while an unconfigured field stays out +// of the request entirely. +// +// `{{ underscore $.Name }}` is Optional + Computed, and 0 is the zero value for ints in Terraform +// SDKv2, so resource data alone cannot distinguish "the practitioner asked for 0" from "the +// practitioner said nothing". Neither of the two simple options works: +// +// - Omitting every 0 (the default expander behavior) drops a configured 0, the API substitutes +// its own default, and the next plan shows a permanent diff. +// - Sending every 0 (send_empty_value) makes unconfigured TTLs explicit, which both overrides the +// API-side default and breaks cache modes that forbid TTLs, e.g. the API rejects +// "default_ttl cannot be specified with USE_ORIGIN_HEADERS cache_mode". +// +// Returning a *int64 threads the needle: a non-nil pointer is not an empty value, so a configured 0 +// is included in the request, while nil leaves the field out. +func expand{{$.GetPrefix}}{{$.TitlelizeProperty}}(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + ttl, ok := v.(int) + if !ok { + return nil, nil + } + if ttl != 0 { + return ttl, nil + } +{{- if not (contains $.ResourceMetadata.ProductMetadata.Compiler "terraformgoogleconversion") }} + + // A 0 is only meaningful when it came from the configuration rather than from the zero value of + // an unset field. + if rd, ok := d.(*schema.ResourceData); ok { + path := cty.GetAttrPath("cdn_policy").IndexInt(0).GetAttr("{{ underscore $.Name }}") + if val, _ := rd.GetRawConfigAt(path); !val.IsNull() && val.IsKnown() { + zero := int64(0) + return &zero, nil + } + } +{{- end }} + + return nil, nil +} diff --git a/mmv1/third_party/terraform/services/compute/resource_compute_backend_service_test.go.tmpl b/mmv1/third_party/terraform/services/compute/resource_compute_backend_service_test.go.tmpl index afca84d6d809..0438b91e9d6b 100644 --- a/mmv1/third_party/terraform/services/compute/resource_compute_backend_service_test.go.tmpl +++ b/mmv1/third_party/terraform/services/compute/resource_compute_backend_service_test.go.tmpl @@ -450,45 +450,6 @@ func TestAccComputeBackendService_withCdnPolicy(t *testing.T) { }) } -func TestAccComputeBackendService_cdnPolicyZeroTtl(t *testing.T) { - t.Parallel() - - serviceName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) - checkName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) - - acctest.VcrTest(t, resource.TestCase{ - PreCheck: func() { acctest.AccTestPreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), - CheckDestroy: testAccCheckComputeBackendServiceDestroyProducer(t), - Steps: []resource.TestStep{ - { - Config: testAccComputeBackendService_withCdnPolicyZeroTtl(serviceName, checkName, 0, 0), - }, - { - ResourceName: "google_compute_backend_service.foobar", - ImportState: true, - ImportStateVerify: true, - }, - { - Config: testAccComputeBackendService_withCdnPolicyZeroTtl(serviceName, checkName, 3600, 1800), - }, - { - ResourceName: "google_compute_backend_service.foobar", - ImportState: true, - ImportStateVerify: true, - }, - { - Config: testAccComputeBackendService_withCdnPolicyZeroTtl(serviceName, checkName, 0, 0), - }, - { - ResourceName: "google_compute_backend_service.foobar", - ImportState: true, - ImportStateVerify: true, - }, - }, - }) -} - func TestAccComputeBackendService_withSecurityPolicy(t *testing.T) { t.Parallel() @@ -1977,9 +1938,17 @@ func testAccComputeBackendService_withCdnPolicy(serviceName, checkName string) s resource "google_compute_backend_service" "foobar" { name = "%s" health_checks = [google_compute_http_health_check.zero.self_link] + enable_cdn = true cdn_policy { - negative_caching = false + // client_ttl and default_ttl are explicitly 0 here. They are Optional + Computed, so an + // explicit 0 has to be told apart from an unset field: dropping it makes the API apply its own + // default and produces a permadiff, while sending it unconditionally breaks cache modes that + // forbid TTLs. testAccComputeBackendService_withCdnPolicyUseOriginHeaders covers the unset side. + cache_mode = "CACHE_ALL_STATIC" + client_ttl = 0 + default_ttl = 0 + negative_caching = false serve_while_stale = 0 cache_key_policy { include_protocol = true @@ -2105,29 +2074,6 @@ resource "google_compute_http_health_check" "zero" { `, serviceName, checkName) } -func testAccComputeBackendService_withCdnPolicyZeroTtl(serviceName, checkName string, clientTtl, defaultTtl int) string { - return fmt.Sprintf(` -resource "google_compute_backend_service" "foobar" { - name = "%s" - health_checks = [google_compute_http_health_check.zero.self_link] - enable_cdn = true - - cdn_policy { - cache_mode = "CACHE_ALL_STATIC" - client_ttl = %d - default_ttl = %d - } -} - -resource "google_compute_http_health_check" "zero" { - name = "%s" - request_path = "/" - check_interval_sec = 1 - timeout_sec = 1 -} -`, serviceName, clientTtl, defaultTtl, checkName) -} - func testAccComputeBackendService_withSecurityPolicy(serviceName, checkName, polName, edgePolName, polLink string, edgePolLink string) string { return fmt.Sprintf(` resource "google_compute_backend_service" "foobar" {