diff --git a/mmv1/products/apigee/Organization.yaml b/mmv1/products/apigee/Organization.yaml index 758675ec6078..2554f9a22fdd 100644 --- a/mmv1/products/apigee/Organization.yaml +++ b/mmv1/products/apigee/Organization.yaml @@ -44,6 +44,7 @@ async: resource_inside_response: true sweeper: identifier_field: "organization" +read_error_transform: 'transformApigeeOrganizationReadError' custom_code: encoder: 'templates/terraform/encoders/apigee_organization.go.tmpl' custom_import: 'templates/terraform/custom_import/apigee_organization.go.tmpl' diff --git a/mmv1/products/apigee/TargetServer.yaml b/mmv1/products/apigee/TargetServer.yaml index 7b5f0d779136..79c183108e8f 100644 --- a/mmv1/products/apigee/TargetServer.yaml +++ b/mmv1/products/apigee/TargetServer.yaml @@ -89,6 +89,7 @@ properties: description: | Enabling/disabling a TargetServer is useful when TargetServers are used in load balancing configurations, and one or more TargetServers need to taken out of rotation periodically. Defaults to true. default_value: true + send_empty_value: true - name: 'sSLInfo' type: NestedObject description: Specifies TLS configuration info for this TargetServer. The JSON name is sSLInfo for legacy/backwards compatibility reasons -- Edge originally supported SSL, and the name is still used for TLS configuration. diff --git a/mmv1/third_party/terraform/services/apigee/resource_apigee_organization_utils.go b/mmv1/third_party/terraform/services/apigee/resource_apigee_organization_utils.go new file mode 100644 index 000000000000..6542fe6e7796 --- /dev/null +++ b/mmv1/third_party/terraform/services/apigee/resource_apigee_organization_utils.go @@ -0,0 +1,40 @@ +package apigee + +import ( + "log" + "strings" + + "github.com/hashicorp/errwrap" + "google.golang.org/api/googleapi" +) + +// transformApigeeOrganizationReadError converts a 403 "permission denied (or +// it may not exist)" error into a 404 so that Terraform's standard +// HandleNotFoundError logic can detect that the organization has been deleted +// out-of-band and propose re-creation rather than surfacing an opaque +// permission error. +// +// Background: when an Apigee organization is deleted via the management API +// (not through Terraform), Terraform still holds state for the resource. On +// the next plan/apply Terraform calls the Read function which GETs +// /v1/organizations/. Because the organization no longer exists the +// Apigee API returns HTTP 403 with a message containing "(or it may not +// exist)" instead of the more intuitive 404 — this is intentional API +// behaviour to avoid leaking existence information to callers that lack +// read access. From Terraform's perspective this ambiguous 403 must be +// treated as "resource is gone" so that the plan shows an add rather than +// failing with an access-denied error. +func transformApigeeOrganizationReadError(err error) error { + if gErr, ok := errwrap.GetType(err, &googleapi.Error{}).(*googleapi.Error); ok { + if gErr.Code == 403 && strings.Contains(gErr.Message, "(or it may not exist)") { + // Rewrite the status code so HandleNotFoundError treats this as a + // deleted resource and schedules re-creation on the next apply. + gErr.Code = 404 + } + + log.Printf("[DEBUG] Transformed ApigeeOrganization read error: %v", gErr) + return gErr + } + + return err +} diff --git a/mmv1/third_party/terraform/services/apigee/resource_apigee_target_server_test.go b/mmv1/third_party/terraform/services/apigee/resource_apigee_target_server_test.go index 71d151b225f7..2381cdb2bdd1 100644 --- a/mmv1/third_party/terraform/services/apigee/resource_apigee_target_server_test.go +++ b/mmv1/third_party/terraform/services/apigee/resource_apigee_target_server_test.go @@ -478,3 +478,208 @@ resource "google_apigee_target_server" "apigee_target_server"{ } `, context) } + +func TestAccApigeeTargetServer_apigeeTargetServerIsEnabledFalse(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "org_id": envvar.GetTestOrgFromEnv(t), + "billing_account": envvar.GetTestBillingAccountFromEnv(t), + "random_suffix": acctest.RandString(t, 10), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {}, + }, + CheckDestroy: testAccCheckApigeeTargetServerDestroyProducer(t), + Steps: []resource.TestStep{ + { + // Create with is_enabled = false; previously the field was never sent + // due to the IsEmptyValue guard treating false as empty, causing the + // API to default is_enabled to true regardless. + Config: testAccApigeeTargetServer_isEnabledFalse(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_apigee_target_server.apigee_target_server", "is_enabled", "false"), + ), + }, + { + ResourceName: "google_apigee_target_server.apigee_target_server", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"env_id"}, + }, + { + // Update is_enabled back to true + Config: testAccApigeeTargetServer_isEnabledTrue(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_apigee_target_server.apigee_target_server", "is_enabled", "true"), + ), + }, + }, + }) +} + +func testAccApigeeTargetServer_isEnabledFalse(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + project_id = "tf-test%{random_suffix}" + name = "tf-test%{random_suffix}" + org_id = "%{org_id}" + billing_account = "%{billing_account}" + deletion_policy = "DELETE" +} + +resource "google_project_service" "apigee" { + project = google_project.project.project_id + service = "apigee.googleapis.com" +} + +resource "google_project_service" "servicenetworking" { + project = google_project.project.project_id + service = "servicenetworking.googleapis.com" + depends_on = [google_project_service.apigee] +} + +resource "google_project_service" "compute" { + project = google_project.project.project_id + service = "compute.googleapis.com" + depends_on = [google_project_service.servicenetworking] +} + +resource "time_sleep" "wait_300_seconds" { + create_duration = "300s" + depends_on = [google_project_service.compute] +} + +resource "google_compute_network" "apigee_network" { + name = "apigee-network" + project = google_project.project.project_id + depends_on = [time_sleep.wait_300_seconds] +} + +resource "google_compute_global_address" "apigee_range" { + name = "apigee-range" + purpose = "VPC_PEERING" + address_type = "INTERNAL" + prefix_length = 16 + network = google_compute_network.apigee_network.id + project = google_project.project.project_id +} + +resource "google_service_networking_connection" "apigee_vpc_connection" { + network = google_compute_network.apigee_network.id + service = "servicenetworking.googleapis.com" + reserved_peering_ranges = [google_compute_global_address.apigee_range.name] + depends_on = [google_project_service.servicenetworking] +} + +resource "google_apigee_organization" "apigee_org" { + analytics_region = "us-central1" + project_id = google_project.project.project_id + authorized_network = google_compute_network.apigee_network.id + depends_on = [ + google_service_networking_connection.apigee_vpc_connection, + google_project_service.apigee, + ] +} + +resource "google_apigee_environment" "apigee_environment" { + org_id = google_apigee_organization.apigee_org.id + name = "tf-test%{random_suffix}" + description = "Apigee Environment" + display_name = "environment-1" +} + +resource "google_apigee_target_server" "apigee_target_server" { + name = "tf-test-target-server%{random_suffix}" + host = "abc.foo.com" + port = 8080 + is_enabled = false + env_id = google_apigee_environment.apigee_environment.id +} +`, context) +} + +func testAccApigeeTargetServer_isEnabledTrue(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + project_id = "tf-test%{random_suffix}" + name = "tf-test%{random_suffix}" + org_id = "%{org_id}" + billing_account = "%{billing_account}" + deletion_policy = "DELETE" +} + +resource "google_project_service" "apigee" { + project = google_project.project.project_id + service = "apigee.googleapis.com" +} + +resource "google_project_service" "servicenetworking" { + project = google_project.project.project_id + service = "servicenetworking.googleapis.com" + depends_on = [google_project_service.apigee] +} + +resource "google_project_service" "compute" { + project = google_project.project.project_id + service = "compute.googleapis.com" + depends_on = [google_project_service.servicenetworking] +} + +resource "time_sleep" "wait_300_seconds" { + create_duration = "300s" + depends_on = [google_project_service.compute] +} + +resource "google_compute_network" "apigee_network" { + name = "apigee-network" + project = google_project.project.project_id + depends_on = [time_sleep.wait_300_seconds] +} + +resource "google_compute_global_address" "apigee_range" { + name = "apigee-range" + purpose = "VPC_PEERING" + address_type = "INTERNAL" + prefix_length = 16 + network = google_compute_network.apigee_network.id + project = google_project.project.project_id +} + +resource "google_service_networking_connection" "apigee_vpc_connection" { + network = google_compute_network.apigee_network.id + service = "servicenetworking.googleapis.com" + reserved_peering_ranges = [google_compute_global_address.apigee_range.name] + depends_on = [google_project_service.servicenetworking] +} + +resource "google_apigee_organization" "apigee_org" { + analytics_region = "us-central1" + project_id = google_project.project.project_id + authorized_network = google_compute_network.apigee_network.id + depends_on = [ + google_service_networking_connection.apigee_vpc_connection, + google_project_service.apigee, + ] +} + +resource "google_apigee_environment" "apigee_environment" { + org_id = google_apigee_organization.apigee_org.id + name = "tf-test%{random_suffix}" + description = "Apigee Environment" + display_name = "environment-1" +} + +resource "google_apigee_target_server" "apigee_target_server" { + name = "tf-test-target-server%{random_suffix}" + host = "abc.foo.com" + port = 8080 + is_enabled = true + env_id = google_apigee_environment.apigee_environment.id +} +`, context) +}