diff --git a/mmv1/products/apigee/NatAddress.yaml b/mmv1/products/apigee/NatAddress.yaml index eb5285ac7ba6..49152a5774e7 100644 --- a/mmv1/products/apigee/NatAddress.yaml +++ b/mmv1/products/apigee/NatAddress.yaml @@ -34,15 +34,13 @@ async: !ruby/object:Api::OpAsync error: !ruby/object:Api::OpAsync::Error path: 'error' message: 'message' -immutable: true +immutable: false description: | Apigee NAT (network address translation) address. A NAT address is a static external IP address used for Internet egress traffic. This is not avaible for Apigee hybrid. - Apigee NAT addresses are not automatically activated because they might require explicit allow entries on the target systems first. See https://cloud.google.com/apigee/docs/reference/apis/apigee/rest/v1/organizations.instances.natAddresses/activate references: !ruby/object:Api::Resource::ReferenceLinks guides: 'Provisioning NAT IPs': 'https://cloud.google.com/apigee/docs/api-platform/security/nat-provisioning' api: 'https://cloud.google.com/apigee/docs/reference/apis/apigee/rest/v1/organizations.instances.natAddresses' - autogen_async: true import_format: ['{{instance_id}}/natAddresses/{{name}}', '{{instance_id}}/{{name}}'] @@ -66,10 +64,23 @@ examples: true # Resource creation race skip_vcr: true + - !ruby/object:Provider::Terraform::Examples + name: 'apigee_nat_address_with_activate' + vars: + nat_address_name: 'my-nat-address' + nat_address_activate: 'true' + skip_test: + true timeouts: !ruby/object:Api::Timeouts insert_minutes: 30 + update_minutes: 30 delete_minutes: 30 custom_code: !ruby/object:Provider::Terraform::CustomCode + constants: templates/terraform/constants/apigee_nat_address.go.erb + encoder: templates/terraform/encoders/apigee_nat_address.go.erb + decoder: templates/terraform/decoders/apigee_nat_address.go.erb + custom_update: templates/terraform/custom_update/apigee_nat_address.go.erb + post_create: templates/terraform/post_create/apigee_nat_address.go.erb custom_import: templates/terraform/custom_import/apigee_nat_address.go.erb parameters: - !ruby/object:Api::Type::String @@ -78,6 +89,7 @@ parameters: The Apigee instance associated with the Apigee environment, in the format `organizations/{{org_name}}/instances/{{instance_name}}`. required: true + immutable: true url_param_only: true properties: - !ruby/object:Api::Type::String @@ -85,6 +97,13 @@ properties: description: | Resource ID of the NAT address. required: true + immutable: true + - !ruby/object:Api::Type::Boolean + name: 'activate' + description: | + Flag that specifies whether the reserved NAT address should be activate. + required: false + default_value: false - !ruby/object:Api::Type::String name: 'ipAddress' description: | diff --git a/mmv1/templates/terraform/constants/apigee_nat_address.go.erb b/mmv1/templates/terraform/constants/apigee_nat_address.go.erb new file mode 100644 index 000000000000..09cb7ebd5b2a --- /dev/null +++ b/mmv1/templates/terraform/constants/apigee_nat_address.go.erb @@ -0,0 +1,22 @@ +<% unless compiler == "terraformgoogleconversion-codegen" -%> +// waitForNatAddressReady waits for an NatAddress to leave the +// "CREATING" state and become "RESERVED", to indicate that it's ready. +func waitForNatAddressReserved(d *schema.ResourceData, config *transport_tpg.Config, timeout time.Duration) error { + return retry.Retry(timeout, func() *retry.RetryError { + if err := resourceApigeeNatAddressRead(d, config); err != nil { + return retry.NonRetryableError(err) + } + + id := d.Id() + state := d.Get("state").(string) + if state == "CREATING" { + return retry.RetryableError(fmt.Errorf("NatAddress %q has state %q.", id, state)) + } else if state == "RESERVED" { + log.Printf("[DEBUG] NatAddress %q has state %q.", id, state) + return nil + } else { + return retry.NonRetryableError(fmt.Errorf("NatAddress %q has state %q.", id, state)) + } + }) +} +<% end -%> \ No newline at end of file diff --git a/mmv1/templates/terraform/custom_update/apigee_nat_address.go.erb b/mmv1/templates/terraform/custom_update/apigee_nat_address.go.erb new file mode 100644 index 000000000000..c864fffda4c5 --- /dev/null +++ b/mmv1/templates/terraform/custom_update/apigee_nat_address.go.erb @@ -0,0 +1,34 @@ +userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) +if err != nil { + return err +} + +billingProject := "" + +obj := make(map[string]interface{}) +nameProp, err := expandApigeeNatAddressName(d.Get("name"), d, config) +if err != nil { + return err +} else if v, ok := d.GetOkExists("name"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, nameProp)) { + obj["name"] = nameProp +} + +log.Printf("[DEBUG] Updating NatAddress %q: %#v", d.Id(), obj) + +// err == nil indicates that the billing_project value was found +if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp +} + +if d.HasChange("activate") { + if !d.Get("activate").(bool) { + return fmt.Errorf("NatAddress %q allows only the activation action", d.Id()) + } else if d.Get("state").(string) == "RESERVED" { + log.Printf("[DEBUG] Activating for NatAddress %q to become ACTIVE", d.Id()) + if err := resourceApigeeNatAddressActivate(config, d, billingProject, userAgent); err != nil { + return fmt.Errorf("Error activating NatAddress: %s", err) + } + } +} + +return resourceApigeeNatAddressRead(d, meta) \ No newline at end of file diff --git a/mmv1/templates/terraform/decoders/apigee_nat_address.go.erb b/mmv1/templates/terraform/decoders/apigee_nat_address.go.erb new file mode 100644 index 000000000000..dc1610fe3a00 --- /dev/null +++ b/mmv1/templates/terraform/decoders/apigee_nat_address.go.erb @@ -0,0 +1,2 @@ +res["activate"] = res["state"].(string) == "ACTIVE" +return res, nil diff --git a/mmv1/templates/terraform/encoders/apigee_nat_address.go.erb b/mmv1/templates/terraform/encoders/apigee_nat_address.go.erb new file mode 100644 index 000000000000..f76b3216780d --- /dev/null +++ b/mmv1/templates/terraform/encoders/apigee_nat_address.go.erb @@ -0,0 +1,3 @@ +// cannot include activate prop in the body +delete(obj, "activate") +return obj, nil \ No newline at end of file diff --git a/mmv1/templates/terraform/examples/apigee_nat_address_basic_test.tf.erb b/mmv1/templates/terraform/examples/apigee_nat_address_basic_test.tf.erb index 7169dabb6715..d124b8b24a39 100644 --- a/mmv1/templates/terraform/examples/apigee_nat_address_basic_test.tf.erb +++ b/mmv1/templates/terraform/examples/apigee_nat_address_basic_test.tf.erb @@ -60,6 +60,6 @@ resource "google_apigee_instance" "apigee_instance" { } resource "google_apigee_nat_address" "<%= ctx[:primary_resource_id] %>" { - name = "tf-test%{random_suffix}" + name = "tf-test%{random_suffix}" instance_id = google_apigee_instance.apigee_instance.id } diff --git a/mmv1/templates/terraform/examples/apigee_nat_address_with_activate.tf.erb b/mmv1/templates/terraform/examples/apigee_nat_address_with_activate.tf.erb new file mode 100644 index 000000000000..a3fdb960b97d --- /dev/null +++ b/mmv1/templates/terraform/examples/apigee_nat_address_with_activate.tf.erb @@ -0,0 +1,75 @@ +data "google_client_config" "current" {} + +resource "google_compute_network" "apigee_network" { + name = "apigee-network" +} + +resource "google_compute_global_address" "apigee_range" { + name = "apigee-range" + purpose = "VPC_PEERING" + address_type = "INTERNAL" + prefix_length = 21 + network = google_compute_network.apigee_network.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] +} + +resource "google_kms_key_ring" "apigee_keyring" { + name = "apigee-keyring" + location = "us-central1" +} + +resource "google_kms_crypto_key" "apigee_key" { + name = "apigee-key" + key_ring = google_kms_key_ring.apigee_keyring.id + + lifecycle { + prevent_destroy = true + } +} + +resource "google_project_service_identity" "apigee_sa" { + provider = google-beta + project = google_project.project.project_id + service = google_project_service.apigee.service +} + +resource "google_kms_crypto_key_iam_member" "apigee_sa_keyuser" { + crypto_key_id = google_kms_crypto_key.apigee_key.id + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + + member = google_project_service_identity.apigee_sa.member +} + +resource "google_apigee_organization" "apigee_org" { + analytics_region = "us-central1" + display_name = "apigee-org" + description = "Terraform-provisioned Apigee Org." + project_id = data.google_client_config.current.project + authorized_network = google_compute_network.apigee_network.id + runtime_database_encryption_key_name = google_kms_crypto_key.apigee_key.id + + depends_on = [ + google_service_networking_connection.apigee_vpc_connection, + google_kms_crypto_key_iam_member.apigee_sa_keyuser, + ] +} + +resource "google_apigee_instance" "apigee_instance" { + name = "apigee-instance" + location = "us-central1" + description = "Terraform-managed Apigee Runtime Instance" + display_name = "apigee-instance" + org_id = google_apigee_organization.apigee_org.id + disk_encryption_key_name = google_kms_crypto_key.apigee_key.id +} + +resource "google_apigee_nat_address" "apigee-nat" { + name = "<%= ctx[:vars]['nat_address_name'] %>" + activate = "<%= ctx[:vars]['nat_address_activate'] %>" + instance_id = google_apigee_instance.apigee_instance.id +} diff --git a/mmv1/templates/terraform/examples/apigee_nat_address_with_activate_test.tf.erb b/mmv1/templates/terraform/examples/apigee_nat_address_with_activate_test.tf.erb new file mode 100644 index 000000000000..5c0af737ffd3 --- /dev/null +++ b/mmv1/templates/terraform/examples/apigee_nat_address_with_activate_test.tf.erb @@ -0,0 +1,66 @@ +resource "google_project" "project" { + project_id = "tf-test%{random_suffix}" + name = "tf-test%{random_suffix}" + org_id = "<%= ctx[:test_env_vars]['org_id'] %>" + billing_account = "<%= ctx[:test_env_vars]['billing_account'] %>" + deletion_policy = "DELETE" +} + +resource "google_project_service" "apigee" { + project = google_project.project.project_id + service = "apigee.googleapis.com" +} + +resource "google_project_service" "compute" { + project = google_project.project.project_id + service = "compute.googleapis.com" +} + +resource "google_project_service" "servicenetworking" { + project = google_project.project.project_id + service = "servicenetworking.googleapis.com" +} + +resource "google_compute_network" "apigee_network" { + name = "apigee-network" + project = google_project.project.project_id + depends_on = [google_project_service.compute] +} + +resource "google_compute_global_address" "apigee_range" { + name = "apigee-range" + purpose = "VPC_PEERING" + address_type = "INTERNAL" + prefix_length = 21 + 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_instance" "apigee_instance" { + name = "apigee-instance" + location = "us-central1" + org_id = google_apigee_organization.apigee_org.id +} + +resource "google_apigee_nat_address" "<%= ctx[:primary_resource_id] %>" { + name = "tf-test%{random_suffix}" + activate = true + instance_id = google_apigee_instance.apigee_instance.id +} diff --git a/mmv1/templates/terraform/post_create/apigee_nat_address.go.erb b/mmv1/templates/terraform/post_create/apigee_nat_address.go.erb new file mode 100644 index 000000000000..2dfe5bf4bfcc --- /dev/null +++ b/mmv1/templates/terraform/post_create/apigee_nat_address.go.erb @@ -0,0 +1,10 @@ +if d.Get("activate").(bool) { + if err := waitForNatAddressReserved(d, config, d.Timeout(schema.TimeoutCreate) - time.Minute); err != nil { + return fmt.Errorf("Error waiting for NatAddress %q to be RESERVED during creation: %q", d.Id(), err) + } + + log.Printf("[DEBUG] Activating for NatAddress %q to become ACTIVE", d.Id()) + if err := resourceApigeeNatAddressActivate(config, d, billingProject, userAgent); err != nil { + return fmt.Errorf("Error activating NatAddress: %s", err) + } +} \ No newline at end of file diff --git a/mmv1/third_party/terraform/services/apigee/apigee_utils.go b/mmv1/third_party/terraform/services/apigee/apigee_utils.go new file mode 100644 index 000000000000..a76e5145096b --- /dev/null +++ b/mmv1/third_party/terraform/services/apigee/apigee_utils.go @@ -0,0 +1,47 @@ +package apigee + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func resourceApigeeNatAddressActivate(config *transport_tpg.Config, d *schema.ResourceData, billingProject string, userAgent string) error { + // 1. check prepare for activation + name := d.Get("name").(string) + + if d.Get("state").(string) != "RESERVED" { + return fmt.Errorf("Activating NAT address requires the state to become RESERVED") + } + + // 2. activation + activateUrl, err := tpgresource.ReplaceVars(d, config, "{{ApigeeBasePath}}{{instance_id}}/natAddresses/{{name}}:activate") + if err != nil { + return err + } + log.Printf("[DEBUG] Activating NAT address: %s", name) + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: billingProject, + RawURL: activateUrl, + UserAgent: userAgent, + }) + if err != nil { + return fmt.Errorf("Error activating NAT address: %s", err) + } + + var opRes map[string]interface{} + err = ApigeeOperationWaitTimeWithResponse( + config, res, &opRes, "Activating NAT address", userAgent, + d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("Error waiting to actiavte NAT address: %s", err) + } else { + log.Printf("[DEBUG] Finished activating NatAddress %q: %#v", d.Id(), res) + } + return nil +} diff --git a/mmv1/third_party/terraform/services/apigee/resource_apigee_nat_address_update_test.go b/mmv1/third_party/terraform/services/apigee/resource_apigee_nat_address_update_test.go new file mode 100644 index 000000000000..654415f539c5 --- /dev/null +++ b/mmv1/third_party/terraform/services/apigee/resource_apigee_nat_address_update_test.go @@ -0,0 +1,118 @@ +package apigee_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" +) + +func TestAccApigeeNatAddress_apigeeNatAddressUpdateTest(t *testing.T) { + acctest.SkipIfVcr(t) + t.Parallel() + + context := map[string]interface{}{ + "billing_account": envvar.GetTestBillingAccountFromEnv(t), + "org_id": envvar.GetTestOrgFromEnv(t), + "random_suffix": acctest.RandString(t, 10), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckApigeeNatAddressDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccApigeeNatAddress_apigeeNatAddressBasicTestExample(context), + }, + { + ResourceName: "google_apigee_nat_address.apigee_nat_address", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"activate", "instance_id"}, + }, + { + Config: testAccApigeeNatAddress_apigeeNatAddressUpdateTest(context), + }, + { + ResourceName: "google_apigee_nat_address.apigee_nat_address", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"activate", "instance_id"}, + }, + }, + }) +} + +func testAccApigeeNatAddress_apigeeNatAddressUpdateTest(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" "compute" { + project = google_project.project.project_id + service = "compute.googleapis.com" +} + +resource "google_project_service" "servicenetworking" { + project = google_project.project.project_id + service = "servicenetworking.googleapis.com" +} + +resource "google_compute_network" "apigee_network" { + name = "apigee-network" + project = google_project.project.project_id + depends_on = [google_project_service.compute] +} + +resource "google_compute_global_address" "apigee_range" { + name = "apigee-range" + purpose = "VPC_PEERING" + address_type = "INTERNAL" + prefix_length = 21 + 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_instance" "apigee_instance" { + name = "apigee-instance" + location = "us-central1" + org_id = google_apigee_organization.apigee_org.id +} + +resource "google_apigee_nat_address" "apigee_nat_address" { + name = "tf-test%{random_suffix}" + activate = true + instance_id = google_apigee_instance.apigee_instance.id +} +`, context) +}