From 43d556a9311505ef0be2218db6a56bbac0570735 Mon Sep 17 00:00:00 2001 From: Mateus Pimenta <1920261+matpimenta@users.noreply.github.com> Date: Wed, 8 Jan 2025 09:55:29 +0000 Subject: [PATCH 1/3] Add private service connect resources and datasources --- .github/workflows/terraform_provider.yml | 2 + GNUmakefile | 2 +- README.md | 2 +- ...d_active_active_private_service_connect.md | 35 ++ ...ctive_private_service_connect_endpoints.md | 51 +++ .../rediscloud_private_service_connect.md | 33 ++ ...cloud_private_service_connect_endpoints.md | 49 +++ ...d_active_active_private_service_connect.md | 31 ++ ...active_private_service_connect_endpoint.md | 203 +++++++++ ...ivate_service_connect_endpoint_accepter.md | 22 + ...loud_active_active_subscription_peering.md | 2 +- .../rediscloud_private_service_connect.md | 29 ++ ...scloud_private_service_connect_endpoint.md | 161 +++++++ ...ivate_service_connect_endpoint_accepter.md | 21 + go.mod | 43 +- go.sum | 94 +++-- ...d_active_active_private_service_connect.go | 81 ++++ ...ctive_private_service_connect_endpoints.go | 141 +++++++ ...urce_rediscloud_private_service_connect.go | 75 ++++ ...cloud_private_service_connect_endpoints.go | 157 +++++++ provider/provider.go | 66 +-- provider/provider_test.go | 11 +- ..._service_connect_endpoint_accepter_test.go | 197 +++++++++ ...e_private_service_connect_endpoint_test.go | 145 +++++++ ...ive_active_private_service_connect_test.go | 126 ++++++ ..._service_connect_endpoint_accepter_test.go | 157 +++++++ ...d_private_service_connect_endpoint_test.go | 106 +++++ ...rediscloud_private_service_connect_test.go | 86 ++++ ...d_active_active_private_service_connect.go | 213 ++++++++++ ...active_private_service_connect_endpoint.go | 366 ++++++++++++++++ ...ivate_service_connect_endpoint_accepter.go | 278 ++++++++++++ ...urce_rediscloud_private_service_connect.go | 212 ++++++++++ ...scloud_private_service_connect_endpoint.go | 398 ++++++++++++++++++ ...ivate_service_connect_endpoint_accepter.go | 304 +++++++++++++ provider/utils.go | 5 +- 35 files changed, 3803 insertions(+), 101 deletions(-) create mode 100644 docs/data-sources/rediscloud_active_active_private_service_connect.md create mode 100644 docs/data-sources/rediscloud_active_active_private_service_connect_endpoints.md create mode 100644 docs/data-sources/rediscloud_private_service_connect.md create mode 100644 docs/data-sources/rediscloud_private_service_connect_endpoints.md create mode 100644 docs/resources/rediscloud_active_active_private_service_connect.md create mode 100644 docs/resources/rediscloud_active_active_private_service_connect_endpoint.md create mode 100644 docs/resources/rediscloud_active_active_private_service_connect_endpoint_accepter.md create mode 100644 docs/resources/rediscloud_private_service_connect.md create mode 100644 docs/resources/rediscloud_private_service_connect_endpoint.md create mode 100644 docs/resources/rediscloud_private_service_connect_endpoint_accepter.md create mode 100644 provider/datasource_rediscloud_active_active_private_service_connect.go create mode 100644 provider/datasource_rediscloud_active_active_private_service_connect_endpoints.go create mode 100644 provider/datasource_rediscloud_private_service_connect.go create mode 100644 provider/datasource_rediscloud_private_service_connect_endpoints.go create mode 100644 provider/rediscloud_active_active_private_service_connect_endpoint_accepter_test.go create mode 100644 provider/rediscloud_active_active_private_service_connect_endpoint_test.go create mode 100644 provider/rediscloud_active_active_private_service_connect_test.go create mode 100644 provider/rediscloud_private_service_connect_endpoint_accepter_test.go create mode 100644 provider/rediscloud_private_service_connect_endpoint_test.go create mode 100644 provider/rediscloud_private_service_connect_test.go create mode 100644 provider/resource_rediscloud_active_active_private_service_connect.go create mode 100644 provider/resource_rediscloud_active_active_private_service_connect_endpoint.go create mode 100644 provider/resource_rediscloud_active_active_private_service_connect_endpoint_accepter.go create mode 100644 provider/resource_rediscloud_private_service_connect.go create mode 100644 provider/resource_rediscloud_private_service_connect_endpoint.go create mode 100644 provider/resource_rediscloud_private_service_connect_endpoint_accepter.go diff --git a/.github/workflows/terraform_provider.yml b/.github/workflows/terraform_provider.yml index ada1e3e7..94f92c7a 100644 --- a/.github/workflows/terraform_provider.yml +++ b/.github/workflows/terraform_provider.yml @@ -126,6 +126,8 @@ jobs: AWS_SIGNIN_URL: ${{ secrets.CLOUD_ACCOUNT_URL }} GCP_VPC_PROJECT: ${{ secrets.GCP_VPC_PROJECT }} GCP_VPC_ID: ${{ secrets.GCP_VPC_ID }} + # TODO + GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} - name: Generate code coverage report if: steps.filter.outputs.code-changes == 'true' && (success() || failure()) run: make generate_coverage diff --git a/GNUmakefile b/GNUmakefile index b5f52fd8..8a19f929 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -10,7 +10,7 @@ PLUGINS_PATH = ~/.terraform.d/plugins PLUGINS_PROVIDER_PATH=$(PROVIDER_HOSTNAME)/$(PROVIDER_NAMESPACE)/$(PROVIDER_TYPE)/$(PROVIDER_VERSION)/$(PROVIDER_TARGET) # Use a parallelism of 3 by default for tests, overriding whatever GOMAXPROCS is set to. -TEST_PARALLELISM?=3 +TEST_PARALLELISM?=6 TESTARGS?=-short bin: diff --git a/README.md b/README.md index 06c08258..aab6d9a9 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ terraform { source = "RedisLabs/rediscloud" } } - required_version = ">= 0.13" + required_version = "~> 1.2" } ``` diff --git a/docs/data-sources/rediscloud_active_active_private_service_connect.md b/docs/data-sources/rediscloud_active_active_private_service_connect.md new file mode 100644 index 00000000..f8b594e8 --- /dev/null +++ b/docs/data-sources/rediscloud_active_active_private_service_connect.md @@ -0,0 +1,35 @@ +--- +layout: "rediscloud" +page_title: "Redis Cloud: rediscloud_active_active_private_service_connect" +description: |- + Active-Active Private Service Connect data source in the Redis Cloud Terraform provider. +--- + +# Data Source: rediscloud_active_active_private_service_connect + +The Active-Active Private Service Connect data source allows access to an available the Private Service Connect Service within your Redis Enterprise Subscription. + +## Example Usage + +```hcl +data "rediscloud_active_active_private_service_connect" "example" { + subscription_id = "1234" + region_id = 1 +} + +output "rediscloud_psc_status" { + value = data.rediscloud_active_active_private_service_connect.example.status +} +``` + +## Argument Reference + +* `subscription_id` - (Required) The ID of an Active-Active subscription +* `region_id` - (Required) The ID of the GCP region + +## Attribute Reference + +* `private_service_connect_service_id` - The ID of the Private Service Connect Service relative to the associated subscription +* `connection_host_name` - The connection hostname +* `service_attachment_name` - The service attachment name +* `status` - The Private Service Connect status diff --git a/docs/data-sources/rediscloud_active_active_private_service_connect_endpoints.md b/docs/data-sources/rediscloud_active_active_private_service_connect_endpoints.md new file mode 100644 index 00000000..0f9545c3 --- /dev/null +++ b/docs/data-sources/rediscloud_active_active_private_service_connect_endpoints.md @@ -0,0 +1,51 @@ +--- +layout: "rediscloud" +page_title: "Redis Cloud: rediscloud_active_active_private_service_connect_endpoints" +description: |- + Active-Active Private Service Connect Endpoints data source in the Redis Cloud Terraform provider. +--- + +# Data Source: rediscloud_active_active_private_service_connect_endpoints + +The Active-Active Private Service Connect Endpoints data source allows access to an available the endpoints within your Redis Enterprise Subscription. + +## Example Usage + +```hcl +data "rediscloud_active_active_private_service_connect_endpoints" "example" { + subscription_id = "1234" + private_service_connect_service_id = 5678 + region_id = 1 +} + +output "rediscloud_endpoints" { + value = data.rediscloud_active_active_private_service_connect.example.endpoints +} +``` + +## Argument Reference + +* `subscription_id` - (Required) The ID of an Active-Active subscription +* `private_service_connect_service_id` - (Required) The ID of the Private Service Connect Service relative to the associated subscription +* `region_id` - (Required) The ID of the GCP region + +## Attribute Reference + +* `endpoints` - List of Private Service Connect endpoints, documented below + +The `endpoints` object has these attributes: + +* `private_service_connect_endpoint_id` - The ID of the Private Service Connect endpoint +* `gcp_project_id` - The Google Cloud Project ID +* `gcp_vpc_name` - The GCP VPC name +* `gcp_vpc_subnet_name` - The GCP Subnet name +* `endpoint_connection_name` - The endpoint connection name +* `status` - The endpoint status +* `service_attachments` - The 40 service attachments that are created for the Private Service Connect endpoint, documented below + +The `service_attachments` object has these attributes: + +* `name` - Name of the service attachment +* `dns_record` - DNS record for the service attachment +* `ip_address_name` - IP address name for the service attachment +* `forwarding_rule_name` - Name of the forwarding rule for the service attachment diff --git a/docs/data-sources/rediscloud_private_service_connect.md b/docs/data-sources/rediscloud_private_service_connect.md new file mode 100644 index 00000000..57fa38f7 --- /dev/null +++ b/docs/data-sources/rediscloud_private_service_connect.md @@ -0,0 +1,33 @@ +--- +layout: "rediscloud" +page_title: "Redis Cloud: rediscloud_private_service_connect" +description: |- + Private Service Connect data source in the Redis Cloud Terraform provider. +--- + +# Data Source: rediscloud_private_service_connect + +The Private Service Connect data source allows access to an available the Private Service Connect Service within your Redis Enterprise Subscription. + +## Example Usage + +```hcl +data "rediscloud_private_service_connect" "example" { + subscription_id = "1234" +} + +output "rediscloud_psc_status" { + value = data.rediscloud_private_service_connect.example.status +} +``` + +## Argument Reference + +* `subscription_id` - (Required) The ID of a Pro subscription + +## Attribute Reference + +* `private_service_connect_service_id` - The ID of the Private Service Connect Service relative to the associated subscription +* `connection_host_name` - The connection hostname +* `service_attachment_name` - The service attachment name +* `status` - The Private Service Connect status diff --git a/docs/data-sources/rediscloud_private_service_connect_endpoints.md b/docs/data-sources/rediscloud_private_service_connect_endpoints.md new file mode 100644 index 00000000..c2bbce9c --- /dev/null +++ b/docs/data-sources/rediscloud_private_service_connect_endpoints.md @@ -0,0 +1,49 @@ +--- +layout: "rediscloud" +page_title: "Redis Cloud: rediscloud_private_service_connect_endpoints" +description: |- + Private Service Connect Endpoints data source in the Redis Cloud Terraform provider. +--- + +# Data Source: rediscloud_private_service_connect_endpoints + +The Private Service Connect Endpoints data source allows access to an available the endpoints within your Redis Enterprise Subscription. + +## Example Usage + +```hcl +data "rediscloud_private_service_connect_endpoints" "example" { + subscription_id = "1234" + private_service_connect_service_id = 5678 +} + +output "rediscloud_endpoints" { + value = data.rediscloud_private_service_connect.example.endpoints +} +``` + +## Argument Reference + +* `subscription_id` - (Required) The ID of a Pro subscription +* `private_service_connect_service_id` - (Required) The ID of the Private Service Connect Service relative to the associated subscription + +## Attribute Reference + +* `endpoints` - List of Private Service Connect endpoints, documented below + +The `endpoints` object has these attributes: + +* `private_service_connect_endpoint_id` - The ID of the Private Service Connect endpoint +* `gcp_project_id` - The Google Cloud Project ID +* `gcp_vpc_name` - The GCP VPC name +* `gcp_vpc_subnet_name` - The GCP Subnet name +* `endpoint_connection_name` - The endpoint connection name +* `status` - The endpoint status +* `service_attachments` - The 40 service attachments that are created for the Private Service Connect endpoint, documented below + +The `service_attachments` object has these attributes: + +* `name` - Name of the service attachment +* `dns_record` - DNS record for the service attachment +* `ip_address_name` - IP address name for the service attachment +* `forwarding_rule_name` - Name of the forwarding rule for the service attachment diff --git a/docs/resources/rediscloud_active_active_private_service_connect.md b/docs/resources/rediscloud_active_active_private_service_connect.md new file mode 100644 index 00000000..232fe041 --- /dev/null +++ b/docs/resources/rediscloud_active_active_private_service_connect.md @@ -0,0 +1,31 @@ +--- +layout: "rediscloud" +page_title: "Redis Cloud: rediscloud_active_active_private_service_connect" +description: |- + Private Service Connect resource for Active-Active Subscription in the Redis Cloud Terraform provider. +--- + +# Resource: rediscloud_active_active_private_service_connect + +Manages a Private Service Connect to an Active-Active Subscription in your Redis Enterprise Cloud Account. + +## Example Usage + +[Full example in the `rediscloud_active_active_private_service_connect_endpoint` resource](./rediscloud_active_active_private_service_connect_endpoint.md) + +## Argument Reference + +* `subscription_id` - (Required) The ID of the Pro subscription to attach **Modifying this attribute will force creation of a new resource.** +* `region_id` - (Required) The ID of the region, as created by the API **Modifying this attribute will force creation of a new resource.** + +## Attribute Reference + +* `private_service_connect_service_id` - The ID of the Private Service Connect Service relative to the associated subscription + +## Import + +`rediscloud_active_active_private_service_connect` can be imported using the ID of the Active-Active subscription, the region ID and the ID of the Private Service Connect in the format {subscription ID/region ID/private service connect ID}, e.g. + +``` +$ terraform import rediscloud_active_active_private_service_connect.id 1000/1/123456 +``` diff --git a/docs/resources/rediscloud_active_active_private_service_connect_endpoint.md b/docs/resources/rediscloud_active_active_private_service_connect_endpoint.md new file mode 100644 index 00000000..f12d6a0c --- /dev/null +++ b/docs/resources/rediscloud_active_active_private_service_connect_endpoint.md @@ -0,0 +1,203 @@ +--- +layout: "rediscloud" +page_title: "Redis Cloud: rediscloud_active_active_private_service_connect_endpoint" +description: |- + Private Service Connect Endpoint resource for Active-Active Subscription in the Redis Cloud Terraform provider. +--- + +# Resource: rediscloud_active_active_private_service_connect + +Manages a Private Service Connect to an Active-Active Subscription in your Redis Enterprise Cloud Account. + +## Example Usage + +The example below creates a Private Service Connect Endpoint in an Active-Active subscription, the respective GCP resources +and accepts the endpoint for the `us-central1` region. + +Please note that an endpoint can only be accepted after, the forwarding rules in GCP are created. + +```hcl +data "rediscloud_payment_method" "card" { + card_type = "Visa" +} + +resource "rediscloud_active_active_subscription" "subscription" { + name = "subscription-name" + payment_method_id = data.rediscloud_payment_method.card.id + cloud_provider = "GCP" + + creation_plan { + memory_limit_in_gb = 1 + quantity = 1 + region { + region = "us-central1" + networking_deployment_cidr = "192.168.0.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + region { + region = "europe-west1" + networking_deployment_cidr = "10.0.1.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + } +} + +resource "rediscloud_active_active_subscription_database" "database" { + subscription_id = rediscloud_active_active_subscription.subscription.id + name = "db" + memory_limit_in_gb = 1 + global_data_persistence = "aof-every-1-second" + global_password = "some-password" +} + +resource "rediscloud_active_active_subscription_regions" "regions" { + subscription_id = rediscloud_active_active_subscription.subscription.id + + region { + region = "us-central1" + networking_deployment_cidr = "192.168.0.0/24" + database { + database_id = rediscloud_active_active_subscription_database.database.db_id + database_name = rediscloud_active_active_subscription_database.database.name + local_write_operations_per_second = 1000 + local_read_operations_per_second = 1000 + } + } + + region { + region = "europe-west1" + networking_deployment_cidr = "10.0.1.0/24" + database { + database_id = rediscloud_active_active_subscription_database.database.db_id + database_name = rediscloud_active_active_subscription_database.database.name + local_write_operations_per_second = 1000 + local_read_operations_per_second = 1000 + } + } +} + +locals { + service_attachment_count = 40 # Each rediscloud_active_active_private_service_connect_endpoint will have exactly 40 service attachments + region_id = one([for r in rediscloud_active_active_subscription_regions.regions.region : r.region_id if r.region == var.gcp_region]) +} + +resource "rediscloud_active_active_private_service_connect" "service" { + subscription_id = rediscloud_active_active_subscription.subscription.id + region_id = local.region_id +} + +resource "rediscloud_active_active_private_service_connect_endpoint" "endpoint" { + subscription_id = rediscloud_active_active_subscription.subscription.id + region_id = local.region_id + private_service_connect_service_id = rediscloud_active_active_private_service_connect.service.private_service_connect_service_id + + gcp_project_id = var.gcp_project_id + gcp_vpc_name = var.gcp_vpc_name + gcp_vpc_subnet_name = var.gcp_subnet_name + endpoint_connection_name = "redis-${rediscloud_active_active_subscription.subscription.id}" +} + +data "google_compute_network" "network" { + project = var.gcp_project_id + name = var.gcp_vpc_name +} + +data "google_compute_subnetwork" "subnet" { + project = var.gcp_project_id + name = var.gcp_subnet_name + region = var.gcp_region +} + +resource "google_compute_address" "default" { + count = local.service_attachment_count + + project = var.gcp_project_id + name = rediscloud_active_active_private_service_connect_endpoint.endpoint.service_attachments[count.index].ip_address_name + subnetwork = data.google_compute_subnetwork.subnet.id + address_type = "INTERNAL" + region = var.gcp_region +} + +resource "google_compute_forwarding_rule" "default" { + count = local.service_attachment_count + + name = rediscloud_active_active_private_service_connect_endpoint.endpoint.service_attachments[count.index].forwarding_rule_name + project = var.gcp_project_id + region = var.gcp_region + ip_address = google_compute_address.default[count.index].id + network = var.gcp_vpc_name + target = rediscloud_active_active_private_service_connect_endpoint.endpoint.service_attachments[count.index].name + load_balancing_scheme = "" +} + +resource "google_dns_response_policy" "redis_response_policy" { + response_policy_name = "redis-${var.gcp_vpc_name}" + project = var.gcp_project_id + + networks { + network_url = data.google_compute_network.network.id + } +} + +resource "google_dns_response_policy_rule" "redis_response_policy_rules" { + count = local.service_attachment_count + + project = var.gcp_project_id + response_policy = google_dns_response_policy.redis_response_policy.response_policy_name + rule_name = "${rediscloud_active_active_private_service_connect_endpoint.endpoint.service_attachments[count.index].forwarding_rule_name}-${var.gcp_region}-rule" + dns_name = rediscloud_active_active_private_service_connect_endpoint.endpoint.service_attachments[count.index].dns_record + + local_data { + local_datas { + name = rediscloud_active_active_private_service_connect_endpoint.endpoint.service_attachments[count.index].dns_record + type = "A" + ttl = 300 + rrdatas = [google_compute_address.default[count.index].address] + } + } +} + +resource "rediscloud_active_active_private_service_connect_endpoint_accepter" "accepter" { + subscription_id = rediscloud_active_active_subscription.subscription.id + region_id = local.region_id + private_service_connect_service_id = rediscloud_active_active_private_service_connect.service.private_service_connect_service_id + private_service_connect_endpoint_id = rediscloud_active_active_private_service_connect_endpoint.endpoint.private_service_connect_endpoint_id + + action = "accept" + + depends_on = [google_compute_forwarding_rule.default] +} + +``` + +## Argument Reference + +* `subscription_id` - (Required) The ID of the Pro subscription to attach **Modifying this attribute will force creation of a new resource.** +* `region_id` - (Required) The ID of the region, as created by the API **Modifying this attribute will force creation of a new resource.** +* `private_service_connect_service_id` - (Required) The ID of the Private Service Connect Service relative to the associated subscription **Modifying this attribute will force creation of a new resource.** +* `gcp_project_id` - (Required) The Google Cloud Project ID **Modifying this attribute will force creation of a new resource.** +* `gcp_vpc_name` - (Required) The GCP VPC Network name **Modifying this attribute will force creation of a new resource.** +* `gcp_vpc_subnet_name` - (Required) The GCP Subnet name **Modifying this attribute will force creation of a new resource.** +* `endpoint_connection_name` - (Required) The endpoint connection name prefix. This prefix that will be used to create the Private Service Connect endpoint in your Google Cloud account **Modifying this attribute will force creation of a new resource.** + +## Attribute Reference + +* `private_service_connect_endpoint_id` - The ID of the Private Service Connect Endpoint +* `service_attachments` - The 40 service attachments that are created for the Private Service Connect endpoint, documented below + +The `service_attachments` object has these attributes: + +* `name` - Name of the service attachment +* `dns_record` - DNS record for the service attachment +* `ip_address_name` - IP address name for the service attachment +* `forwarding_rule_name` - Name of the forwarding rule for the service attachment + +## Import + +`rediscloud_active_active_private_service_connect_endpoint` can be imported using the ID of the Active-Active subscription, the region ID and the ID of the Private Service Connect in the format {subscription ID/region ID/private service connect ID//private service connect endpoint ID}, e.g. + +``` +$ terraform import rediscloud_active_active_private_service_connect_endpoint.id 1000/1/123456/654321 +``` diff --git a/docs/resources/rediscloud_active_active_private_service_connect_endpoint_accepter.md b/docs/resources/rediscloud_active_active_private_service_connect_endpoint_accepter.md new file mode 100644 index 00000000..7fd1acaa --- /dev/null +++ b/docs/resources/rediscloud_active_active_private_service_connect_endpoint_accepter.md @@ -0,0 +1,22 @@ +--- +layout: "rediscloud" +page_title: "Redis Cloud: rediscloud_active_active_private_service_connect_endpoint_accepter" +description: |- + Private Service Connect Endpoint Accepter resource for Active-Active Subscription in the Redis Cloud Terraform provider. +--- + +# Resource: rediscloud_active_active_private_service_connect_endpoint_accepter + +Manages a Private Service Connect Endpoint state in your Redis Enterprise Cloud Account. + +## Example Usage + +[Full example in the `rediscloud_active_active_private_service_connect_endpoint` resource](./rediscloud_active_active_private_service_connect_endpoint.md) + +## Argument Reference + +* `subscription_id` - (Required) The ID of the Pro subscription to attach **Modifying this attribute will force creation of a new resource.** +* `region_id` - (Required) The ID of the region, as created by the API **Modifying this attribute will force creation of a new resource.** +* `private_service_connect_service_id` - (Required) The ID of the Private Service Connect Service relative to the associated subscription **Modifying this attribute will force creation of a new resource.** +* `private_service_connect_endpoint_id` - (Required) The ID of the Private Service Connect Service relative to the associated subscription **Modifying this attribute will force creation of a new resource.** +* `action` - (Required) Accept or reject the endpoint (accepted values are `accept` and `reject`) diff --git a/docs/resources/rediscloud_active_active_subscription_peering.md b/docs/resources/rediscloud_active_active_subscription_peering.md index 17bbbc8c..9da44167 100644 --- a/docs/resources/rediscloud_active_active_subscription_peering.md +++ b/docs/resources/rediscloud_active_active_subscription_peering.md @@ -2,7 +2,7 @@ layout: "rediscloud" page_title: "Redis Cloud: rediscloud_active_active_subscription_peering" description: |- - Active-Active subscription VPC peering resource in the Redis Cloud Terraform provider. + Active-Active subscription VPC peering resource in the Redis Cloud Terraform provider. --- # Resource: rediscloud_active_active_subscription_peering diff --git a/docs/resources/rediscloud_private_service_connect.md b/docs/resources/rediscloud_private_service_connect.md new file mode 100644 index 00000000..db845899 --- /dev/null +++ b/docs/resources/rediscloud_private_service_connect.md @@ -0,0 +1,29 @@ +--- +layout: "rediscloud" +page_title: "Redis Cloud: rediscloud_private_service_connect" +description: |- + Private Service Connect resource for Pro Subscription in the Redis Cloud Terraform provider. +--- + +# Resource: rediscloud_private_service_connect + +Manages a Private Service Connect to a Pro Subscription in your Redis Enterprise Cloud Account. + +## Example Usage + +[Full example in the `rediscloud_private_service_connect_endpoint` resource](./rediscloud_private_service_connect_endpoint.md) + +## Argument Reference + +* `subscription_id` - (Required) The ID of the Pro subscription to attach **Modifying this attribute will force creation of a new resource.** + +## Attribute Reference + +* `private_service_connect_service_id` - The ID of the Private Service Connect Service relative to the associated subscription + +## Import +`rediscloud_private_service_connect` can be imported using the ID of the subscription and the ID of the Private Service Connect in the format {subscription ID/private service connect ID}, e.g. + +``` +$ terraform import rediscloud_private_service_connect.id 1000/123456 +``` diff --git a/docs/resources/rediscloud_private_service_connect_endpoint.md b/docs/resources/rediscloud_private_service_connect_endpoint.md new file mode 100644 index 00000000..972a0ac9 --- /dev/null +++ b/docs/resources/rediscloud_private_service_connect_endpoint.md @@ -0,0 +1,161 @@ +--- +layout: "rediscloud" +page_title: "Redis Cloud: rediscloud_private_service_connect" +description: |- + Private Service Connect Endpoint resource fo Pro Subscription in the Redis Cloud Terraform provider. +--- + +# Resource: rediscloud_private_service_connect + +Manages a Private Service Connect to a Pro subscription in your Redis Enterprise Cloud Account. + +## Example Usage + +The example below creates a Private Service Connect Endpoint in a Pro subscription, the respective GCP resources +and accepts the endpoint. + +Please note that an endpoint can only be accepted after, the forwarding rules in GCP are created. + +```hcl +data "rediscloud_payment_method" "card" { + card_type = "Visa" +} + +resource "rediscloud_subscription" "subscription" { + name = "subscription-name" + payment_method_id = data.rediscloud_payment_method.card.id + + cloud_provider { + provider = "GCP" + region { + region = var.gcp_region + networking_deployment_cidr = "10.0.1.0/24" + } + } + + creation_plan { + dataset_size_in_gb = 15 + quantity = 1 + replication = true + throughput_measurement_by = "operations-per-second" + throughput_measurement_value = 20000 + } +} + +locals { + service_attachment_count = 40 # Each rediscloud_private_service_connect_endpoint will have exactly 40 service attachments +} + +resource "rediscloud_private_service_connect" "service" { + subscription_id = rediscloud_subscription.subscription.id +} + +resource "rediscloud_private_service_connect_endpoint" "endpoint" { + subscription_id = rediscloud_subscription.subscription.id + private_service_connect_service_id = rediscloud_private_service_connect.service.private_service_connect_service_id + + gcp_project_id = var.gcp_project_id + gcp_vpc_name = var.gcp_vpc_name + gcp_vpc_subnet_name = var.gcp_subnet_name + endpoint_connection_name = "redis-${rediscloud_subscription.subscription.id}" +} + +data "google_compute_network" "network" { + project = var.gcp_project_id + name = var.gcp_vpc_name +} + +data "google_compute_subnetwork" "subnet" { + project = var.gcp_project_id + name = var.gcp_subnet_name + region = var.gcp_region +} + +resource "google_compute_address" "default" { + count = local.service_attachment_count + + project = var.gcp_project_id + name = rediscloud_private_service_connect_endpoint.endpoint.service_attachments[count.index].ip_address_name + subnetwork = data.google_compute_subnetwork.subnet.id + address_type = "INTERNAL" + region = var.gcp_region +} + +resource "google_compute_forwarding_rule" "default" { + count = local.service_attachment_count + + name = rediscloud_private_service_connect_endpoint.endpoint.service_attachments[count.index].forwarding_rule_name + project = var.gcp_project_id + region = var.gcp_region + ip_address = google_compute_address.default[count.index].id + network = var.gcp_vpc_name + target = rediscloud_private_service_connect_endpoint.endpoint.service_attachments[count.index].name + load_balancing_scheme = "" +} + +resource "google_dns_response_policy" "redis_response_policy" { + response_policy_name = "redis-${var.gcp_vpc_name}" + project = var.gcp_project_id + + networks { + network_url = data.google_compute_network.network.id + } +} + +resource "google_dns_response_policy_rule" "redis_response_policy_rules" { + count = local.service_attachment_count + + project = var.gcp_project_id + response_policy = google_dns_response_policy.redis_response_policy.response_policy_name + rule_name = "${rediscloud_private_service_connect_endpoint.endpoint.service_attachments[count.index].forwarding_rule_name}-${var.gcp_region}-rule" + dns_name = rediscloud_private_service_connect_endpoint.endpoint.service_attachments[count.index].dns_record + + local_data { + local_datas { + name = rediscloud_private_service_connect_endpoint.endpoint.service_attachments[count.index].dns_record + type = "A" + ttl = 300 + rrdatas = [google_compute_address.default[count.index].address] + } + } +} + +resource "rediscloud_private_service_connect_endpoint_accepter" "accepter" { + subscription_id = rediscloud_subscription.subscription.id + private_service_connect_service_id = rediscloud_private_service_connect.service.private_service_connect_service_id + private_service_connect_endpoint_id = rediscloud_private_service_connect_endpoint.endpoint.private_service_connect_endpoint_id + + action = "accept" + + depends_on = [google_compute_forwarding_rule.default] +} + +``` + +## Argument Reference + +* `subscription_id` - (Required) The ID of the Pro subscription to attach **Modifying this attribute will force creation of a new resource.** +* `private_service_connect_service_id` - (Required) The ID of the Private Service Connect Service relative to the associated subscription **Modifying this attribute will force creation of a new resource.** +* `gcp_project_id` - (Required) The Google Cloud Project ID **Modifying this attribute will force creation of a new resource.** +* `gcp_vpc_name` - (Required) The GCP VPC Network name **Modifying this attribute will force creation of a new resource.** +* `gcp_vpc_subnet_name` - (Required) The GCP Subnet name **Modifying this attribute will force creation of a new resource.** +* `endpoint_connection_name` - (Required) The endpoint connection name prefix. This prefix that will be used to create the Private Service Connect endpoint in your Google Cloud account **Modifying this attribute will force creation of a new resource.** + +## Attribute Reference + +* `private_service_connect_endpoint_id` - The ID of the Private Service Connect Endpoint +* `service_attachments` - The 40 service attachments that are created for the Private Service Connect endpoint, documented below + +The `service_attachments` object has these attributes: + +* `name` - Name of the service attachment +* `dns_record` - DNS record for the service attachment +* `ip_address_name` - IP address name for the service attachment +* `forwarding_rule_name` - Name of the forwarding rule for the service attachment + +## Import +`rediscloud_private_service_connect_endpoint` can be imported using the ID of the subscription and the ID of the Private Service Connect in the format {subscription ID/private service connect ID/private service connect endpoint ID}, e.g. + +``` +$ terraform import rediscloud_private_service_connect_endpoint.id 1000/123456/654321 +``` diff --git a/docs/resources/rediscloud_private_service_connect_endpoint_accepter.md b/docs/resources/rediscloud_private_service_connect_endpoint_accepter.md new file mode 100644 index 00000000..5a95ebea --- /dev/null +++ b/docs/resources/rediscloud_private_service_connect_endpoint_accepter.md @@ -0,0 +1,21 @@ +--- +layout: "rediscloud" +page_title: "Redis Cloud: rediscloud_private_service_connect_endpoint_accepter" +description: |- + Private Service Connect Endpoint Accepter resource for a Pro Subscription in the Redis Cloud Terraform provider. +--- + +# Resource: # Resource: rediscloud_private_service_connect_endpoint_accepter + +Manages a Private Service Connect Endpoint state in your Redis Enterprise Cloud Account. + +## Example Usage + +[Full example in the `rediscloud_private_service_connect_endpoint` resource](./rediscloud_private_service_connect_endpoint.md) + +## Argument Reference + +* `subscription_id` - (Required) The ID of the Pro subscription to attach **Modifying this attribute will force creation of a new resource.** +* `private_service_connect_service_id` - (Required) The ID of the Private Service Connect Service relative to the associated subscription **Modifying this attribute will force creation of a new resource.** +* `private_service_connect_endpoint_id` - (Required) The ID of the Private Service Connect Service relative to the associated subscription **Modifying this attribute will force creation of a new resource.** +* `action` - (Required) Accept or reject the endpoint (accepted values are `accept` and `reject`) diff --git a/go.mod b/go.mod index ea499efd..34642cdd 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/RedisLabs/terraform-provider-rediscloud go 1.22.4 require ( - github.com/RedisLabs/rediscloud-go-api v0.21.0 + github.com/RedisLabs/rediscloud-go-api v0.22.0 github.com/bflad/tfproviderlint v0.30.0 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 - github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 - github.com/stretchr/testify v1.9.0 + github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0 + github.com/stretchr/testify v1.10.0 ) require ( @@ -24,17 +24,18 @@ require ( github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.6.0 // indirect + github.com/hashicorp/go-plugin v1.6.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/hashicorp/go-version v1.6.0 // indirect - github.com/hashicorp/hc-install v0.6.4 // indirect - github.com/hashicorp/hcl/v2 v2.20.1 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hc-install v0.9.0 // indirect + github.com/hashicorp/hcl/v2 v2.22.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.21.0 // indirect - github.com/hashicorp/terraform-json v0.22.1 // indirect - github.com/hashicorp/terraform-plugin-go v0.23.0 // indirect + github.com/hashicorp/terraform-json v0.23.0 // indirect + github.com/hashicorp/terraform-plugin-go v0.25.0 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect @@ -51,17 +52,17 @@ require ( github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/zclconf/go-cty v1.14.4 // indirect - golang.org/x/crypto v0.26.0 // indirect - golang.org/x/mod v0.20.0 // indirect - golang.org/x/net v0.28.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.23.0 // indirect - golang.org/x/text v0.17.0 // indirect - golang.org/x/tools v0.24.0 // indirect + github.com/zclconf/go-cty v1.15.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/tools v0.29.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/grpc v1.63.2 // indirect - google.golang.org/protobuf v1.34.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3927b815..e9a0c69a 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg= github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/RedisLabs/rediscloud-go-api v0.21.0 h1:4f5fz7cfP8zRtmzR4sW7dwmLnxgKgUInxTBMGmc6gFE= -github.com/RedisLabs/rediscloud-go-api v0.21.0/go.mod h1:3/oVb71rv2OstFRYEc65QCIbfwnJTgZeQhtPCcdHook= +github.com/RedisLabs/rediscloud-go-api v0.22.0 h1:Tb3vMtCq7ks5kpniF/Rkod7TvHhcXZ2oYJJT3pMgyQ8= +github.com/RedisLabs/rediscloud-go-api v0.22.0/go.mod h1:3/oVb71rv2OstFRYEc65QCIbfwnJTgZeQhtPCcdHook= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= @@ -59,33 +59,35 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= -github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= -github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= -github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= +github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= +github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hc-install v0.6.4 h1:QLqlM56/+SIIGvGcfFiwMY3z5WGXT066suo/v9Km8e0= -github.com/hashicorp/hc-install v0.6.4/go.mod h1:05LWLy8TD842OtgcfBbOT0WMoInBMUSHjmDx10zuBIA= -github.com/hashicorp/hcl/v2 v2.20.1 h1:M6hgdyz7HYt1UN9e61j+qKJBqR3orTWbI1HKBJEdxtc= -github.com/hashicorp/hcl/v2 v2.20.1/go.mod h1:TZDqQ4kNKCbh1iJp99FdPiUaVDDUPivbqxZulxDYqL4= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hc-install v0.9.0 h1:2dIk8LcvANwtv3QZLckxcjyF5w8KVtiMxu6G6eLhghE= +github.com/hashicorp/hc-install v0.9.0/go.mod h1:+6vOP+mf3tuGgMApVYtmsnDoKWMDcFXeTxCACYZ8SFg= +github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= +github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ= github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg= -github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec= -github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= -github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= -github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= +github.com/hashicorp/terraform-json v0.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2xoR+lppBkI= +github.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c= +github.com/hashicorp/terraform-plugin-go v0.25.0 h1:oi13cx7xXA6QciMcpcFi/rwA974rdTxjqEhXJjbAyks= +github.com/hashicorp/terraform-plugin-go v0.25.0/go.mod h1:+SYagMYadJP86Kvn+TGeV+ofr/R3g4/If0O5sO96MVw= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 h1:kJiWGx2kiQVo97Y5IOGR4EMcZ8DtMswHhUuFibsCQQE= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0/go.mod h1:sl/UoabMc37HA6ICVMmGO+/0wofkVIRxf+BMb/dnoIg= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0 h1:wyKCCtn6pBBL46c1uIIBNUOWlNfYXfXpVo16iDyLp8Y= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0/go.mod h1:B0Al8NyYVr8Mp/KLwssKXG1RqnTk7FySqSn4fRuLNgw= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= @@ -136,8 +138,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= @@ -148,30 +150,30 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= -github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= -github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= -github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= +github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ= +github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= -golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -185,39 +187,39 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200214201135-548b770e2dfa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= +golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= -google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/provider/datasource_rediscloud_active_active_private_service_connect.go b/provider/datasource_rediscloud_active_active_private_service_connect.go new file mode 100644 index 00000000..371231b0 --- /dev/null +++ b/provider/datasource_rediscloud_active_active_private_service_connect.go @@ -0,0 +1,81 @@ +package provider + +import ( + "context" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceActiveActivePrivateServiceConnect() *schema.Resource { + return &schema.Resource{ + Description: "The Active-Active Private Service Connect data source allows access to an available Private Service Connect Service within your Redis Enterprise Cloud Account.", + ReadContext: dataSourceActiveActivePrivateServiceConnectRead, + + Schema: map[string]*schema.Schema{ + "subscription_id": { + Description: "The ID of an Active-Active subscription", + Type: schema.TypeString, + Required: true, + }, + "region_id": { + Description: "The ID of the GCP region", + Type: schema.TypeInt, + Required: true, + }, + "private_service_connect_service_id": { + Description: "The ID of the Private Service Connect Service relative to the associated subscription", + Type: schema.TypeInt, + Computed: true, + }, + "connection_host_name": { + Description: "The connection host name", + Type: schema.TypeString, + Computed: true, + }, + "service_attachment_name": { + Description: "The service attachment name", + Type: schema.TypeString, + Computed: true, + }, + "status": { + Description: "The Private Service Connect status", + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceActiveActivePrivateServiceConnectRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + api := meta.(*apiClient) + + subId, err := strconv.Atoi(d.Get("subscription_id").(string)) + if err != nil { + return diag.FromErr(err) + } + + regionId := d.Get("region_id").(int) + pscService, err := api.client.PrivateServiceConnect.GetActiveActiveService(ctx, subId, regionId) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(buildPrivateServiceConnectActiveActiveId(subId, regionId, *pscService.ID)) + if err := d.Set("private_service_connect_service_id", pscService.ID); err != nil { + return diag.FromErr(err) + } + if err := d.Set("connection_host_name", pscService.ConnectionHostName); err != nil { + return diag.FromErr(err) + } + if err := d.Set("service_attachment_name", pscService.ServiceAttachmentName); err != nil { + return diag.FromErr(err) + } + if err := d.Set("status", pscService.Status); err != nil { + return diag.FromErr(err) + } + + return diags +} diff --git a/provider/datasource_rediscloud_active_active_private_service_connect_endpoints.go b/provider/datasource_rediscloud_active_active_private_service_connect_endpoints.go new file mode 100644 index 00000000..8eaafb17 --- /dev/null +++ b/provider/datasource_rediscloud_active_active_private_service_connect_endpoints.go @@ -0,0 +1,141 @@ +package provider + +import ( + "context" + "strconv" + + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/rediscloud-go-api/service/psc" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceActiveActivePrivateServiceConnectEndpoints() *schema.Resource { + return &schema.Resource{ + Description: "The Active-Active Private Service Connect Endpoints data source allows access to an available endpoints on a Private Service Connect Service within your Redis Enterprise Cloud Account.", + ReadContext: dataSourceActiveActivePrivateServiceConnectEndpointsRead, + + Schema: map[string]*schema.Schema{ + "subscription_id": { + Description: "The ID of an Active-Active subscription", + Type: schema.TypeString, + Required: true, + }, + "region_id": { + Description: "The ID of the GCP region", + Type: schema.TypeInt, + Required: true, + }, + "private_service_connect_service_id": { + Description: "The ID of the Private Service Connect Service relative to the associated subscription", + Type: schema.TypeInt, + Required: true, + }, + "endpoints": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "private_service_connect_endpoint_id": { + Description: "The ID of the Private Service Connect endpoint", + Type: schema.TypeInt, + Computed: true, + }, + "gcp_project_id": { + Description: "The Google Cloud Project ID", + Type: schema.TypeString, + Computed: true, + }, + "gcp_vpc_name": { + Description: "The GCP VPC name", + Type: schema.TypeString, + Computed: true, + }, + "gcp_vpc_subnet_name": { + Description: "The GCP Subnet name", + Type: schema.TypeString, + Computed: true, + }, + "endpoint_connection_name": { + Description: "The endpoint connection name", + Type: schema.TypeString, + Computed: true, + }, + "status": { + Description: "The endpoint status", + Type: schema.TypeString, + Computed: true, + }, + "service_attachments": { + Description: "The service attachments that were created for the Private Service Connect endpoint", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Description: "Name of the service attachment", + Type: schema.TypeString, + Computed: true, + }, + "dns_record": { + Description: "DNS record for the service attachment", + Type: schema.TypeString, + Computed: true, + }, + "ip_address_name": { + Description: "IP address name for the service attachment", + Type: schema.TypeString, + Computed: true, + }, + "forwarding_rule_name": { + Description: "Name of the forwarding rule for the service attachment", + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func dataSourceActiveActivePrivateServiceConnectEndpointsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + api := meta.(*apiClient) + + subId, err := strconv.Atoi(d.Get("subscription_id").(string)) + if err != nil { + return diag.FromErr(err) + } + + regionId := d.Get("region_id").(int) + pscServiceId := d.Get("private_service_connect_service_id").(int) + + endpoints, err := api.client.PrivateServiceConnect.GetActiveActiveEndpoints(ctx, subId, regionId, pscServiceId) + if err != nil { + return diag.FromErr(err) + } + + serviceAttachments := map[int][]psc.TerraformGCPServiceAttachment{} + for _, endpoint := range endpoints.Endpoints { + serviceAttachments[*endpoint.ID] = []psc.TerraformGCPServiceAttachment{} + if redis.StringValue(endpoint.Status) != psc.EndpointStatusRejected && redis.StringValue(endpoint.Status) != psc.EndpointStatusDeleted { + script, err := api.client.PrivateServiceConnect.GetActiveActiveEndpointCreationScripts(ctx, subId, regionId, pscServiceId, redis.IntValue(endpoint.ID), true) + if err != nil { + return diag.FromErr(err) + } + serviceAttachments[*endpoint.ID] = script.Script.TerraformGcp.ServiceAttachments + } + } + + if err := d.Set("endpoints", flattenPrivateServiceConnectEndpoints(endpoints.Endpoints, serviceAttachments)); err != nil { + return diag.FromErr(err) + } + + d.SetId(buildPrivateServiceConnectActiveActiveId(subId, regionId, pscServiceId)) + + return diags +} diff --git a/provider/datasource_rediscloud_private_service_connect.go b/provider/datasource_rediscloud_private_service_connect.go new file mode 100644 index 00000000..4ff8027b --- /dev/null +++ b/provider/datasource_rediscloud_private_service_connect.go @@ -0,0 +1,75 @@ +package provider + +import ( + "context" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourcePrivateServiceConnect() *schema.Resource { + return &schema.Resource{ + Description: "The Private Service Connect data source allows access to an available Private Service Connect Service within your Redis Enterprise Cloud Account.", + ReadContext: dataSourcePrivateServiceConnectRead, + + Schema: map[string]*schema.Schema{ + "subscription_id": { + Description: "The ID of a Pro subscription", + Type: schema.TypeString, + Required: true, + }, + "private_service_connect_service_id": { + Description: "The ID of the Private Service Connect Service relative to the associated subscription", + Type: schema.TypeInt, + Computed: true, + }, + "connection_host_name": { + Description: "The connection host name", + Type: schema.TypeString, + Computed: true, + }, + "service_attachment_name": { + Description: "The service attachment name", + Type: schema.TypeString, + Computed: true, + }, + "status": { + Description: "The Private Service Connect status", + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourcePrivateServiceConnectRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + api := meta.(*apiClient) + + subId, err := strconv.Atoi(d.Get("subscription_id").(string)) + if err != nil { + return diag.FromErr(err) + } + + pscService, err := api.client.PrivateServiceConnect.GetService(ctx, subId) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(buildPrivateServiceConnectId(subId, *pscService.ID)) + if err := d.Set("private_service_connect_service_id", pscService.ID); err != nil { + return diag.FromErr(err) + } + if err := d.Set("connection_host_name", pscService.ConnectionHostName); err != nil { + return diag.FromErr(err) + } + if err := d.Set("service_attachment_name", pscService.ServiceAttachmentName); err != nil { + return diag.FromErr(err) + } + if err := d.Set("status", pscService.Status); err != nil { + return diag.FromErr(err) + } + + return diags +} diff --git a/provider/datasource_rediscloud_private_service_connect_endpoints.go b/provider/datasource_rediscloud_private_service_connect_endpoints.go new file mode 100644 index 00000000..a35317e2 --- /dev/null +++ b/provider/datasource_rediscloud_private_service_connect_endpoints.go @@ -0,0 +1,157 @@ +package provider + +import ( + "context" + "strconv" + + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/rediscloud-go-api/service/psc" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourcePrivateServiceConnectEndpoints() *schema.Resource { + return &schema.Resource{ + Description: "The Private Service Connect Endpoints data source allows access to an available endpoints on a Private Service Connect Service within your Redis Enterprise Cloud Account.", + ReadContext: dataSourcePrivateServiceConnectEndpointsRead, + + Schema: map[string]*schema.Schema{ + "subscription_id": { + Description: "The ID of a Pro subscription", + Type: schema.TypeString, + Required: true, + }, + "private_service_connect_service_id": { + Description: "The ID of the Private Service Connect Service relative to the associated subscription", + Type: schema.TypeInt, + Required: true, + }, + "endpoints": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "private_service_connect_endpoint_id": { + Description: "The ID of the Private Service Connect endpoint", + Type: schema.TypeInt, + Computed: true, + }, + "gcp_project_id": { + Description: "The Google Cloud Project ID", + Type: schema.TypeString, + Computed: true, + }, + "gcp_vpc_name": { + Description: "The GCP VPC name", + Type: schema.TypeString, + Computed: true, + }, + "gcp_vpc_subnet_name": { + Description: "The GCP Subnet name", + Type: schema.TypeString, + Computed: true, + }, + "endpoint_connection_name": { + Description: "The endpoint connection name", + Type: schema.TypeString, + Computed: true, + }, + "status": { + Description: "The endpoint status", + Type: schema.TypeString, + Computed: true, + }, + "service_attachments": { + Description: "The service attachments that were created for the Private Service Connect endpoint", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Description: "Name of the service attachment", + Type: schema.TypeString, + Computed: true, + }, + "dns_record": { + Description: "DNS record for the service attachment", + Type: schema.TypeString, + Computed: true, + }, + "ip_address_name": { + Description: "IP address name for the service attachment", + Type: schema.TypeString, + Computed: true, + }, + "forwarding_rule_name": { + Description: "Name of the forwarding rule for the service attachment", + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func dataSourcePrivateServiceConnectEndpointsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + api := meta.(*apiClient) + + subId, err := strconv.Atoi(d.Get("subscription_id").(string)) + if err != nil { + return diag.FromErr(err) + } + + pscServiceId := d.Get("private_service_connect_service_id").(int) + + endpoints, err := api.client.PrivateServiceConnect.GetEndpoints(ctx, subId, pscServiceId) + if err != nil { + return diag.FromErr(err) + } + + serviceAttachments := map[int][]psc.TerraformGCPServiceAttachment{} + for _, endpoint := range endpoints.Endpoints { + serviceAttachments[*endpoint.ID] = []psc.TerraformGCPServiceAttachment{} + if redis.StringValue(endpoint.Status) != psc.EndpointStatusRejected && redis.StringValue(endpoint.Status) != psc.EndpointStatusDeleted { + script, err := api.client.PrivateServiceConnect.GetEndpointCreationScripts(ctx, subId, pscServiceId, redis.IntValue(endpoint.ID), true) + if err != nil { + return diag.FromErr(err) + } + serviceAttachments[*endpoint.ID] = script.Script.TerraformGcp.ServiceAttachments + } + } + + if err := d.Set("endpoints", flattenPrivateServiceConnectEndpoints(endpoints.Endpoints, serviceAttachments)); err != nil { + return diag.FromErr(err) + } + + d.SetId(buildPrivateServiceConnectId(subId, pscServiceId)) + + return diags +} + +func flattenPrivateServiceConnectEndpoints(endpoints []*psc.PrivateServiceConnectEndpoint, + serviceAttachments map[int][]psc.TerraformGCPServiceAttachment) []map[string]interface{} { + + var rl []map[string]interface{} + for _, endpoint := range endpoints { + + endpointMapString := map[string]interface{}{ + "private_service_connect_endpoint_id": redis.IntValue(endpoint.ID), + "gcp_project_id": redis.StringValue(endpoint.GCPProjectID), + "gcp_vpc_name": redis.StringValue(endpoint.GCPVPCName), + "gcp_vpc_subnet_name": redis.StringValue(endpoint.GCPVPCSubnetName), + "endpoint_connection_name": redis.StringValue(endpoint.EndpointConnectionName), + "status": redis.StringValue(endpoint.Status), + "service_attachments": flattenPrivateServiceConnectEndpointServiceAttachments(serviceAttachments[redis.IntValue(endpoint.ID)]), + } + + rl = append(rl, endpointMapString) + } + + return rl +} diff --git a/provider/provider.go b/provider/provider.go index 149a7559..8e660bad 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -46,24 +46,28 @@ func New(version string) func() *schema.Provider { // Note the difference in public data-source name and the file/method name. // This is to help the developer relate their changes to what they would see happening in the Redis Console. // == flexible == pro - "rediscloud_subscription": dataSourceRedisCloudProSubscription(), - "rediscloud_database": dataSourceRedisCloudProDatabase(), - "rediscloud_database_modules": dataSourceRedisCloudDatabaseModules(), - "rediscloud_payment_method": dataSourceRedisCloudPaymentMethod(), - "rediscloud_regions": dataSourceRedisCloudRegions(), - "rediscloud_essentials_plan": dataSourceRedisCloudEssentialsPlan(), - "rediscloud_essentials_subscription": dataSourceRedisCloudEssentialsSubscription(), - "rediscloud_essentials_database": dataSourceRedisCloudEssentialsDatabase(), - "rediscloud_subscription_peerings": dataSourceRedisCloudSubscriptionPeerings(), - "rediscloud_active_active_subscription": dataSourceRedisCloudActiveActiveSubscription(), + "rediscloud_subscription": dataSourceRedisCloudProSubscription(), + "rediscloud_database": dataSourceRedisCloudProDatabase(), + "rediscloud_database_modules": dataSourceRedisCloudDatabaseModules(), + "rediscloud_payment_method": dataSourceRedisCloudPaymentMethod(), + "rediscloud_regions": dataSourceRedisCloudRegions(), + "rediscloud_essentials_plan": dataSourceRedisCloudEssentialsPlan(), + "rediscloud_essentials_subscription": dataSourceRedisCloudEssentialsSubscription(), + "rediscloud_essentials_database": dataSourceRedisCloudEssentialsDatabase(), + "rediscloud_subscription_peerings": dataSourceRedisCloudSubscriptionPeerings(), + "rediscloud_private_service_connect": dataSourcePrivateServiceConnect(), + "rediscloud_private_service_connect_endpoints": dataSourcePrivateServiceConnectEndpoints(), + "rediscloud_active_active_subscription": dataSourceRedisCloudActiveActiveSubscription(), // Note the difference in public data-source name and the file/method name. // active_active_subscription_database == active_active_database - "rediscloud_active_active_subscription_database": dataSourceRedisCloudActiveActiveDatabase(), - "rediscloud_transit_gateway": dataSourceTransitGateway(), - "rediscloud_active_active_transit_gateway": dataSourceActiveActiveTransitGateway(), - "rediscloud_acl_rule": dataSourceRedisCloudAclRule(), - "rediscloud_acl_role": dataSourceRedisCloudAclRole(), - "rediscloud_acl_user": dataSourceRedisCloudAclUser(), + "rediscloud_active_active_subscription_database": dataSourceRedisCloudActiveActiveDatabase(), + "rediscloud_active_active_private_service_connect": dataSourceActiveActivePrivateServiceConnect(), + "rediscloud_active_active_private_service_connect_endpoints": dataSourceActiveActivePrivateServiceConnectEndpoints(), + "rediscloud_transit_gateway": dataSourceTransitGateway(), + "rediscloud_active_active_transit_gateway": dataSourceActiveActiveTransitGateway(), + "rediscloud_acl_rule": dataSourceRedisCloudAclRule(), + "rediscloud_acl_role": dataSourceRedisCloudAclRole(), + "rediscloud_acl_user": dataSourceRedisCloudAclUser(), }, ResourcesMap: map[string]*schema.Resource{ "rediscloud_cloud_account": resourceRedisCloudCloudAccount(), @@ -71,20 +75,26 @@ func New(version string) func() *schema.Provider { "rediscloud_essentials_database": resourceRedisCloudEssentialsDatabase(), // Note the difference in public resource name and the file/method name. // == flexible == pro - "rediscloud_subscription": resourceRedisCloudProSubscription(), - "rediscloud_subscription_database": resourceRedisCloudProDatabase(), - "rediscloud_subscription_peering": resourceRedisCloudSubscriptionPeering(), - "rediscloud_active_active_subscription": resourceRedisCloudActiveActiveSubscription(), + "rediscloud_subscription": resourceRedisCloudProSubscription(), + "rediscloud_subscription_database": resourceRedisCloudProDatabase(), + "rediscloud_subscription_peering": resourceRedisCloudSubscriptionPeering(), + "rediscloud_private_service_connect": resourceRedisCloudPrivateServiceConnect(), + "rediscloud_private_service_connect_endpoint": resourceRedisCloudPrivateServiceConnectEndpoint(), + "rediscloud_private_service_connect_endpoint_accepter": resourceRedisCloudPrivateServiceConnectEndpointAccepter(), + "rediscloud_active_active_subscription": resourceRedisCloudActiveActiveSubscription(), // Note the difference in public resource name and the file/method name. // active_active_subscription_database == active_active_database - "rediscloud_active_active_subscription_database": resourceRedisCloudActiveActiveDatabase(), - "rediscloud_active_active_subscription_regions": resourceRedisCloudActiveActiveSubscriptionRegions(), - "rediscloud_active_active_subscription_peering": resourceRedisCloudActiveActiveSubscriptionPeering(), - "rediscloud_transit_gateway_attachment": resourceRedisCloudTransitGatewayAttachment(), - "rediscloud_active_active_transit_gateway_attachment": resourceRedisCloudActiveActiveTransitGatewayAttachment(), - "rediscloud_acl_rule": resourceRedisCloudAclRule(), - "rediscloud_acl_role": resourceRedisCloudAclRole(), - "rediscloud_acl_user": resourceRedisCloudAclUser(), + "rediscloud_active_active_subscription_database": resourceRedisCloudActiveActiveDatabase(), + "rediscloud_active_active_subscription_regions": resourceRedisCloudActiveActiveSubscriptionRegions(), + "rediscloud_active_active_subscription_peering": resourceRedisCloudActiveActiveSubscriptionPeering(), + "rediscloud_active_active_private_service_connect": resourceRedisCloudActiveActivePrivateServiceConnect(), + "rediscloud_active_active_private_service_connect_endpoint": resourceRedisCloudActiveActivePrivateServiceConnectEndpoint(), + "rediscloud_active_active_private_service_connect_endpoint_accepter": resourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepter(), + "rediscloud_transit_gateway_attachment": resourceRedisCloudTransitGatewayAttachment(), + "rediscloud_active_active_transit_gateway_attachment": resourceRedisCloudActiveActiveTransitGatewayAttachment(), + "rediscloud_acl_rule": resourceRedisCloudAclRule(), + "rediscloud_acl_role": resourceRedisCloudAclRole(), + "rediscloud_acl_user": resourceRedisCloudAclUser(), }, } diff --git a/provider/provider_test.go b/provider/provider_test.go index 57ed81d1..76d00940 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -1,10 +1,11 @@ package provider import ( - rediscloudApi "github.com/RedisLabs/rediscloud-go-api" "os" "testing" + rediscloudApi "github.com/RedisLabs/rediscloud-go-api" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -42,6 +43,14 @@ func testAccAwsPeeringPreCheck(t *testing.T) { requireEnvironmentVariables(t, "AWS_PEERING_REGION", "AWS_ACCOUNT_ID", "AWS_VPC_ID", "AWS_VPC_CIDR") } +func testAccGcpProjectPreCheck(t *testing.T) { + requireEnvironmentVariables(t, "GCP_VPC_PROJECT") +} + +func testAccGcpCredentialsPreCheck(t *testing.T) { + requireEnvironmentVariables(t, "GOOGLE_APPLICATION_CREDENTIALS") +} + func testAccAwsPreExistingTgwCheck(t *testing.T) { requireEnvironmentVariables(t, "AWS_TEST_TGW_ID") } diff --git a/provider/rediscloud_active_active_private_service_connect_endpoint_accepter_test.go b/provider/rediscloud_active_active_private_service_connect_endpoint_accepter_test.go new file mode 100644 index 00000000..96dda986 --- /dev/null +++ b/provider/rediscloud_active_active_private_service_connect_endpoint_accepter_test.go @@ -0,0 +1,197 @@ +package provider + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/rediscloud-go-api/service/psc" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccResourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepter_Create(t *testing.T) { + + baseName := acctest.RandomWithPrefix(testResourcePrefix) + "-pro-pscea" + + const resourceName = "rediscloud_active_active_private_service_connect_endpoint_accepter.accepter" + gcpProjectId := os.Getenv("GCP_VPC_PROJECT") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccGcpProjectPreCheck(t); testAccGcpCredentialsPreCheck(t) }, + ProviderFactories: providerFactories, + ExternalProviders: map[string]resource.ExternalProvider{ + "google": { + Source: "hashicorp/google", + VersionConstraint: "~> 6.5", + }, + }, + CheckDestroy: testAccCheckProSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccResourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepterPro, baseName, gcpProjectId), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "id"), + func(s *terraform.State) error { + r := s.RootModule().Resources[resourceName] + + accepterId, err := toPscEndpointActiveActiveAccepterId(r.Primary.ID) + if err != nil { + return fmt.Errorf("couldn't parse the accepter ID: %s", r.Primary.ID) + } + + client := testProvider.Meta().(*apiClient) + endpoints, err := client.client.PrivateServiceConnect.GetActiveActiveEndpoints(context.TODO(), + accepterId.subscriptionId, accepterId.regionId, accepterId.pscServiceId) + if err != nil { + return err + } + + endpoint := findPrivateServiceConnectEndpoints(accepterId.endpointId, endpoints.Endpoints) + if endpoint == nil { + return fmt.Errorf("couldn't find endpoint with ID: %d", accepterId.endpointId) + } + + if redis.StringValue(endpoint.Status) != psc.EndpointStatusActive { + return fmt.Errorf("expected endpoint status to be active - current status %s", redis.StringValue(endpoint.Status)) + } + + return nil + }, + ), + }, + }, + }) +} + +const testAccResourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepterPro = ` +data "rediscloud_payment_method" "card" { + card_type = "Visa" +} + +resource "rediscloud_active_active_subscription" "subscription" { + name = "%s" + payment_method_id = data.rediscloud_payment_method.card.id + cloud_provider = "GCP" + + creation_plan { + memory_limit_in_gb = 1 + quantity = 1 + region { + region = "us-central1" + networking_deployment_cidr = "192.168.0.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + region { + region = "europe-west1" + networking_deployment_cidr = "10.0.1.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + } +} + +resource "rediscloud_active_active_subscription_database" "database" { + subscription_id = rediscloud_active_active_subscription.subscription.id + name = "db" + memory_limit_in_gb = 1 + global_data_persistence = "aof-every-1-second" + global_password = "some-password" +} + +resource "rediscloud_active_active_subscription_regions" "regions" { + subscription_id = rediscloud_active_active_subscription.subscription.id + + region { + region = "us-central1" + networking_deployment_cidr = "192.168.0.0/24" + database { + database_id = rediscloud_active_active_subscription_database.database.db_id + database_name = rediscloud_active_active_subscription_database.database.name + local_write_operations_per_second = 1000 + local_read_operations_per_second = 1000 + } + } + + region { + region = "europe-west1" + networking_deployment_cidr = "10.0.1.0/24" + database { + database_id = rediscloud_active_active_subscription_database.database.db_id + database_name = rediscloud_active_active_subscription_database.database.name + local_write_operations_per_second = 1000 + local_read_operations_per_second = 1000 + } + } +} + +resource "rediscloud_active_active_private_service_connect" "psc" { + subscription_id = rediscloud_active_active_subscription.subscription.id + region_id = one([for r in rediscloud_active_active_subscription_regions.regions.region : r.region_id if r.region == "us-central1"]) +} + +resource "rediscloud_active_active_private_service_connect_endpoint" "psce" { + subscription_id = rediscloud_active_active_subscription.subscription.id + region_id = one([for r in rediscloud_active_active_subscription_regions.regions.region : r.region_id if r.region == "us-central1"]) + private_service_connect_service_id = rediscloud_active_active_private_service_connect.psc.private_service_connect_service_id + gcp_project_id = "%[2]s" + gcp_vpc_name = google_compute_network.network.name + gcp_vpc_subnet_name = google_compute_subnetwork.subnet.name + endpoint_connection_name = "redis-${rediscloud_active_active_subscription.subscription.id}" +} + +resource "google_compute_network" "network" { + project = "%[2]s" + name = "%[1]s" + auto_create_subnetworks = false +} + +resource "google_compute_subnetwork" "subnet" { + project = "%[2]s" + name = "%[1]s" + ip_cidr_range = "192.168.1.0/24" + region = "us-central1" + network = google_compute_network.network.id +} + +locals { + service_attachment_count = 40 +} + +resource "google_compute_address" "default" { + count = local.service_attachment_count + + project = "%[2]s" + name = rediscloud_active_active_private_service_connect_endpoint.psce.service_attachments[count.index].ip_address_name + subnetwork = google_compute_subnetwork.subnet.id + address_type = "INTERNAL" + region = "us-central1" +} + +resource "google_compute_forwarding_rule" "default" { + count = local.service_attachment_count + + name = rediscloud_active_active_private_service_connect_endpoint.psce.service_attachments[count.index].forwarding_rule_name + project = "%[2]s" + region = "us-central1" + ip_address = google_compute_address.default[count.index].id + network = google_compute_network.network.name + target = rediscloud_active_active_private_service_connect_endpoint.psce.service_attachments[count.index].name + load_balancing_scheme = "" +} + +resource "rediscloud_active_active_private_service_connect_endpoint_accepter" "accepter" { + subscription_id = rediscloud_active_active_subscription.subscription.id + region_id = one([for r in rediscloud_active_active_subscription_regions.regions.region : r.region_id if r.region == "us-central1"]) + private_service_connect_service_id = rediscloud_active_active_private_service_connect.psc.private_service_connect_service_id + private_service_connect_endpoint_id = rediscloud_active_active_private_service_connect_endpoint.psce.private_service_connect_endpoint_id + + action = "accept" + + depends_on = [google_compute_forwarding_rule.default] +} +` diff --git a/provider/rediscloud_active_active_private_service_connect_endpoint_test.go b/provider/rediscloud_active_active_private_service_connect_endpoint_test.go new file mode 100644 index 00000000..826c00ab --- /dev/null +++ b/provider/rediscloud_active_active_private_service_connect_endpoint_test.go @@ -0,0 +1,145 @@ +package provider + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccResourceRedisCloudActiveActivePrivateServiceConnectEndpoint_CRUDI(t *testing.T) { + + baseName := acctest.RandomWithPrefix(testResourcePrefix) + "-pro-psce" + + const resourceName = "rediscloud_active_active_private_service_connect_endpoint.psce" + const datasourceName = "data.rediscloud_active_active_private_service_connect_endpoints.psce" + gcpProjectId := os.Getenv("GCP_VPC_PROJECT") + gcpVPCName := fmt.Sprintf("%s-network", baseName) + gcpVPCSubnetName := fmt.Sprintf("%s-subnet", baseName) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccGcpProjectPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckActiveActiveSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccResourceRedisCloudActiveActivePrivateServiceConnectEndpointProStep1, baseName, gcpProjectId, gcpVPCName, gcpVPCSubnetName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "private_service_connect_endpoint_id"), + ), + }, + { + Config: fmt.Sprintf(testAccResourceRedisCloudActiveActivePrivateServiceConnectEndpointProStep2, baseName, gcpProjectId, gcpVPCName, gcpVPCSubnetName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(datasourceName, "id"), + resource.TestCheckResourceAttr(datasourceName, "endpoints.#", "1"), + resource.TestCheckResourceAttr(datasourceName, "endpoints.0.gcp_project_id", gcpProjectId), + resource.TestCheckResourceAttr(datasourceName, "endpoints.0.gcp_vpc_name", gcpVPCName), + resource.TestCheckResourceAttr(datasourceName, "endpoints.0.gcp_vpc_subnet_name", gcpVPCSubnetName), + resource.TestCheckResourceAttrWith(datasourceName, "endpoints.0.endpoint_connection_name", func(value string) error { + if !strings.HasPrefix(value, "redis-") { + return fmt.Errorf("expected %s to have prefix 'redis-'", value) + } + return nil + }), + resource.TestCheckResourceAttr(datasourceName, "endpoints.0.service_attachments.#", "40"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + }, + }, + }) +} + +const testAccResourceRedisCloudActiveActivePrivateServiceConnectEndpointProStep1 = ` +data "rediscloud_payment_method" "card" { + card_type = "Visa" +} + +resource "rediscloud_active_active_subscription" "subscription_resource" { + name = "%s" + payment_method_id = data.rediscloud_payment_method.card.id + cloud_provider = "GCP" + + creation_plan { + memory_limit_in_gb = 1 + quantity = 1 + region { + region = "us-central1" + networking_deployment_cidr = "192.168.0.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + region { + region = "europe-west1" + networking_deployment_cidr = "10.0.1.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + } +} + +resource "rediscloud_active_active_subscription_database" "database_resource" { + subscription_id = rediscloud_active_active_subscription.subscription_resource.id + name = "db" + memory_limit_in_gb = 1 + global_data_persistence = "aof-every-1-second" + global_password = "some-password" +} + +resource "rediscloud_active_active_subscription_regions" "regions_resource" { + subscription_id = rediscloud_active_active_subscription.subscription_resource.id + + region { + region = "us-central1" + networking_deployment_cidr = "192.168.0.0/24" + database { + database_id = rediscloud_active_active_subscription_database.database_resource.db_id + database_name = rediscloud_active_active_subscription_database.database_resource.name + local_write_operations_per_second = 1000 + local_read_operations_per_second = 1000 + } + } + + region { + region = "europe-west1" + networking_deployment_cidr = "10.0.1.0/24" + database { + database_id = rediscloud_active_active_subscription_database.database_resource.db_id + database_name = rediscloud_active_active_subscription_database.database_resource.name + local_write_operations_per_second = 1000 + local_read_operations_per_second = 1000 + } + } +} + +resource "rediscloud_active_active_private_service_connect" "psc" { + subscription_id = rediscloud_active_active_subscription.subscription_resource.id + region_id = one([for r in rediscloud_active_active_subscription_regions.regions_resource.region : r.region_id if r.region == "us-central1"]) +} + +resource "rediscloud_active_active_private_service_connect_endpoint" "psce" { + subscription_id = rediscloud_active_active_subscription.subscription_resource.id + region_id = one([for r in rediscloud_active_active_subscription_regions.regions_resource.region : r.region_id if r.region == "us-central1"]) + private_service_connect_service_id = rediscloud_active_active_private_service_connect.psc.private_service_connect_service_id + gcp_project_id = "%s" + gcp_vpc_name = "%s" + gcp_vpc_subnet_name = "%s" + endpoint_connection_name = "redis-${rediscloud_active_active_subscription.subscription_resource.id}" +} +` + +const testAccResourceRedisCloudActiveActivePrivateServiceConnectEndpointProStep2 = testAccResourceRedisCloudActiveActivePrivateServiceConnectEndpointProStep1 + ` + +data "rediscloud_active_active_private_service_connect_endpoints" "psce" { + subscription_id = rediscloud_active_active_subscription.subscription_resource.id + region_id = one([for r in rediscloud_active_active_subscription_regions.regions_resource.region : r.region_id if r.region == "us-central1"]) + private_service_connect_service_id = rediscloud_active_active_private_service_connect.psc.private_service_connect_service_id +} +` diff --git a/provider/rediscloud_active_active_private_service_connect_test.go b/provider/rediscloud_active_active_private_service_connect_test.go new file mode 100644 index 00000000..23a98990 --- /dev/null +++ b/provider/rediscloud_active_active_private_service_connect_test.go @@ -0,0 +1,126 @@ +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccResourceRedisCloudActiveActivePrivateServiceConnect_CRUDI(t *testing.T) { + + baseName := acctest.RandomWithPrefix(testResourcePrefix) + "-pro-psc" + + const resourceName = "rediscloud_active_active_private_service_connect.psc" + const datasourceName = "data.rediscloud_active_active_private_service_connect.psc" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckActiveActiveSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccResourceRedisCloudActiveActivePrivateServiceConnectProStep1, baseName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "region_id"), + resource.TestCheckResourceAttrSet(resourceName, "subscription_id"), + resource.TestCheckResourceAttrSet(resourceName, "private_service_connect_service_id"), + ), + }, + { + Config: fmt.Sprintf(testAccResourceRedisCloudActiveActivePrivateServiceConnectProStep2, baseName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(datasourceName, "id"), + resource.TestCheckResourceAttrSet(datasourceName, "region_id"), + resource.TestCheckResourceAttrSet(datasourceName, "subscription_id"), + resource.TestCheckResourceAttrSet(datasourceName, "private_service_connect_service_id"), + resource.TestCheckResourceAttrSet(datasourceName, "connection_host_name"), + resource.TestCheckResourceAttrSet(datasourceName, "service_attachment_name"), + resource.TestCheckResourceAttr(datasourceName, "status", "active"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + }, + }, + }) +} + +const testAccResourceRedisCloudActiveActivePrivateServiceConnectProStep1 = ` +data "rediscloud_payment_method" "card" { + card_type = "Visa" +} + +resource "rediscloud_active_active_subscription" "subscription_resource" { + name = "%s" + payment_method_id = data.rediscloud_payment_method.card.id + cloud_provider = "GCP" + + creation_plan { + memory_limit_in_gb = 1 + quantity = 1 + region { + region = "us-central1" + networking_deployment_cidr = "192.168.0.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + region { + region = "europe-west1" + networking_deployment_cidr = "10.0.1.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + } +} + +resource "rediscloud_active_active_subscription_database" "database_resource" { + subscription_id = rediscloud_active_active_subscription.subscription_resource.id + name = "db" + memory_limit_in_gb = 1 + global_data_persistence = "aof-every-1-second" + global_password = "some-password" +} + +resource "rediscloud_active_active_subscription_regions" "regions_resource" { + subscription_id = rediscloud_active_active_subscription.subscription_resource.id + + region { + region = "us-central1" + networking_deployment_cidr = "192.168.0.0/24" + database { + database_id = rediscloud_active_active_subscription_database.database_resource.db_id + database_name = rediscloud_active_active_subscription_database.database_resource.name + local_write_operations_per_second = 1000 + local_read_operations_per_second = 1000 + } + } + + region { + region = "europe-west1" + networking_deployment_cidr = "10.0.1.0/24" + database { + database_id = rediscloud_active_active_subscription_database.database_resource.db_id + database_name = rediscloud_active_active_subscription_database.database_resource.name + local_write_operations_per_second = 1000 + local_read_operations_per_second = 1000 + } + } +} + +resource "rediscloud_active_active_private_service_connect" "psc" { + subscription_id = rediscloud_active_active_subscription.subscription_resource.id + region_id = one([for r in rediscloud_active_active_subscription_regions.regions_resource.region : r.region_id if r.region == "us-central1"]) +} +` + +const testAccResourceRedisCloudActiveActivePrivateServiceConnectProStep2 = testAccResourceRedisCloudActiveActivePrivateServiceConnectProStep1 + ` + +data "rediscloud_active_active_private_service_connect" "psc" { + subscription_id = rediscloud_active_active_subscription.subscription_resource.id + region_id = one([for r in rediscloud_active_active_subscription_regions.regions_resource.region : r.region_id if r.region == "us-central1"]) +} +` diff --git a/provider/rediscloud_private_service_connect_endpoint_accepter_test.go b/provider/rediscloud_private_service_connect_endpoint_accepter_test.go new file mode 100644 index 00000000..ba2f5970 --- /dev/null +++ b/provider/rediscloud_private_service_connect_endpoint_accepter_test.go @@ -0,0 +1,157 @@ +package provider + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/rediscloud-go-api/service/psc" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccResourceRedisCloudPrivateServiceConnectEndpointAccepter_Create(t *testing.T) { + + baseName := acctest.RandomWithPrefix(testResourcePrefix) + "-pro-pscea" + + const resourceName = "rediscloud_private_service_connect_endpoint_accepter.accepter" + gcpProjectId := os.Getenv("GCP_VPC_PROJECT") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccGcpProjectPreCheck(t); testAccGcpCredentialsPreCheck(t) }, + ProviderFactories: providerFactories, + ExternalProviders: map[string]resource.ExternalProvider{ + "google": { + Source: "hashicorp/google", + VersionConstraint: "~> 6.5", + }, + }, + CheckDestroy: testAccCheckProSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccResourceRedisCloudPrivateServiceConnectEndpointAccepterPro, baseName, gcpProjectId), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "id"), + func(s *terraform.State) error { + r := s.RootModule().Resources[resourceName] + + accepterId, err := toPscEndpointAccepterId(r.Primary.ID) + if err != nil { + return fmt.Errorf("couldn't parse the accepter ID: %s", r.Primary.ID) + } + + client := testProvider.Meta().(*apiClient) + endpoints, err := client.client.PrivateServiceConnect.GetEndpoints(context.TODO(), accepterId.subscriptionId, accepterId.pscServiceId) + if err != nil { + return err + } + + endpoint := findPrivateServiceConnectEndpoints(accepterId.endpointId, endpoints.Endpoints) + if endpoint == nil { + return fmt.Errorf("couldn't find endpoint with ID: %d", accepterId.endpointId) + } + + if redis.StringValue(endpoint.Status) != psc.EndpointStatusActive { + return fmt.Errorf("expected endpoint status to be active - current status %s", redis.StringValue(endpoint.Status)) + } + + return nil + }, + ), + }, + }, + }) +} + +const testAccResourceRedisCloudPrivateServiceConnectEndpointAccepterPro = ` +data "rediscloud_payment_method" "card" { + card_type = "Visa" +} + +resource "rediscloud_subscription" "subscription" { + name = "%s" + payment_method_id = data.rediscloud_payment_method.card.id + + cloud_provider { + provider = "GCP" + region { + region = "us-central1" + networking_deployment_cidr = "10.0.0.0/24" + } + } + + creation_plan { + dataset_size_in_gb = 15 + quantity = 1 + replication = true + throughput_measurement_by = "operations-per-second" + throughput_measurement_value = 20000 + } +} + +resource "rediscloud_private_service_connect" "psc" { + subscription_id = rediscloud_subscription.subscription.id +} + +resource "rediscloud_private_service_connect_endpoint" "psce" { + subscription_id = rediscloud_subscription.subscription.id + private_service_connect_service_id = rediscloud_private_service_connect.psc.private_service_connect_service_id + gcp_project_id = "%[2]s" + gcp_vpc_name = google_compute_network.network.name + gcp_vpc_subnet_name = google_compute_subnetwork.subnet.name + endpoint_connection_name = "redis-${rediscloud_subscription.subscription.id}" +} + +resource "google_compute_network" "network" { + project = "%[2]s" + name = "%[1]s" + auto_create_subnetworks = false +} + +resource "google_compute_subnetwork" "subnet" { + project = "%[2]s" + name = "%[1]s" + ip_cidr_range = "10.0.1.0/24" + region = "us-central1" + network = google_compute_network.network.id +} + +locals { + service_attachment_count = 40 +} + +resource "google_compute_address" "default" { + count = local.service_attachment_count + + project = "%[2]s" + name = rediscloud_private_service_connect_endpoint.psce.service_attachments[count.index].ip_address_name + subnetwork = google_compute_subnetwork.subnet.id + address_type = "INTERNAL" + region = "us-central1" +} + +resource "google_compute_forwarding_rule" "default" { + count = local.service_attachment_count + + name = rediscloud_private_service_connect_endpoint.psce.service_attachments[count.index].forwarding_rule_name + project = "%[2]s" + region = "us-central1" + ip_address = google_compute_address.default[count.index].id + network = google_compute_network.network.name + target = rediscloud_private_service_connect_endpoint.psce.service_attachments[count.index].name + load_balancing_scheme = "" +} + +resource "rediscloud_private_service_connect_endpoint_accepter" "accepter" { + subscription_id = rediscloud_subscription.subscription.id + private_service_connect_service_id = rediscloud_private_service_connect.psc.private_service_connect_service_id + private_service_connect_endpoint_id = rediscloud_private_service_connect_endpoint.psce.private_service_connect_endpoint_id + + action = "accept" + + depends_on = [google_compute_forwarding_rule.default] +} +` diff --git a/provider/rediscloud_private_service_connect_endpoint_test.go b/provider/rediscloud_private_service_connect_endpoint_test.go new file mode 100644 index 00000000..13a4ad50 --- /dev/null +++ b/provider/rediscloud_private_service_connect_endpoint_test.go @@ -0,0 +1,106 @@ +package provider + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccResourceRedisCloudPrivateServiceConnectEndpoint_CRUDI(t *testing.T) { + + baseName := acctest.RandomWithPrefix(testResourcePrefix) + "-pro-psce" + + const resourceName = "rediscloud_private_service_connect_endpoint.psce" + const datasourceName = "data.rediscloud_private_service_connect_endpoints.psce" + gcpProjectId := os.Getenv("GCP_VPC_PROJECT") + gcpVPCName := fmt.Sprintf("%s-network", baseName) + gcpVPCSubnetName := fmt.Sprintf("%s-subnet", baseName) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccGcpProjectPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckProSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccResourceRedisCloudPrivateServiceConnectEndpointProStep1, baseName, gcpProjectId, gcpVPCName, gcpVPCSubnetName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "private_service_connect_endpoint_id"), + ), + }, + { + Config: fmt.Sprintf(testAccResourceRedisCloudPrivateServiceConnectEndpointProStep2, baseName, gcpProjectId, gcpVPCName, gcpVPCSubnetName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(datasourceName, "id"), + resource.TestCheckResourceAttr(datasourceName, "endpoints.#", "1"), + resource.TestCheckResourceAttr(datasourceName, "endpoints.0.gcp_project_id", gcpProjectId), + resource.TestCheckResourceAttr(datasourceName, "endpoints.0.gcp_vpc_name", gcpVPCName), + resource.TestCheckResourceAttr(datasourceName, "endpoints.0.gcp_vpc_subnet_name", gcpVPCSubnetName), + resource.TestCheckResourceAttrWith(datasourceName, "endpoints.0.endpoint_connection_name", func(value string) error { + if !strings.HasPrefix(value, "redis-") { + return fmt.Errorf("expected %s to have prefix 'redis-'", value) + } + return nil + }), + resource.TestCheckResourceAttr(datasourceName, "endpoints.0.service_attachments.#", "40"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + }, + }, + }) +} + +const testAccResourceRedisCloudPrivateServiceConnectEndpointProStep1 = ` +data "rediscloud_payment_method" "card" { + card_type = "Visa" +} + +resource "rediscloud_subscription" "subscription_resource" { + name = "%s" + payment_method_id = data.rediscloud_payment_method.card.id + + cloud_provider { + provider = "GCP" + region { + region = "us-central1" + networking_deployment_cidr = "10.0.0.0/24" + } + } + + creation_plan { + dataset_size_in_gb = 15 + quantity = 1 + replication = true + throughput_measurement_by = "operations-per-second" + throughput_measurement_value = 20000 + } +} + +resource "rediscloud_private_service_connect" "psc" { + subscription_id = rediscloud_subscription.subscription_resource.id +} + +resource "rediscloud_private_service_connect_endpoint" "psce" { + subscription_id = rediscloud_subscription.subscription_resource.id + private_service_connect_service_id = rediscloud_private_service_connect.psc.private_service_connect_service_id + gcp_project_id = "%s" + gcp_vpc_name = "%s" + gcp_vpc_subnet_name = "%s" + endpoint_connection_name = "redis-${rediscloud_subscription.subscription_resource.id}" +} +` + +const testAccResourceRedisCloudPrivateServiceConnectEndpointProStep2 = testAccResourceRedisCloudPrivateServiceConnectEndpointProStep1 + ` + +data "rediscloud_private_service_connect_endpoints" "psce" { + subscription_id = rediscloud_subscription.subscription_resource.id + private_service_connect_service_id = rediscloud_private_service_connect.psc.private_service_connect_service_id +} +` diff --git a/provider/rediscloud_private_service_connect_test.go b/provider/rediscloud_private_service_connect_test.go new file mode 100644 index 00000000..178fd40b --- /dev/null +++ b/provider/rediscloud_private_service_connect_test.go @@ -0,0 +1,86 @@ +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccResourceRedisCloudPrivateServiceConnect_CRUDI(t *testing.T) { + + baseName := acctest.RandomWithPrefix(testResourcePrefix) + "-pro-psc" + + const resourceName = "rediscloud_private_service_connect.psc" + const datasourceName = "data.rediscloud_private_service_connect.psc" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckProSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccResourceRedisCloudPrivateServiceConnectProStep1, baseName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "subscription_id"), + resource.TestCheckResourceAttrSet(resourceName, "private_service_connect_service_id"), + ), + }, + { + Config: fmt.Sprintf(testAccResourceRedisCloudPrivateServiceConnectProStep2, baseName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(datasourceName, "id"), + resource.TestCheckResourceAttrSet(datasourceName, "subscription_id"), + resource.TestCheckResourceAttrSet(datasourceName, "private_service_connect_service_id"), + resource.TestCheckResourceAttrSet(datasourceName, "connection_host_name"), + resource.TestCheckResourceAttrSet(datasourceName, "service_attachment_name"), + resource.TestCheckResourceAttr(datasourceName, "status", "active"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + }, + }, + }) +} + +const testAccResourceRedisCloudPrivateServiceConnectProStep1 = ` +data "rediscloud_payment_method" "card" { + card_type = "Visa" +} + +resource "rediscloud_subscription" "subscription_resource" { + name = "%s" + payment_method_id = data.rediscloud_payment_method.card.id + + cloud_provider { + provider = "GCP" + region { + region = "us-central1" + networking_deployment_cidr = "10.0.0.0/24" + } + } + + creation_plan { + dataset_size_in_gb = 15 + quantity = 1 + replication = true + throughput_measurement_by = "operations-per-second" + throughput_measurement_value = 20000 + } +} + +resource "rediscloud_private_service_connect" "psc" { + subscription_id = rediscloud_subscription.subscription_resource.id +} +` + +const testAccResourceRedisCloudPrivateServiceConnectProStep2 = testAccResourceRedisCloudPrivateServiceConnectProStep1 + ` + +data "rediscloud_private_service_connect" "psc" { + subscription_id = rediscloud_subscription.subscription_resource.id +} +` diff --git a/provider/resource_rediscloud_active_active_private_service_connect.go b/provider/resource_rediscloud_active_active_private_service_connect.go new file mode 100644 index 00000000..1f647836 --- /dev/null +++ b/provider/resource_rediscloud_active_active_private_service_connect.go @@ -0,0 +1,213 @@ +package provider + +import ( + "context" + "errors" + "fmt" + "log" + "strconv" + "strings" + "time" + + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/rediscloud-go-api/service/psc" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceRedisCloudActiveActivePrivateServiceConnect() *schema.Resource { + return &schema.Resource{ + Description: "Manages a Private Service Connect to an Active-Active Subscription in your Redis Enterprise Cloud Account.", + CreateContext: resourceRedisCloudActiveActivePrivateServiceConnectCreate, + ReadContext: resourceRedisCloudActiveActivePrivateServiceConnectRead, + DeleteContext: resourceRedisCloudActiveActivePrivateServiceConnectDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Read: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "subscription_id": { + Description: "The ID of the Pro subscription to attach", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "region_id": { + Description: "The ID of the GCP region", + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "private_service_connect_service_id": { + Description: "The ID of the Private Service Connect", + Type: schema.TypeInt, + Computed: true, + }, + }, + } +} + +func resourceRedisCloudActiveActivePrivateServiceConnectCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + api := meta.(*apiClient) + + subscriptionId, err := strconv.Atoi(d.Get("subscription_id").(string)) + if err != nil { + return diag.FromErr(err) + } + subscriptionMutex.Lock(subscriptionId) + + regionId := d.Get("region_id").(int) + + pscServiceId, err := api.client.PrivateServiceConnect.CreateActiveActiveService(ctx, subscriptionId, regionId) + if err != nil { + subscriptionMutex.Unlock(subscriptionId) + return diag.FromErr(err) + } + + d.SetId(buildPrivateServiceConnectActiveActiveId(subscriptionId, regionId, pscServiceId)) + + err = waitForPrivateServiceConnectServiceToBeActive(ctx, func() (result interface{}, state string, err error) { + return refreshPrivateServiceConnectServiceActiveActiveStatus(ctx, subscriptionId, regionId, api) + }) + if err != nil { + subscriptionMutex.Unlock(subscriptionId) + return diag.FromErr(err) + } + + err = waitForSubscriptionToBeActive(ctx, subscriptionId, api) + if err != nil { + subscriptionMutex.Unlock(subscriptionId) + return diag.FromErr(err) + } + + subscriptionMutex.Unlock(subscriptionId) + return resourceRedisCloudActiveActivePrivateServiceConnectRead(ctx, d, meta) +} + +func resourceRedisCloudActiveActivePrivateServiceConnectRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + api := meta.(*apiClient) + + resId, err := toPscServiceActiveActiveId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + pscObj, err := api.client.PrivateServiceConnect.GetActiveActiveService(ctx, resId.subscriptionId, resId.regionId) + if err != nil { + var notFound *psc.NotFoundActiveActive + if errors.As(err, ¬Found) { + d.SetId("") + return diags + } + return diag.FromErr(err) + } + + d.SetId(buildPrivateServiceConnectActiveActiveId(resId.subscriptionId, resId.regionId, resId.pscServiceId)) + + err = d.Set("subscription_id", strconv.Itoa(resId.subscriptionId)) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("region_id", resId.regionId) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("private_service_connect_service_id", redis.IntValue(pscObj.ID)) + if err != nil { + return diag.FromErr(err) + } + + return diags +} + +func resourceRedisCloudActiveActivePrivateServiceConnectDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + api := meta.(*apiClient) + + subscriptionId, err := strconv.Atoi(d.Get("subscription_id").(string)) + if err != nil { + return diag.FromErr(err) + } + subscriptionMutex.Lock(subscriptionId) + defer subscriptionMutex.Unlock(subscriptionId) + + regionId := d.Get("region_id").(int) + + err = api.client.PrivateServiceConnect.DeleteActiveActiveService(ctx, subscriptionId, regionId) + if err != nil { + var notFound *psc.NotFoundActiveActive + if errors.As(err, ¬Found) { + d.SetId("") + return diags + } + return diag.FromErr(err) + } + + d.SetId("") + + err = waitForSubscriptionToBeActive(ctx, subscriptionId, api) + if err != nil { + return diag.FromErr(err) + } + + return diags +} + +func buildPrivateServiceConnectActiveActiveId(subId int, regionId int, pscServiceId int) string { + return fmt.Sprintf("%d/%d/%d", subId, regionId, pscServiceId) +} + +type privateServiceConnectServiceActiveActiveId struct { + subscriptionId int + regionId int + pscServiceId int +} + +func toPscServiceActiveActiveId(id string) (*privateServiceConnectServiceActiveActiveId, error) { + parts := strings.Split(id, "/") + if len(parts) != 3 { + return nil, fmt.Errorf("invalid id: %s", id) + } + + subId, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, err + } + + regionId, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, err + } + + pscServiceId, err := strconv.Atoi(parts[2]) + if err != nil { + return nil, err + } + + return &privateServiceConnectServiceActiveActiveId{ + subscriptionId: subId, + regionId: regionId, + pscServiceId: pscServiceId, + }, nil +} + +func refreshPrivateServiceConnectServiceActiveActiveStatus(ctx context.Context, subscriptionId int, regionId int, api *apiClient) (result interface{}, state string, err error) { + log.Printf("[DEBUG] Waiting for private service connect service status %d/%d to be active", subscriptionId, regionId) + + pscService, err := api.client.PrivateServiceConnect.GetActiveActiveService(ctx, subscriptionId, regionId) + if err != nil { + return nil, "", err + } + + return redis.StringValue(pscService.Status), redis.StringValue(pscService.Status), nil +} diff --git a/provider/resource_rediscloud_active_active_private_service_connect_endpoint.go b/provider/resource_rediscloud_active_active_private_service_connect_endpoint.go new file mode 100644 index 00000000..decd4f5f --- /dev/null +++ b/provider/resource_rediscloud_active_active_private_service_connect_endpoint.go @@ -0,0 +1,366 @@ +package provider + +import ( + "context" + "errors" + "fmt" + "log" + "strconv" + "strings" + "time" + + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/rediscloud-go-api/service/psc" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const placeholderStatusDisappear = "disappeared" + +func resourceRedisCloudActiveActivePrivateServiceConnectEndpoint() *schema.Resource { + return &schema.Resource{ + Description: "Manages a Private Service Connect Endpoint to an Active-Active Subscription in your Redis Enterprise Cloud Account.", + CreateContext: resourceRedisCloudActiveActivePrivateServiceConnectEndpointCreate, + ReadContext: resourceRedisCloudActiveActivePrivateServiceConnectEndpointRead, + DeleteContext: resourceRedisCloudActiveActivePrivateServiceConnectEndpointDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Read: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "subscription_id": { + Description: "The ID of the Pro subscription to attach", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "region_id": { + Description: "The ID of the GCP region", + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "private_service_connect_service_id": { + Description: "The ID of the Private Service Connect", + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "private_service_connect_endpoint_id": { + Description: "The ID of the Private Service Connect Endpoint", + Type: schema.TypeInt, + Computed: true, + }, + "gcp_project_id": { + Description: "The Google Cloud Project ID", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "gcp_vpc_name": { + Description: "The GCP VPC Network name", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "gcp_vpc_subnet_name": { + Description: "The GCP Subnet name", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "endpoint_connection_name": { + Description: "The endpoint connection name prefix. This prefix that will be used to create the Private Service Connect endpoint in your Google Cloud account", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "service_attachments": { + Description: "The service attachments that were created for the Private Service Connect endpoint", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Description: "Name of the service attachment", + Type: schema.TypeString, + Computed: true, + }, + "dns_record": { + Description: "DNS record for the service attachment", + Type: schema.TypeString, + Computed: true, + }, + "ip_address_name": { + Description: "IP address name for the service attachment", + Type: schema.TypeString, + Computed: true, + }, + "forwarding_rule_name": { + Description: "Name of the forwarding rule for the service attachment", + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func resourceRedisCloudActiveActivePrivateServiceConnectEndpointCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + api := meta.(*apiClient) + + subscriptionId, err := strconv.Atoi(d.Get("subscription_id").(string)) + if err != nil { + return diag.FromErr(err) + } + subscriptionMutex.Lock(subscriptionId) + defer subscriptionMutex.Unlock(subscriptionId) + + regionId := d.Get("region_id").(int) + pscServiceId := d.Get("private_service_connect_service_id").(int) + gcpProjectId := d.Get("gcp_project_id").(string) + gcpVpcName := d.Get("gcp_vpc_name").(string) + gcpVpcSubnetName := d.Get("gcp_vpc_subnet_name").(string) + endpointConnectionNamePrefix := d.Get("endpoint_connection_name").(string) + + endpointId, err := api.client.PrivateServiceConnect.CreateActiveActiveEndpoint(ctx, subscriptionId, regionId, pscServiceId, psc.CreatePrivateServiceConnectEndpoint{ + GCPProjectID: &gcpProjectId, + GCPVPCName: &gcpVpcName, + GCPVPCSubnetName: &gcpVpcSubnetName, + EndpointConnectionName: &endpointConnectionNamePrefix, + }) + + if err != nil { + return diag.FromErr(err) + } + + d.SetId(buildPrivateServiceConnectActiveActiveEndpointId(subscriptionId, regionId, pscServiceId, endpointId)) + + err = waitForSubscriptionToBeActive(ctx, subscriptionId, api) + if err != nil { + return diag.FromErr(err) + } + + return resourceRedisCloudActiveActivePrivateServiceConnectEndpointRead(ctx, d, meta) +} + +func resourceRedisCloudActiveActivePrivateServiceConnectEndpointRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + api := meta.(*apiClient) + + resId, err := toPscEndpointActiveActiveId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + endpoints, err := api.client.PrivateServiceConnect.GetActiveActiveEndpoints(ctx, resId.subscriptionId, resId.regionId, resId.pscServiceId) + if err != nil { + var notFound *psc.NotFoundActiveActive + if errors.As(err, ¬Found) { + d.SetId("") + return diags + } + return diag.FromErr(err) + } + + endpoint := findPrivateServiceConnectEndpoints(resId.endpointId, endpoints.Endpoints) + if endpoint == nil { + d.SetId("") + return diags + } + + d.SetId(buildPrivateServiceConnectActiveActiveEndpointId(resId.subscriptionId, resId.regionId, resId.pscServiceId, redis.IntValue(endpoint.ID))) + + if redis.StringValue(endpoint.Status) != psc.EndpointStatusRejected && redis.StringValue(endpoint.Status) != psc.EndpointStatusDeleted { + creationScript, err := api.client.PrivateServiceConnect.GetActiveActiveEndpointCreationScripts(ctx, + resId.subscriptionId, resId.regionId, resId.pscServiceId, redis.IntValue(endpoint.ID), true) + if err != nil { + var notFound *psc.NotFoundActiveActive + if errors.As(err, ¬Found) { + d.SetId("") + return diags + } + return diag.FromErr(err) + } + + if err := d.Set("service_attachments", flattenPrivateServiceConnectEndpointServiceAttachments(creationScript.Script.TerraformGcp.ServiceAttachments)); err != nil { + return diag.FromErr(err) + } + } else { + if err := d.Set("service_attachments", []any{}); err != nil { + return diag.FromErr(err) + } + } + + err = d.Set("subscription_id", strconv.Itoa(resId.subscriptionId)) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("region_id", resId.regionId) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("private_service_connect_service_id", resId.pscServiceId) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("private_service_connect_endpoint_id", endpoint.ID) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("gcp_project_id", endpoint.GCPProjectID) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("gcp_vpc_name", endpoint.GCPVPCName) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("gcp_vpc_subnet_name", endpoint.GCPVPCSubnetName) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("endpoint_connection_name", endpoint.EndpointConnectionName) + if err != nil { + return diag.FromErr(err) + } + + return diags +} + +func resourceRedisCloudActiveActivePrivateServiceConnectEndpointDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + api := meta.(*apiClient) + + resId, err := toPscEndpointActiveActiveId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + subscriptionMutex.Lock(resId.subscriptionId) + defer subscriptionMutex.Unlock(resId.subscriptionId) + + endpoints, err := api.client.PrivateServiceConnect.GetActiveActiveEndpoints(ctx, resId.subscriptionId, resId.regionId, resId.pscServiceId) + if err != nil { + var notFound *psc.NotFoundActiveActive + if errors.As(err, ¬Found) { + d.SetId("") + return diags + } + return diag.FromErr(err) + } + + endpoint := findPrivateServiceConnectEndpoints(resId.endpointId, endpoints.Endpoints) + if endpoint == nil { + d.SetId("") + return diags + } + + if redis.StringValue(endpoint.Status) == psc.EndpointStatusInitialized { + // It's only possible to delete an endpoint in initialized status + err = api.client.PrivateServiceConnect.DeleteActiveActiveEndpoint(ctx, resId.subscriptionId, resId.regionId, resId.pscServiceId, resId.endpointId) + if err != nil { + return diag.FromErr(err) + } + return diags + } + + // Endpoints will be automatically removed once related GCP resources are removed. So we will wait for this + // to happen, but we can't check the GCP resources from this provider + err = waitForPrivateServiceConnectServiceEndpointDisappear(ctx, func() (result interface{}, state string, err error) { + return refreshPrivateServiceConnectServiceActiveActiveEndpointDisappear(ctx, resId.subscriptionId, resId.regionId, resId.pscServiceId, resId.endpointId, api) + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + + return diags +} + +func buildPrivateServiceConnectActiveActiveEndpointId(subId int, regionId int, pscId int, endpointId int) string { + return privateServiceConnectActiveActiveEndpointId{ + subscriptionId: subId, + regionId: regionId, + pscServiceId: pscId, + endpointId: endpointId}.String() +} + +type privateServiceConnectActiveActiveEndpointId struct { + subscriptionId int + regionId int + pscServiceId int + endpointId int +} + +func (p privateServiceConnectActiveActiveEndpointId) String() string { + return fmt.Sprintf("%d/%d/%d/%d", p.subscriptionId, p.regionId, p.pscServiceId, p.endpointId) +} + +func toPscEndpointActiveActiveId(id string) (*privateServiceConnectActiveActiveEndpointId, error) { + parts := strings.Split(id, "/") + if len(parts) != 4 { + return nil, fmt.Errorf("invalid id: %s", id) + } + + subId, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, err + } + + regionId, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, err + } + + pscId, err := strconv.Atoi(parts[2]) + if err != nil { + return nil, err + } + + endpointId, err := strconv.Atoi(parts[3]) + if err != nil { + return nil, err + } + + return &privateServiceConnectActiveActiveEndpointId{ + subscriptionId: subId, + regionId: regionId, + pscServiceId: pscId, + endpointId: endpointId, + }, nil +} + +func refreshPrivateServiceConnectServiceActiveActiveEndpointDisappear(ctx context.Context, subscriptionId int, + regionId int, pscServiceId int, endpointId int, api *apiClient) (result interface{}, state string, err error) { + log.Printf("[DEBUG] Waiting for private service connect service endpoint %d/%d/%d to be deleted", + subscriptionId, pscServiceId, endpointId) + + endpoints, err := api.client.PrivateServiceConnect.GetActiveActiveEndpoints(ctx, subscriptionId, regionId, pscServiceId) + if err != nil { + return nil, "", err + } + + endpoint := findPrivateServiceConnectEndpoints(endpointId, endpoints.Endpoints) + if endpoint == nil { + return placeholderStatusDisappear, placeholderStatusDisappear, nil + } + + return redis.StringValue(endpoint.Status), redis.StringValue(endpoint.Status), nil +} diff --git a/provider/resource_rediscloud_active_active_private_service_connect_endpoint_accepter.go b/provider/resource_rediscloud_active_active_private_service_connect_endpoint_accepter.go new file mode 100644 index 00000000..b02a9c08 --- /dev/null +++ b/provider/resource_rediscloud_active_active_private_service_connect_endpoint_accepter.go @@ -0,0 +1,278 @@ +package provider + +import ( + "context" + "errors" + "fmt" + "log" + "strconv" + "strings" + "time" + + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/rediscloud-go-api/service/psc" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepter() *schema.Resource { + return &schema.Resource{ + Description: "Manages the state of Private Service Connect Endpoint to an Active-Active Subscription in your Redis Enterprise Cloud Account.", + CreateContext: resourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepterCreate, + ReadContext: resourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepterRead, + UpdateContext: resourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepterUpdate, + DeleteContext: resourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepterDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Read: schema.DefaultTimeout(10 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "subscription_id": { + Description: "The ID of the Pro subscription to attach", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "region_id": { + Description: "The ID of the GCP region", + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "private_service_connect_service_id": { + Description: "The ID of the Private Service Connect", + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "private_service_connect_endpoint_id": { + Description: "The ID of the Private Service Connect Endpoint", + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "action": { + Description: "Accept or reject the endpoint", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{psc.EndpointActionAccept, psc.EndpointActionReject}, false)), + }, + }, + } +} + +func resourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepterCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + api := meta.(*apiClient) + + subscriptionId, err := strconv.Atoi(d.Get("subscription_id").(string)) + if err != nil { + return diag.FromErr(err) + } + subscriptionMutex.Lock(subscriptionId) + defer subscriptionMutex.Unlock(subscriptionId) + + regionId := d.Get("region_id").(int) + pscServiceId := d.Get("private_service_connect_service_id").(int) + endpointId := d.Get("private_service_connect_endpoint_id").(int) + action := d.Get("action").(string) + + endpoints, err := api.client.PrivateServiceConnect.GetActiveActiveEndpoints(ctx, subscriptionId, regionId, pscServiceId) + if err != nil { + var notFound *psc.NotFoundActiveActive + if errors.As(err, ¬Found) { + d.SetId("") + return diags + } + return diag.FromErr(err) + } + + endpoint := findPrivateServiceConnectEndpoints(endpointId, endpoints.Endpoints) + if endpoint == nil { + return diag.FromErr(fmt.Errorf("endpoint with id %d not found", endpointId)) + } + + if endpoint.Status == nil { + return diag.FromErr(fmt.Errorf("endpoint with id %d has no status", endpointId)) + } + + if redis.StringValue(endpoint.Status) == psc.EndpointStatusActive && action == psc.EndpointActionAccept { + d.SetId(buildPrivateServiceConnectActiveActiveEndpointAccepterId(subscriptionId, regionId, pscServiceId, endpointId)) + return diag.Diagnostics{} + } + + if redis.StringValue(endpoint.Status) == psc.EndpointStatusRejected && action == psc.EndpointActionReject { + d.SetId(buildPrivateServiceConnectActiveActiveEndpointAccepterId(subscriptionId, regionId, pscServiceId, endpointId)) + return diag.Diagnostics{} + } + + refreshFunc := func(targetStatus string) (result interface{}, state string, err error) { + return refreshPrivateServiceConnectServiceEndpointActiveActiveStatus(ctx, subscriptionId, regionId, pscServiceId, endpointId, targetStatus, api) + } + + if redis.StringValue(endpoint.Status) == psc.EndpointStatusInitialized || redis.StringValue(endpoint.Status) == psc.EndpointStatusProcessing { + err = waitForPrivateServiceConnectServiceEndpointToBePending(ctx, refreshFunc) + if err != nil { + return diag.FromErr(err) + } + } + + d.SetId(buildPrivateServiceConnectActiveActiveEndpointAccepterId(subscriptionId, regionId, pscServiceId, endpointId)) + + err = api.client.PrivateServiceConnect.UpdateActiveActiveEndpoint(ctx, subscriptionId, regionId, pscServiceId, endpointId, &psc.UpdatePrivateServiceConnectEndpoint{ + Action: redis.String(action), + }) + if err != nil { + return diag.FromErr(err) + } + + if action == psc.EndpointActionAccept { + err = waitForPrivateServiceConnectServiceEndpointToBeActive(ctx, refreshFunc) + if err != nil { + return diag.FromErr(err) + } + } else { + err = waitForPrivateServiceConnectServiceEndpointToBeRejected(ctx, refreshFunc) + if err != nil { + return diag.FromErr(err) + } + } + + return resourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepterRead(ctx, d, meta) +} + +func resourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepterRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + api := meta.(*apiClient) + + resId, err := toPscEndpointActiveActiveAccepterId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + endpoints, err := api.client.PrivateServiceConnect.GetActiveActiveEndpoints(ctx, resId.subscriptionId, resId.regionId, resId.pscServiceId) + if err != nil { + var notFound *psc.NotFoundActiveActive + if errors.As(err, ¬Found) { + d.SetId("") + return diags + } + return diag.FromErr(err) + } + + endpoint := findPrivateServiceConnectEndpoints(resId.endpointId, endpoints.Endpoints) + if endpoint == nil { + d.SetId("") + return diags + } + + d.SetId(buildPrivateServiceConnectActiveActiveEndpointAccepterId(resId.subscriptionId, resId.regionId, resId.pscServiceId, redis.IntValue(endpoint.ID))) + + err = d.Set("subscription_id", strconv.Itoa(resId.subscriptionId)) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("region_id", resId.regionId) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("private_service_connect_service_id", resId.pscServiceId) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("private_service_connect_endpoint_id", redis.IntValue(endpoint.ID)) + if err != nil { + return diag.FromErr(err) + } + + return diags +} + +func buildPrivateServiceConnectActiveActiveEndpointAccepterId(subId int, regionId int, pscId int, endpointId int) string { + return privateServiceConnectActiveActiveEndpointId{ + subscriptionId: subId, + regionId: regionId, + pscServiceId: pscId, + endpointId: endpointId}.String() +} + +type privateServiceConnectActiveActiveEndpointAccepterId struct { + subscriptionId int + regionId int + pscServiceId int + endpointId int +} + +func (p privateServiceConnectActiveActiveEndpointAccepterId) String() string { + return fmt.Sprintf("%d/%d/%d/%d", p.subscriptionId, p.regionId, p.pscServiceId, p.endpointId) +} + +func toPscEndpointActiveActiveAccepterId(id string) (*privateServiceConnectActiveActiveEndpointAccepterId, error) { + parts := strings.Split(id, "/") + if len(parts) != 4 { + return nil, fmt.Errorf("invalid id: %s", id) + } + + subId, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, err + } + + regionId, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, err + } + + pscId, err := strconv.Atoi(parts[2]) + if err != nil { + return nil, err + } + + endpointId, err := strconv.Atoi(parts[3]) + if err != nil { + return nil, err + } + + return &privateServiceConnectActiveActiveEndpointAccepterId{ + subscriptionId: subId, + regionId: regionId, + pscServiceId: pscId, + endpointId: endpointId, + }, nil +} + +func resourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepterUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return resourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepterCreate(ctx, d, meta) +} + +func resourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepterDelete(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + var diags diag.Diagnostics + d.SetId("") + return diags +} + +func refreshPrivateServiceConnectServiceEndpointActiveActiveStatus(ctx context.Context, subscriptionId int, regionId int, + pscServiceId int, endpointId int, targetStatus string, api *apiClient) (result interface{}, state string, err error) { + log.Printf("[DEBUG] Waiting for private service connect service endpoint status %d/%d/%d/%d to be %s", + subscriptionId, regionId, pscServiceId, endpointId, targetStatus) + + endpoints, err := api.client.PrivateServiceConnect.GetActiveActiveEndpoints(ctx, subscriptionId, regionId, pscServiceId) + if err != nil { + return nil, "", err + } + + endpoint := findPrivateServiceConnectEndpoints(endpointId, endpoints.Endpoints) + if endpoint == nil { + return nil, "", fmt.Errorf("endpoint with id %d not found", endpointId) + } + + return redis.StringValue(endpoint.Status), redis.StringValue(endpoint.Status), nil +} diff --git a/provider/resource_rediscloud_private_service_connect.go b/provider/resource_rediscloud_private_service_connect.go new file mode 100644 index 00000000..13686e6f --- /dev/null +++ b/provider/resource_rediscloud_private_service_connect.go @@ -0,0 +1,212 @@ +package provider + +import ( + "context" + "errors" + "fmt" + "log" + "strconv" + "strings" + "time" + + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/rediscloud-go-api/service/psc" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceRedisCloudPrivateServiceConnect() *schema.Resource { + return &schema.Resource{ + Description: "Manages a Private Service Connect to an Active-Active Subscription in your Redis Enterprise Cloud Account.", + CreateContext: resourceRedisCloudPrivateServiceConnectCreate, + ReadContext: resourceRedisCloudPrivateServiceConnectRead, + DeleteContext: resourceRedisCloudPrivateServiceConnectDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Read: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "subscription_id": { + Description: "The ID of the Pro subscription to attach", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "private_service_connect_service_id": { + Description: "The ID of the Private Service Connect", + Type: schema.TypeInt, + Computed: true, + }, + }, + } +} + +func resourceRedisCloudPrivateServiceConnectCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + api := meta.(*apiClient) + + subscriptionId, err := strconv.Atoi(d.Get("subscription_id").(string)) + if err != nil { + return diag.FromErr(err) + } + subscriptionMutex.Lock(subscriptionId) + + pscServiceId, err := api.client.PrivateServiceConnect.CreateService(ctx, subscriptionId) + if err != nil { + subscriptionMutex.Unlock(subscriptionId) + return diag.FromErr(err) + } + + d.SetId(buildPrivateServiceConnectId(subscriptionId, pscServiceId)) + + err = waitForPrivateServiceConnectServiceToBeActive(ctx, func() (result interface{}, state string, err error) { + return refreshPrivateServiceConnectServiceStatus(ctx, subscriptionId, api) + }) + if err != nil { + subscriptionMutex.Unlock(subscriptionId) + return diag.FromErr(err) + } + + err = waitForSubscriptionToBeActive(ctx, subscriptionId, api) + if err != nil { + subscriptionMutex.Unlock(subscriptionId) + return diag.FromErr(err) + } + + subscriptionMutex.Unlock(subscriptionId) + return resourceRedisCloudPrivateServiceConnectRead(ctx, d, meta) +} + +func resourceRedisCloudPrivateServiceConnectRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + api := meta.(*apiClient) + + resId, err := toPscServiceId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + pscObj, err := api.client.PrivateServiceConnect.GetService(ctx, resId.subscriptionId) + if err != nil { + var notFound *psc.NotFound + if errors.As(err, ¬Found) { + d.SetId("") + return diags + } + return diag.FromErr(err) + } + + d.SetId(buildPrivateServiceConnectId(resId.subscriptionId, resId.pscServiceId)) + + err = d.Set("subscription_id", strconv.Itoa(resId.subscriptionId)) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("private_service_connect_service_id", redis.IntValue(pscObj.ID)) + if err != nil { + return diag.FromErr(err) + } + + return diags +} + +func resourceRedisCloudPrivateServiceConnectDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + api := meta.(*apiClient) + + subscriptionId, err := strconv.Atoi(d.Get("subscription_id").(string)) + if err != nil { + return diag.FromErr(err) + } + subscriptionMutex.Lock(subscriptionId) + defer subscriptionMutex.Unlock(subscriptionId) + + err = api.client.PrivateServiceConnect.DeleteService(ctx, subscriptionId) + if err != nil { + var notFound *psc.NotFound + if errors.As(err, ¬Found) { + d.SetId("") + return diags + } + return diag.FromErr(err) + } + + d.SetId("") + + err = waitForSubscriptionToBeActive(ctx, subscriptionId, api) + if err != nil { + return diag.FromErr(err) + } + + return diags +} + +func buildPrivateServiceConnectId(subId int, pscServiceId int) string { + return fmt.Sprintf("%d/%d", subId, pscServiceId) +} + +type privateServiceConnectServiceId struct { + subscriptionId int + pscServiceId int +} + +func toPscServiceId(id string) (*privateServiceConnectServiceId, error) { + parts := strings.Split(id, "/") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid id: %s", id) + } + + subId, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, err + } + + pscServiceId, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, err + } + + return &privateServiceConnectServiceId{ + subscriptionId: subId, + pscServiceId: pscServiceId, + }, nil +} + +func refreshPrivateServiceConnectServiceStatus(ctx context.Context, subscriptionId int, api *apiClient) (result interface{}, state string, err error) { + log.Printf("[DEBUG] Waiting for private service connect service status %d to be active", subscriptionId) + + pscService, err := api.client.PrivateServiceConnect.GetService(ctx, subscriptionId) + if err != nil { + return nil, "", err + } + + return redis.StringValue(pscService.Status), redis.StringValue(pscService.Status), nil +} + +func waitForPrivateServiceConnectServiceToBeActive(ctx context.Context, refreshFunc func() (result interface{}, state string, err error)) error { + wait := &retry.StateChangeConf{ + Pending: []string{ + psc.ServiceStatusCreateQueued, + psc.ServiceStatusInitialized, + psc.ServiceStatusCreatePending}, + Target: []string{psc.ServiceStatusActive}, + Timeout: safetyTimeout, + Delay: 10 * time.Second, + PollInterval: 30 * time.Second, + + Refresh: refreshFunc, + } + if _, err := wait.WaitForStateContext(ctx); err != nil { + return err + } + + return nil +} diff --git a/provider/resource_rediscloud_private_service_connect_endpoint.go b/provider/resource_rediscloud_private_service_connect_endpoint.go new file mode 100644 index 00000000..2cdc5b28 --- /dev/null +++ b/provider/resource_rediscloud_private_service_connect_endpoint.go @@ -0,0 +1,398 @@ +package provider + +import ( + "context" + "errors" + "fmt" + "log" + "strconv" + "strings" + "time" + + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/rediscloud-go-api/service/psc" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceRedisCloudPrivateServiceConnectEndpoint() *schema.Resource { + return &schema.Resource{ + Description: "Manages a Private Service Connect Endpoint to a Pro Subscription in your Redis Enterprise Cloud Account.", + CreateContext: resourceRedisCloudPrivateServiceConnectEndpointCreate, + ReadContext: resourceRedisCloudPrivateServiceConnectEndpointRead, + DeleteContext: resourceRedisCloudPrivateServiceConnectEndpointDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Read: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "subscription_id": { + Description: "The ID of the Pro subscription to attach", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "private_service_connect_service_id": { + Description: "The ID of the Private Service Connect", + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "private_service_connect_endpoint_id": { + Description: "The ID of the Private Service Connect Endpoint", + Type: schema.TypeInt, + Computed: true, + }, + "gcp_project_id": { + Description: "The Google Cloud Project ID", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "gcp_vpc_name": { + Description: "The GCP VPC Network name", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "gcp_vpc_subnet_name": { + Description: "The GCP Subnet name", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "endpoint_connection_name": { + Description: "The endpoint connection name prefix. This prefix that will be used to create the Private Service Connect endpoint in your Google Cloud account", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "service_attachments": { + Description: "The service attachments that were created for the Private Service Connect endpoint", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Description: "Name of the service attachment", + Type: schema.TypeString, + Computed: true, + }, + "dns_record": { + Description: "DNS record for the service attachment", + Type: schema.TypeString, + Computed: true, + }, + "ip_address_name": { + Description: "IP address name for the service attachment", + Type: schema.TypeString, + Computed: true, + }, + "forwarding_rule_name": { + Description: "Name of the forwarding rule for the service attachment", + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func resourceRedisCloudPrivateServiceConnectEndpointCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + api := meta.(*apiClient) + + subscriptionId, err := strconv.Atoi(d.Get("subscription_id").(string)) + if err != nil { + return diag.FromErr(err) + } + subscriptionMutex.Lock(subscriptionId) + defer subscriptionMutex.Unlock(subscriptionId) + + pscServiceId := d.Get("private_service_connect_service_id").(int) + gcpProjectId := d.Get("gcp_project_id").(string) + gcpVpcName := d.Get("gcp_vpc_name").(string) + gcpVpcSubnetName := d.Get("gcp_vpc_subnet_name").(string) + endpointConnectionNamePrefix := d.Get("endpoint_connection_name").(string) + + endpointId, err := api.client.PrivateServiceConnect.CreateEndpoint(ctx, subscriptionId, pscServiceId, psc.CreatePrivateServiceConnectEndpoint{ + GCPProjectID: &gcpProjectId, + GCPVPCName: &gcpVpcName, + GCPVPCSubnetName: &gcpVpcSubnetName, + EndpointConnectionName: &endpointConnectionNamePrefix, + }) + + if err != nil { + return diag.FromErr(err) + } + + d.SetId(buildPrivateServiceConnectEndpointId(subscriptionId, pscServiceId, endpointId)) + + err = waitForSubscriptionToBeActive(ctx, subscriptionId, api) + if err != nil { + return diag.FromErr(err) + } + + return resourceRedisCloudPrivateServiceConnectEndpointRead(ctx, d, meta) +} + +func resourceRedisCloudPrivateServiceConnectEndpointRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + api := meta.(*apiClient) + + resId, err := toPscEndpointId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + endpoints, err := api.client.PrivateServiceConnect.GetEndpoints(ctx, resId.subscriptionId, resId.pscServiceId) + if err != nil { + var notFound *psc.NotFound + if errors.As(err, ¬Found) { + d.SetId("") + return diags + } + return diag.FromErr(err) + } + + endpoint := findPrivateServiceConnectEndpoints(resId.endpointId, endpoints.Endpoints) + if endpoint == nil { + d.SetId("") + return diags + } + + d.SetId(buildPrivateServiceConnectEndpointId(resId.subscriptionId, resId.pscServiceId, *endpoint.ID)) + + if redis.StringValue(endpoint.Status) != psc.EndpointStatusRejected && redis.StringValue(endpoint.Status) != psc.EndpointStatusDeleted { + creationScript, err := api.client.PrivateServiceConnect.GetEndpointCreationScripts(ctx, + resId.subscriptionId, resId.pscServiceId, *endpoint.ID, true) + if err != nil { + var notFound *psc.NotFound + if errors.As(err, ¬Found) { + d.SetId("") + return diags + } + return diag.FromErr(err) + } + + if err := d.Set("service_attachments", flattenPrivateServiceConnectEndpointServiceAttachments(creationScript.Script.TerraformGcp.ServiceAttachments)); err != nil { + return diag.FromErr(err) + } + } else { + if err := d.Set("service_attachments", []any{}); err != nil { + return diag.FromErr(err) + } + } + + err = d.Set("subscription_id", strconv.Itoa(resId.subscriptionId)) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("private_service_connect_service_id", resId.pscServiceId) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("private_service_connect_endpoint_id", endpoint.ID) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("gcp_project_id", endpoint.GCPProjectID) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("gcp_vpc_name", endpoint.GCPVPCName) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("gcp_vpc_subnet_name", endpoint.GCPVPCSubnetName) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("endpoint_connection_name", endpoint.EndpointConnectionName) + if err != nil { + return diag.FromErr(err) + } + + return diags +} + +func resourceRedisCloudPrivateServiceConnectEndpointDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + api := meta.(*apiClient) + + resId, err := toPscEndpointId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + subscriptionMutex.Lock(resId.subscriptionId) + defer subscriptionMutex.Unlock(resId.subscriptionId) + + endpoints, err := api.client.PrivateServiceConnect.GetEndpoints(ctx, resId.subscriptionId, resId.pscServiceId) + if err != nil { + var notFound *psc.NotFound + if errors.As(err, ¬Found) { + d.SetId("") + return diags + } + return diag.FromErr(err) + } + + endpoint := findPrivateServiceConnectEndpoints(resId.endpointId, endpoints.Endpoints) + if endpoint == nil { + d.SetId("") + return diags + } + + if redis.StringValue(endpoint.Status) == psc.EndpointStatusInitialized { + // It's only possible to delete an endpoint in initialized status + err = api.client.PrivateServiceConnect.DeleteEndpoint(ctx, resId.subscriptionId, resId.pscServiceId, resId.endpointId) + if err != nil { + return diag.FromErr(err) + } + return diags + } + + // Endpoints will be automatically removed once related GCP resources are removed. So we will wait for this + // to happen, but we can't check the GCP resources from this provider + err = waitForPrivateServiceConnectServiceEndpointDisappear(ctx, func() (result interface{}, state string, err error) { + return refreshPrivateServiceConnectServiceEndpointDisappear(ctx, resId.subscriptionId, resId.pscServiceId, resId.endpointId, api) + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + + return diags +} + +func buildPrivateServiceConnectEndpointId(subId int, pscId int, endpointId int) string { + return privateServiceConnectEndpointId{ + subscriptionId: subId, + pscServiceId: pscId, + endpointId: endpointId}.String() +} + +type privateServiceConnectEndpointId struct { + subscriptionId int + pscServiceId int + endpointId int +} + +func (p privateServiceConnectEndpointId) String() string { + return fmt.Sprintf("%d/%d/%d", p.subscriptionId, p.pscServiceId, p.endpointId) +} + +func toPscEndpointId(id string) (*privateServiceConnectEndpointId, error) { + parts := strings.Split(id, "/") + if len(parts) != 3 { + return nil, fmt.Errorf("invalid id: %s", id) + } + + subId, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, err + } + + pscId, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, err + } + + endpointId, err := strconv.Atoi(parts[2]) + if err != nil { + return nil, err + } + + return &privateServiceConnectEndpointId{ + subscriptionId: subId, + pscServiceId: pscId, + endpointId: endpointId, + }, nil +} + +func refreshPrivateServiceConnectServiceEndpointDisappear(ctx context.Context, subscriptionId int, + pscServiceId int, endpointId int, api *apiClient) (result interface{}, state string, err error) { + + log.Printf("[DEBUG] Waiting for private service connect service endpoint %d/%d/%d to be deleted", + subscriptionId, pscServiceId, endpointId) + + endpoints, err := api.client.PrivateServiceConnect.GetEndpoints(ctx, subscriptionId, pscServiceId) + if err != nil { + return nil, "", err + } + + endpoint := findPrivateServiceConnectEndpoints(endpointId, endpoints.Endpoints) + if endpoint == nil { + return placeholderStatusDisappear, placeholderStatusDisappear, nil + } + + return redis.StringValue(endpoint.Status), redis.StringValue(endpoint.Status), nil +} + +func findPrivateServiceConnectEndpoints(id int, endpoints []*psc.PrivateServiceConnectEndpoint) *psc.PrivateServiceConnectEndpoint { + for _, endpoint := range endpoints { + if redis.IntValue(endpoint.ID) == id { + return endpoint + } + } + return nil +} + +func flattenPrivateServiceConnectEndpointServiceAttachments(serviceAttachments []psc.TerraformGCPServiceAttachment) []map[string]interface{} { + var rl []map[string]interface{} + for _, serviceAttachment := range serviceAttachments { + + serviceAttachmentMapString := map[string]interface{}{ + "name": serviceAttachment.Name, + "dns_record": serviceAttachment.DNSRecord, + "ip_address_name": serviceAttachment.IPAddressName, + "forwarding_rule_name": serviceAttachment.ForwardingRuleName, + } + + rl = append(rl, serviceAttachmentMapString) + } + + return rl +} + +func waitForPrivateServiceConnectServiceEndpointDisappear(ctx context.Context, refreshFunc func() (result interface{}, state string, err error)) error { + wait := &retry.StateChangeConf{ + Pending: []string{ + psc.EndpointStatusProcessing, + psc.EndpointStatusPending, + psc.EndpointStatusAcceptPending, + psc.EndpointStatusActive, + psc.EndpointStatusDeleted, + psc.EndpointStatusRejected, + psc.EndpointStatusRejectPending, + psc.EndpointStatusFailed, + }, + Target: []string{placeholderStatusDisappear}, + Timeout: safetyTimeout, + Delay: 10 * time.Second, + PollInterval: 30 * time.Second, + + Refresh: refreshFunc, + } + if _, err := wait.WaitForStateContext(ctx); err != nil { + return err + } + + return nil +} diff --git a/provider/resource_rediscloud_private_service_connect_endpoint_accepter.go b/provider/resource_rediscloud_private_service_connect_endpoint_accepter.go new file mode 100644 index 00000000..d37945fb --- /dev/null +++ b/provider/resource_rediscloud_private_service_connect_endpoint_accepter.go @@ -0,0 +1,304 @@ +package provider + +import ( + "context" + "errors" + "fmt" + "log" + "strconv" + "strings" + "time" + + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/rediscloud-go-api/service/psc" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceRedisCloudPrivateServiceConnectEndpointAccepter() *schema.Resource { + return &schema.Resource{ + Description: "Manages the state of Private Service Connect Endpoint to a Pro Subscription in your Redis Enterprise Cloud Account.", + CreateContext: resourceRedisCloudPrivateServiceConnectEndpointAccepterCreate, + ReadContext: resourceRedisCloudPrivateServiceConnectEndpointAccepterRead, + UpdateContext: resourceRedisCloudPrivateServiceConnectEndpointAccepterUpdate, + DeleteContext: resourceRedisCloudPrivateServiceConnectEndpointAccepterDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Read: schema.DefaultTimeout(10 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "subscription_id": { + Description: "The ID of the Pro subscription to attach", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "private_service_connect_service_id": { + Description: "The ID of the Private Service Connect", + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "private_service_connect_endpoint_id": { + Description: "The ID of the Private Service Connect Endpoint", + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "action": { + Description: "Accept or reject the endpoint", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{psc.EndpointActionAccept, psc.EndpointActionReject}, false)), + }, + }, + } +} + +func resourceRedisCloudPrivateServiceConnectEndpointAccepterCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + api := meta.(*apiClient) + + subscriptionId, err := strconv.Atoi(d.Get("subscription_id").(string)) + if err != nil { + return diag.FromErr(err) + } + subscriptionMutex.Lock(subscriptionId) + defer subscriptionMutex.Unlock(subscriptionId) + + pscServiceId := d.Get("private_service_connect_service_id").(int) + endpointId := d.Get("private_service_connect_endpoint_id").(int) + action := d.Get("action").(string) + + endpoints, err := api.client.PrivateServiceConnect.GetEndpoints(ctx, subscriptionId, pscServiceId) + if err != nil { + var notFound *psc.NotFoundActiveActive + if errors.As(err, ¬Found) { + d.SetId("") + return diags + } + return diag.FromErr(err) + } + + endpoint := findPrivateServiceConnectEndpoints(endpointId, endpoints.Endpoints) + if endpoint == nil { + return diag.FromErr(fmt.Errorf("endpoint with id %d not found", endpointId)) + } + + if endpoint.Status == nil { + return diag.FromErr(fmt.Errorf("endpoint with id %d has no status", endpointId)) + } + + if redis.StringValue(endpoint.Status) == psc.EndpointStatusActive && action == psc.EndpointActionAccept { + d.SetId(buildPrivateServiceConnectEndpointAccepterId(subscriptionId, pscServiceId, endpointId)) + return diag.Diagnostics{} + } + + if redis.StringValue(endpoint.Status) == psc.EndpointStatusRejected && action == psc.EndpointActionReject { + d.SetId(buildPrivateServiceConnectEndpointAccepterId(subscriptionId, pscServiceId, endpointId)) + return diag.Diagnostics{} + } + + refreshFunc := func(targetStatus string) (result interface{}, state string, err error) { + return refreshPrivateServiceConnectServiceEndpointStatus(ctx, subscriptionId, pscServiceId, endpointId, targetStatus, api) + } + + if redis.StringValue(endpoint.Status) == psc.EndpointStatusInitialized || redis.StringValue(endpoint.Status) == psc.EndpointStatusProcessing { + err = waitForPrivateServiceConnectServiceEndpointToBePending(ctx, refreshFunc) + if err != nil { + return diag.FromErr(err) + } + } + + d.SetId(buildPrivateServiceConnectEndpointAccepterId(subscriptionId, pscServiceId, endpointId)) + + err = api.client.PrivateServiceConnect.UpdateEndpoint(ctx, subscriptionId, pscServiceId, endpointId, &psc.UpdatePrivateServiceConnectEndpoint{ + Action: redis.String(action), + }) + if err != nil { + return diag.FromErr(err) + } + + if action == psc.EndpointActionAccept { + err = waitForPrivateServiceConnectServiceEndpointToBeActive(ctx, refreshFunc) + if err != nil { + return diag.FromErr(err) + } + } else { + err = waitForPrivateServiceConnectServiceEndpointToBeRejected(ctx, refreshFunc) + if err != nil { + return diag.FromErr(err) + } + } + + return resourceRedisCloudPrivateServiceConnectEndpointAccepterRead(ctx, d, meta) +} + +func resourceRedisCloudPrivateServiceConnectEndpointAccepterRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + api := meta.(*apiClient) + + resId, err := toPscEndpointAccepterId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + endpoints, err := api.client.PrivateServiceConnect.GetEndpoints(ctx, resId.subscriptionId, resId.pscServiceId) + if err != nil { + var notFound *psc.NotFound + if errors.As(err, ¬Found) { + d.SetId("") + return diags + } + return diag.FromErr(err) + } + + endpoint := findPrivateServiceConnectEndpoints(resId.endpointId, endpoints.Endpoints) + if endpoint == nil { + d.SetId("") + return diags + } + + d.SetId(buildPrivateServiceConnectEndpointAccepterId(resId.subscriptionId, resId.pscServiceId, redis.IntValue(endpoint.ID))) + + err = d.Set("subscription_id", strconv.Itoa(resId.subscriptionId)) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("private_service_connect_service_id", resId.pscServiceId) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("private_service_connect_endpoint_id", redis.IntValue(endpoint.ID)) + if err != nil { + return diag.FromErr(err) + } + + return diags +} + +func buildPrivateServiceConnectEndpointAccepterId(subId int, pscId int, endpointId int) string { + return privateServiceConnectEndpointId{ + subscriptionId: subId, + pscServiceId: pscId, + endpointId: endpointId}.String() +} + +type privateServiceConnectEndpointAccepterId struct { + subscriptionId int + pscServiceId int + endpointId int +} + +func (p privateServiceConnectEndpointAccepterId) String() string { + return fmt.Sprintf("%d/%d/%d", p.subscriptionId, p.pscServiceId, p.endpointId) +} + +func toPscEndpointAccepterId(id string) (*privateServiceConnectEndpointAccepterId, error) { + parts := strings.Split(id, "/") + if len(parts) != 3 { + return nil, fmt.Errorf("invalid id: %s", id) + } + + subId, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, err + } + + pscId, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, err + } + + endpointId, err := strconv.Atoi(parts[2]) + if err != nil { + return nil, err + } + + return &privateServiceConnectEndpointAccepterId{ + subscriptionId: subId, + pscServiceId: pscId, + endpointId: endpointId, + }, nil +} + +func resourceRedisCloudPrivateServiceConnectEndpointAccepterUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return resourceRedisCloudPrivateServiceConnectEndpointAccepterCreate(ctx, d, meta) +} + +func resourceRedisCloudPrivateServiceConnectEndpointAccepterDelete(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + var diags diag.Diagnostics + d.SetId("") + return diags +} + +func refreshPrivateServiceConnectServiceEndpointStatus(ctx context.Context, subscriptionId int, + pscServiceId int, endpointId int, targetStatus string, api *apiClient) (result interface{}, state string, err error) { + log.Printf("[DEBUG] Waiting for private service connect service endpoint status %d/%d/%d to be %s", + subscriptionId, pscServiceId, endpointId, targetStatus) + + endpoints, err := api.client.PrivateServiceConnect.GetEndpoints(ctx, subscriptionId, pscServiceId) + if err != nil { + return nil, "", err + } + + endpoint := findPrivateServiceConnectEndpoints(endpointId, endpoints.Endpoints) + if endpoint == nil { + return nil, "", fmt.Errorf("endpoint with id %d not found", endpointId) + } + + return redis.StringValue(endpoint.Status), redis.StringValue(endpoint.Status), nil +} + +func waitForPrivateServiceConnectServiceEndpointToBePending(ctx context.Context, refreshFunc func(targetStatus string) (result interface{}, state string, err error)) error { + targetStatus := psc.EndpointStatusPending + return waitForPrivateServiceConnectServiceEndpointToBeInStatus(ctx, func() (result interface{}, state string, err error) { + return refreshFunc(targetStatus) + }, targetStatus, []string{ + psc.EndpointStatusInitialized, + psc.EndpointStatusProcessing}) +} + +func waitForPrivateServiceConnectServiceEndpointToBeActive(ctx context.Context, refreshFunc func(targetStatus string) (result interface{}, state string, err error)) error { + targetStatus := psc.EndpointStatusActive + return waitForPrivateServiceConnectServiceEndpointToBeInStatus(ctx, func() (result interface{}, state string, err error) { + return refreshFunc(targetStatus) + }, targetStatus, []string{ + psc.EndpointStatusPending, + psc.EndpointStatusAcceptPending}) +} + +func waitForPrivateServiceConnectServiceEndpointToBeRejected(ctx context.Context, refreshFunc func(targetStatus string) (result interface{}, state string, err error)) error { + targetStatus := psc.EndpointStatusRejected + return waitForPrivateServiceConnectServiceEndpointToBeInStatus(ctx, func() (result interface{}, state string, err error) { + return refreshFunc(targetStatus) + }, targetStatus, []string{ + psc.EndpointStatusPending, + psc.EndpointStatusRejectPending}) +} + +func waitForPrivateServiceConnectServiceEndpointToBeInStatus(ctx context.Context, + refreshFunc func() (result interface{}, state string, err error), status string, pendingStatus []string) error { + wait := &retry.StateChangeConf{ + Pending: pendingStatus, + Target: []string{status}, + Timeout: safetyTimeout, + Delay: 10 * time.Second, + PollInterval: 30 * time.Second, + + Refresh: refreshFunc, + } + if _, err := wait.WaitForStateContext(ctx); err != nil { + return err + } + + return nil +} diff --git a/provider/utils.go b/provider/utils.go index c3fa7b42..eeb2b0c1 100644 --- a/provider/utils.go +++ b/provider/utils.go @@ -2,14 +2,15 @@ package provider import ( "fmt" + "sync" + "time" + "github.com/RedisLabs/rediscloud-go-api/redis" "github.com/RedisLabs/rediscloud-go-api/service/latest_backups" "github.com/RedisLabs/rediscloud-go-api/service/latest_imports" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "sync" - "time" ) // This timeout is an absolute maximum used in some of the waitForStatus operations concerning creation and updating From f78162134447041162dc91527cb93dd099d23c03 Mon Sep 17 00:00:00 2001 From: Mateus Pimenta <1920261+matpimenta@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:02:18 +0000 Subject: [PATCH 2/3] Configure provider to connect to GCP during tests --- .github/workflows/terraform_provider.yml | 4 ++-- provider/provider_test.go | 4 ++-- ...e_active_private_service_connect_endpoint_accepter_test.go | 2 +- ...oud_active_active_private_service_connect_endpoint_test.go | 2 +- ...discloud_private_service_connect_endpoint_accepter_test.go | 2 +- provider/rediscloud_private_service_connect_endpoint_test.go | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/terraform_provider.yml b/.github/workflows/terraform_provider.yml index 94f92c7a..8679728f 100644 --- a/.github/workflows/terraform_provider.yml +++ b/.github/workflows/terraform_provider.yml @@ -126,8 +126,8 @@ jobs: AWS_SIGNIN_URL: ${{ secrets.CLOUD_ACCOUNT_URL }} GCP_VPC_PROJECT: ${{ secrets.GCP_VPC_PROJECT }} GCP_VPC_ID: ${{ secrets.GCP_VPC_ID }} - # TODO - GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} + GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} + GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }} - name: Generate code coverage report if: steps.filter.outputs.code-changes == 'true' && (success() || failure()) run: make generate_coverage diff --git a/provider/provider_test.go b/provider/provider_test.go index 76d00940..f76ee953 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -44,11 +44,11 @@ func testAccAwsPeeringPreCheck(t *testing.T) { } func testAccGcpProjectPreCheck(t *testing.T) { - requireEnvironmentVariables(t, "GCP_VPC_PROJECT") + requireEnvironmentVariables(t, "GCP_PROJECT_ID") } func testAccGcpCredentialsPreCheck(t *testing.T) { - requireEnvironmentVariables(t, "GOOGLE_APPLICATION_CREDENTIALS") + requireEnvironmentVariables(t, "GOOGLE_CREDENTIALS") } func testAccAwsPreExistingTgwCheck(t *testing.T) { diff --git a/provider/rediscloud_active_active_private_service_connect_endpoint_accepter_test.go b/provider/rediscloud_active_active_private_service_connect_endpoint_accepter_test.go index 96dda986..b1f52516 100644 --- a/provider/rediscloud_active_active_private_service_connect_endpoint_accepter_test.go +++ b/provider/rediscloud_active_active_private_service_connect_endpoint_accepter_test.go @@ -18,7 +18,7 @@ func TestAccResourceRedisCloudActiveActivePrivateServiceConnectEndpointAccepter_ baseName := acctest.RandomWithPrefix(testResourcePrefix) + "-pro-pscea" const resourceName = "rediscloud_active_active_private_service_connect_endpoint_accepter.accepter" - gcpProjectId := os.Getenv("GCP_VPC_PROJECT") + gcpProjectId := os.Getenv("GCP_PROJECT_ID") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccGcpProjectPreCheck(t); testAccGcpCredentialsPreCheck(t) }, diff --git a/provider/rediscloud_active_active_private_service_connect_endpoint_test.go b/provider/rediscloud_active_active_private_service_connect_endpoint_test.go index 826c00ab..f611fda3 100644 --- a/provider/rediscloud_active_active_private_service_connect_endpoint_test.go +++ b/provider/rediscloud_active_active_private_service_connect_endpoint_test.go @@ -16,7 +16,7 @@ func TestAccResourceRedisCloudActiveActivePrivateServiceConnectEndpoint_CRUDI(t const resourceName = "rediscloud_active_active_private_service_connect_endpoint.psce" const datasourceName = "data.rediscloud_active_active_private_service_connect_endpoints.psce" - gcpProjectId := os.Getenv("GCP_VPC_PROJECT") + gcpProjectId := os.Getenv("GCP_PROJECT_ID") gcpVPCName := fmt.Sprintf("%s-network", baseName) gcpVPCSubnetName := fmt.Sprintf("%s-subnet", baseName) diff --git a/provider/rediscloud_private_service_connect_endpoint_accepter_test.go b/provider/rediscloud_private_service_connect_endpoint_accepter_test.go index ba2f5970..5c70e2f4 100644 --- a/provider/rediscloud_private_service_connect_endpoint_accepter_test.go +++ b/provider/rediscloud_private_service_connect_endpoint_accepter_test.go @@ -18,7 +18,7 @@ func TestAccResourceRedisCloudPrivateServiceConnectEndpointAccepter_Create(t *te baseName := acctest.RandomWithPrefix(testResourcePrefix) + "-pro-pscea" const resourceName = "rediscloud_private_service_connect_endpoint_accepter.accepter" - gcpProjectId := os.Getenv("GCP_VPC_PROJECT") + gcpProjectId := os.Getenv("GCP_PROJECT_ID") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccGcpProjectPreCheck(t); testAccGcpCredentialsPreCheck(t) }, diff --git a/provider/rediscloud_private_service_connect_endpoint_test.go b/provider/rediscloud_private_service_connect_endpoint_test.go index 13a4ad50..21ebcdec 100644 --- a/provider/rediscloud_private_service_connect_endpoint_test.go +++ b/provider/rediscloud_private_service_connect_endpoint_test.go @@ -16,7 +16,7 @@ func TestAccResourceRedisCloudPrivateServiceConnectEndpoint_CRUDI(t *testing.T) const resourceName = "rediscloud_private_service_connect_endpoint.psce" const datasourceName = "data.rediscloud_private_service_connect_endpoints.psce" - gcpProjectId := os.Getenv("GCP_VPC_PROJECT") + gcpProjectId := os.Getenv("GCP_PROJECT_ID") gcpVPCName := fmt.Sprintf("%s-network", baseName) gcpVPCSubnetName := fmt.Sprintf("%s-subnet", baseName) From febf5b9f30f4a43440cb4a7dc25713133393c7bb Mon Sep 17 00:00:00 2001 From: Mateus Pimenta <1920261+matpimenta@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:20:59 +0000 Subject: [PATCH 3/3] Update and pin main GH actions to a specific commit --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/terraform_provider.yml | 26 ++++++++++++------------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1b7a5fe8..6a9886ac 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f13736b0..fa2826b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,13 +20,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Unshallow run: git fetch --prune --unshallow - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version-file: go.mod - diff --git a/.github/workflows/terraform_provider.yml b/.github/workflows/terraform_provider.yml index 8679728f..b18f5e2e 100644 --- a/.github/workflows/terraform_provider.yml +++ b/.github/workflows/terraform_provider.yml @@ -21,8 +21,8 @@ jobs: name: go build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 - - uses: actions/cache@v3.3.2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 continue-on-error: true id: cache-terraform-plugin-dir timeout-minutes: 2 @@ -30,7 +30,7 @@ jobs: path: terraform-plugin-dir key: ${{ runner.os }}-terraform-plugin-dir-${{ hashFiles('go.sum') }}-${{ hashFiles('provider/**') }} - if: steps.cache-terraform-plugin-dir.outputs.cache-hit != 'true' || steps.cache-terraform-plugin-dir.outcome == 'failure' - uses: actions/setup-go@v4 + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version-file: go.mod - if: steps.cache-terraform-plugin-dir.outputs.cache-hit != 'true' || steps.cache-terraform-plugin-dir.outcome == 'failure' @@ -45,8 +45,8 @@ jobs: needs: [go_build] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 - - uses: actions/cache@v3.3.2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 continue-on-error: true id: cache-terraform-providers-schema timeout-minutes: 2 @@ -54,13 +54,13 @@ jobs: path: terraform-providers-schema key: ${{ runner.os }}-terraform-providers-schema-${{ hashFiles('go.sum') }}-${{ hashFiles('provider/**') }} - if: steps.cache-terraform-providers-schema.outputs.cache-hit != 'true' || steps.cache-terraform-providers-schema.outcome == 'failure' - uses: actions/cache@v3.3.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 timeout-minutes: 2 with: path: terraform-plugin-dir key: ${{ runner.os }}-terraform-plugin-dir-${{ hashFiles('go.sum') }}-${{ hashFiles('provider/**') }} - if: steps.cache-terraform-providers-schema.outputs.cache-hit != 'true' || steps.cache-terraform-providers-schema.outcome == 'failure' - uses: hashicorp/setup-terraform@v3.0.0 + uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 with: terraform_version: ${{ env.TERRAFORM_VERSION }} terraform_wrapper: false @@ -91,11 +91,11 @@ jobs: needs: [go_build] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 - - uses: actions/setup-go@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version-file: go.mod - - uses: dorny/paths-filter@v2.11.1 + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 id: filter with: filters: | @@ -133,7 +133,7 @@ jobs: run: make generate_coverage - name: Upload code coverage report if: steps.filter.outputs.code-changes == 'true' && (success() || failure()) - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: coverage.html path: bin/coverage.html @@ -143,8 +143,8 @@ jobs: needs: [go_build] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 - - uses: actions/setup-go@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version-file: go.mod - run: make tfproviderlint