diff --git a/mmv1/products/alloydb/Instance.yaml b/mmv1/products/alloydb/Instance.yaml index 5d4dd0a56794..be7f762f7f9f 100644 --- a/mmv1/products/alloydb/Instance.yaml +++ b/mmv1/products/alloydb/Instance.yaml @@ -452,6 +452,7 @@ properties: - name: networkConfig type: NestedObject default_from_api: true + custom_flatten: templates/terraform/custom_flatten/alloydb_instance_networkconfig_flatten.go.tmpl description: | Instance level network configuration. properties: diff --git a/mmv1/templates/terraform/custom_flatten/alloydb_instance_networkconfig_flatten.go.tmpl b/mmv1/templates/terraform/custom_flatten/alloydb_instance_networkconfig_flatten.go.tmpl new file mode 100644 index 000000000000..41b6af6faf44 --- /dev/null +++ b/mmv1/templates/terraform/custom_flatten/alloydb_instance_networkconfig_flatten.go.tmpl @@ -0,0 +1,75 @@ +{{/* + The license inside this block applies to this file + Copyright 2024 Google Inc. + 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. +*/ -}} +func flatten{{$.GetPrefix}}{{$.TitlelizeProperty}}(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + original, ok := v.(map[string]interface{}) + if !ok || len(original) == 0 { + return flattenAlloydbInstanceEmptyNetworkConfig(d) + } + transformed := make(map[string]interface{}) + transformed["authorized_external_networks"] = + flattenAlloydbInstanceNetworkConfigAuthorizedExternalNetworks(original["authorizedExternalNetworks"], d, config) + transformed["enable_public_ip"] = + flattenAlloydbInstanceNetworkConfigEnablePublicIp(original["enablePublicIp"], d, config) + transformed["enable_outbound_public_ip"] = + flattenAlloydbInstanceNetworkConfigEnableOutboundPublicIp(original["enableOutboundPublicIp"], d, config) + transformed["allocated_ip_range_override"] = + flattenAlloydbInstanceNetworkConfigAllocatedIpRangeOverride(original["allocatedIpRangeOverride"], d, config) + return []interface{}{transformed} +} + +func flattenAlloydbInstanceEmptyNetworkConfig(d *schema.ResourceData) interface{} { + // The API returns nil/empty for networkConfig when enable_public_ip and + // enable_outbound_public_ip are both false (the default). Keep the user's + // existing boolean values to avoid a permadiff. + return []interface{}{ + map[string]interface{}{ + "enable_public_ip": d.Get("network_config.0.enable_public_ip"), + "enable_outbound_public_ip": d.Get("network_config.0.enable_outbound_public_ip"), + }, + } +} + +func flattenAlloydbInstanceNetworkConfigAuthorizedExternalNetworks(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + l := v.([]interface{}) + transformed := make([]interface{}, 0, len(l)) + for _, raw := range l { + original := raw.(map[string]interface{}) + if len(original) < 1 { + // Do not include empty json objects coming back from the api + continue + } + transformed = append(transformed, map[string]interface{}{ + "cidr_range": flattenAlloydbInstanceNetworkConfigAuthorizedExternalNetworksCidrRange(original["cidrRange"], d, config), + }) + } + return transformed +} + +func flattenAlloydbInstanceNetworkConfigAuthorizedExternalNetworksCidrRange(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenAlloydbInstanceNetworkConfigEnablePublicIp(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenAlloydbInstanceNetworkConfigEnableOutboundPublicIp(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenAlloydbInstanceNetworkConfigAllocatedIpRangeOverride(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} diff --git a/mmv1/third_party/terraform/services/alloydb/resource_alloydb_instance_test.go.tmpl b/mmv1/third_party/terraform/services/alloydb/resource_alloydb_instance_test.go.tmpl index ce9a0a9a2399..575e242cf57f 100644 --- a/mmv1/third_party/terraform/services/alloydb/resource_alloydb_instance_test.go.tmpl +++ b/mmv1/third_party/terraform/services/alloydb/resource_alloydb_instance_test.go.tmpl @@ -1719,6 +1719,88 @@ resource "google_alloydb_cluster" "default" { } deletion_protection = false } +data "google_compute_network" "default" { + name = "%{network_name}" +} +`, context) +} + +// Regression test for https://github.com/hashicorp/terraform-provider-google/issues/22197 +// Verifies no permadiff occurs when network_config is set with enable_public_ip = false (the default). +func TestAccAlloydbInstance_networkConfigNoPublicIpNoDiff(t *testing.T) { + t.Parallel() + + suffix := acctest.RandString(t, 10) + networkName := servicenetworking.BootstrapSharedServiceNetworkingConnection(t, "alloydb-1") + + context := map[string]interface{}{ + "random_suffix": suffix, + "network_name": networkName, + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckAlloydbInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + // Create an instance with network_config.enable_public_ip explicitly set to false. + Config: testAccAlloydbInstance_networkConfigPublicIpDisabled(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_alloydb_instance.default", "network_config.0.enable_public_ip", "false"), + resource.TestCheckResourceAttr("google_alloydb_instance.default", "network_config.0.enable_outbound_public_ip", "false"), + ), + }, + { + ResourceName: "google_alloydb_instance.default", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"cluster", "instance_id", "reconciling", "update_time"}, + }, + { + // Apply the same config again — expect no diff (regression check for permadiff). + Config: testAccAlloydbInstance_networkConfigPublicIpDisabled(context), + PlanOnly: true, + ExpectNonEmptyPlan: false, + }, + }, + }) +} + +func testAccAlloydbInstance_networkConfigPublicIpDisabled(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_alloydb_instance" "default" { + cluster = google_alloydb_cluster.default.name + instance_id = "tf-test-alloydb-instance%{random_suffix}" + instance_type = "PRIMARY" + + machine_config { + cpu_count = 2 + } + + network_config { + enable_public_ip = false + enable_outbound_public_ip = false + } + + database_flags = { + "password.enforce_complexity" = "true" + } +} + +resource "google_alloydb_cluster" "default" { + cluster_id = "tf-test-alloydb-cluster%{random_suffix}" + location = "us-central1" + network_config { + network = data.google_compute_network.default.id + } + initial_user { + password = "tf-test-alloydb-cluster%{random_suffix}" + } + + deletion_protection = false +} + data "google_compute_network" "default" { name = "%{network_name}" }