diff --git a/.changelog/3818.txt b/.changelog/3818.txt new file mode 100644 index 0000000000..42f48c13d2 --- /dev/null +++ b/.changelog/3818.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/mongodbatlas_stream_privatelink_endpoint: Adds support for GCP Confluent +``` + +```release-note:enhancement +data-source/mongodbatlas_stream_privatelink_endpoint: Adds support for GCP Confluent +``` + +```release-note:enhancement +data-source/mongodbatlas_stream_privatelink_endpoints: Adds support for GCP Confluent +``` \ No newline at end of file diff --git a/docs/data-sources/stream_privatelink_endpoint.md b/docs/data-sources/stream_privatelink_endpoint.md index b9e95589c0..800a0e5b8d 100644 --- a/docs/data-sources/stream_privatelink_endpoint.md +++ b/docs/data-sources/stream_privatelink_endpoint.md @@ -253,6 +253,45 @@ output "privatelink_endpoint_id" { } ``` +### GCP Confluent Privatelink +```terraform +resource "mongodbatlas_stream_privatelink_endpoint" "gcp_confluent" { + project_id = var.project_id + + provider_name = "GCP" + vendor = "CONFLUENT" + region = var.gcp_region + + dns_domain = var.confluent_dns_domain + dns_sub_domain = var.confluent_dns_subdomains + + service_attachment_uris = [ + "projects/my-project/regions/us-west1/serviceAttachments/confluent-attachment-1", + "projects/my-project/regions/us-west1/serviceAttachments/confluent-attachment-2" + ] +} + +data "mongodbatlas_stream_privatelink_endpoint" "gcp_confluent" { + project_id = var.project_id + id = mongodbatlas_stream_privatelink_endpoint.gcp_confluent.id +} + +output "privatelink_endpoint_id" { + description = "The ID of the MongoDB Atlas Stream Private Link Endpoint" + value = mongodbatlas_stream_privatelink_endpoint.gcp_confluent.id +} + +output "privatelink_endpoint_state" { + description = "The state of the MongoDB Atlas Stream Private Link Endpoint" + value = data.mongodbatlas_stream_privatelink_endpoint.gcp_confluent.state +} + +output "service_attachment_uris" { + description = "The GCP service attachment URIs used for the private link" + value = mongodbatlas_stream_privatelink_endpoint.gcp_confluent.service_attachment_uris +} +``` + ## Schema @@ -274,8 +313,9 @@ output "privatelink_endpoint_id" { - `interface_endpoint_id` (String) Interface endpoint ID that is created from the specified service endpoint ID. - `interface_endpoint_name` (String) Name of interface endpoint that is created from the specified service endpoint ID. - `provider_account_id` (String) Account ID from the cloud provider. -- `provider_name` (String) Provider where the endpoint is deployed. Valid values are AWS and AZURE. +- `provider_name` (String) Provider where the endpoint is deployed. Valid values are AWS, AZURE, and GCP. - `region` (String) The region of the Provider’s cluster. See [AZURE](https://www.mongodb.com/docs/atlas/reference/microsoft-azure/#stream-processing-instances) and [AWS](https://www.mongodb.com/docs/atlas/reference/amazon-aws/#stream-processing-instances) supported regions. When the vendor is `CONFLUENT`, this is the domain name of Confluent cluster. When the vendor is `MSK`, this is computed by the API from the provided `arn`. +- `service_attachment_uris` (List of String) List of GCP service attachment URIs for Confluent vendor. Required for GCP provider with CONFLUENT vendor. - `service_endpoint_id` (String) For AZURE EVENTHUB, this is the [namespace endpoint ID](https://learn.microsoft.com/en-us/rest/api/eventhub/namespaces/get). For AWS CONFLUENT cluster, this is the [VPC Endpoint service name](https://docs.confluent.io/cloud/current/networking/private-links/aws-privatelink.html). - `state` (String) Status of the connection. - `vendor` (String) Vendor that manages the endpoint. The following are the vendor values per provider: @@ -284,4 +324,6 @@ output "privatelink_endpoint_id" { * **Azure**: EVENTHUB and CONFLUENT + * **GCP**: CONFLUENT + For more information see: [MongoDB Atlas API - Streams Privatelink](https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/operation/operation-createprivatelinkconnection) Documentation. diff --git a/docs/data-sources/stream_privatelink_endpoints.md b/docs/data-sources/stream_privatelink_endpoints.md index 62ad0f29f7..0be2540009 100644 --- a/docs/data-sources/stream_privatelink_endpoints.md +++ b/docs/data-sources/stream_privatelink_endpoints.md @@ -253,6 +253,45 @@ output "privatelink_endpoint_id" { } ``` +### GCP Confluent Privatelink +```terraform +resource "mongodbatlas_stream_privatelink_endpoint" "gcp_confluent" { + project_id = var.project_id + + provider_name = "GCP" + vendor = "CONFLUENT" + region = var.gcp_region + + dns_domain = var.confluent_dns_domain + dns_sub_domain = var.confluent_dns_subdomains + + service_attachment_uris = [ + "projects/my-project/regions/us-west1/serviceAttachments/confluent-attachment-1", + "projects/my-project/regions/us-west1/serviceAttachments/confluent-attachment-2" + ] +} + +data "mongodbatlas_stream_privatelink_endpoint" "gcp_confluent" { + project_id = var.project_id + id = mongodbatlas_stream_privatelink_endpoint.gcp_confluent.id +} + +output "privatelink_endpoint_id" { + description = "The ID of the MongoDB Atlas Stream Private Link Endpoint" + value = mongodbatlas_stream_privatelink_endpoint.gcp_confluent.id +} + +output "privatelink_endpoint_state" { + description = "The state of the MongoDB Atlas Stream Private Link Endpoint" + value = data.mongodbatlas_stream_privatelink_endpoint.gcp_confluent.state +} + +output "service_attachment_uris" { + description = "The GCP service attachment URIs used for the private link" + value = mongodbatlas_stream_privatelink_endpoint.gcp_confluent.service_attachment_uris +} +``` + ## Schema @@ -282,8 +321,9 @@ Read-Only: - `interface_endpoint_name` (String) Name of interface endpoint that is created from the specified service endpoint ID. - `project_id` (String) Unique 24-hexadecimal digit string that identifies your project. Use the [/groups](#tag/Projects/operation/listProjects) endpoint to retrieve all projects to which the authenticated user has access.
**NOTE**: Groups and projects are synonymous terms. Your group id is the same as your project id. For existing groups, your group or project id remains the same. The resource and corresponding endpoints use the term groups. - `provider_account_id` (String) Account ID from the cloud provider. -- `provider_name` (String) Provider where the endpoint is deployed. Valid values are AWS and AZURE. +- `provider_name` (String) Provider where the endpoint is deployed. Valid values are AWS, AZURE, and GCP. - `region` (String) The region of the Provider’s cluster. See [AZURE](https://www.mongodb.com/docs/atlas/reference/microsoft-azure/#stream-processing-instances) and [AWS](https://www.mongodb.com/docs/atlas/reference/amazon-aws/#stream-processing-instances) supported regions. When the vendor is `CONFLUENT`, this is the domain name of Confluent cluster. When the vendor is `MSK`, this is computed by the API from the provided `arn`. +- `service_attachment_uris` (List of String) List of GCP service attachment URIs for Confluent vendor. Required for GCP provider with CONFLUENT vendor. - `service_endpoint_id` (String) For AZURE EVENTHUB, this is the [namespace endpoint ID](https://learn.microsoft.com/en-us/rest/api/eventhub/namespaces/get). For AWS CONFLUENT cluster, this is the [VPC Endpoint service name](https://docs.confluent.io/cloud/current/networking/private-links/aws-privatelink.html). - `state` (String) Status of the connection. - `vendor` (String) Vendor that manages the endpoint. The following are the vendor values per provider: @@ -292,4 +332,6 @@ Read-Only: * **Azure**: EVENTHUB and CONFLUENT + * **GCP**: CONFLUENT + For more information see: [MongoDB Atlas API - Streams Privatelink](https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/operation/operation-createprivatelinkconnection) Documentation. diff --git a/docs/resources/stream_privatelink_endpoint.md b/docs/resources/stream_privatelink_endpoint.md index db8aead2e1..1b9d2bf19d 100644 --- a/docs/resources/stream_privatelink_endpoint.md +++ b/docs/resources/stream_privatelink_endpoint.md @@ -253,11 +253,51 @@ output "privatelink_endpoint_id" { } ``` +### GCP Confluent Privatelink +```terraform +resource "mongodbatlas_stream_privatelink_endpoint" "gcp_confluent" { + project_id = var.project_id + + provider_name = "GCP" + vendor = "CONFLUENT" + region = var.gcp_region + + dns_domain = var.confluent_dns_domain + dns_sub_domain = var.confluent_dns_subdomains + + service_attachment_uris = [ + "projects/my-project/regions/us-west1/serviceAttachments/confluent-attachment-1", + "projects/my-project/regions/us-west1/serviceAttachments/confluent-attachment-2" + ] +} + +data "mongodbatlas_stream_privatelink_endpoint" "gcp_confluent" { + project_id = var.project_id + id = mongodbatlas_stream_privatelink_endpoint.gcp_confluent.id +} + +output "privatelink_endpoint_id" { + description = "The ID of the MongoDB Atlas Stream Private Link Endpoint" + value = mongodbatlas_stream_privatelink_endpoint.gcp_confluent.id +} + +output "privatelink_endpoint_state" { + description = "The state of the MongoDB Atlas Stream Private Link Endpoint" + value = data.mongodbatlas_stream_privatelink_endpoint.gcp_confluent.state +} + +output "service_attachment_uris" { + description = "The GCP service attachment URIs used for the private link" + value = mongodbatlas_stream_privatelink_endpoint.gcp_confluent.service_attachment_uris +} +``` + ### Further Examples - [AWS Confluent PrivateLink](https://github.com/mongodb/terraform-provider-mongodbatlas/tree/v2.1.0/examples/mongodbatlas_stream_privatelink_endpoint/confluent_serverless) - [Confluent Dedicated Cluster](https://github.com/mongodb/terraform-provider-mongodbatlas/tree/v2.1.0/examples/mongodbatlas_stream_privatelink_endpoint/confluent_dedicated_cluster) - [AWS MSK PrivateLink](https://github.com/mongodb/terraform-provider-mongodbatlas/tree/v2.1.0/examples/mongodbatlas_stream_privatelink_endpoint/aws_msk_cluster) - [AWS S3 PrivateLink](https://github.com/mongodb/terraform-provider-mongodbatlas/tree/v2.1.0/examples/mongodbatlas_stream_privatelink_endpoint/s3) +- [GCP Confluent PrivateLink](https://github.com/mongodb/terraform-provider-mongodbatlas/tree/v2.1.0/examples/mongodbatlas_stream_privatelink_endpoint/gcp_confluent) - [Azure PrivateLink](https://github.com/mongodb/terraform-provider-mongodbatlas/tree/v2.1.0/examples/mongodbatlas_stream_privatelink_endpoint/azure) @@ -266,13 +306,15 @@ output "privatelink_endpoint_id" { ### Required - `project_id` (String) Unique 24-hexadecimal digit string that identifies your project. Use the [/groups](#tag/Projects/operation/listProjects) endpoint to retrieve all projects to which the authenticated user has access.
**NOTE**: Groups and projects are synonymous terms. Your group id is the same as your project id. For existing groups, your group or project id remains the same. The resource and corresponding endpoints use the term groups. -- `provider_name` (String) Provider where the endpoint is deployed. Valid values are AWS and AZURE. +- `provider_name` (String) Provider where the endpoint is deployed. Valid values are AWS, AZURE, and GCP. - `vendor` (String) Vendor that manages the endpoint. The following are the vendor values per provider: * **AWS**: MSK, CONFLUENT, and S3 * **Azure**: EVENTHUB and CONFLUENT + * **GCP**: CONFLUENT + ### Optional - `arn` (String) Amazon Resource Name (ARN). Required for AWS Provider and MSK vendor. @@ -283,6 +325,7 @@ output "privatelink_endpoint_id" { * AZURE provider with EVENTHUB or CONFLUENT vendor. - `dns_sub_domain` (List of String) Sub-Domain name of Confluent cluster. These are typically your availability zones. Required for AWS Provider and CONFLUENT vendor. If your AWS CONFLUENT cluster doesn't use subdomains, you must set this to the empty array []. - `region` (String) The region of the Provider’s cluster. See [AZURE](https://www.mongodb.com/docs/atlas/reference/microsoft-azure/#stream-processing-instances) and [AWS](https://www.mongodb.com/docs/atlas/reference/amazon-aws/#stream-processing-instances) supported regions. When the vendor is `CONFLUENT`, this is the domain name of Confluent cluster. When the vendor is `MSK`, this is computed by the API from the provided `arn`. +- `service_attachment_uris` (List of String) List of GCP service attachment URIs for Confluent vendor. Required for GCP provider with CONFLUENT vendor. - `service_endpoint_id` (String) For AZURE EVENTHUB, this is the [namespace endpoint ID](https://learn.microsoft.com/en-us/rest/api/eventhub/namespaces/get). For AWS CONFLUENT cluster, this is the [VPC Endpoint service name](https://docs.confluent.io/cloud/current/networking/private-links/aws-privatelink.html). ### Read-Only diff --git a/examples/mongodbatlas_stream_privatelink_endpoint/gcp_confluent/README.md b/examples/mongodbatlas_stream_privatelink_endpoint/gcp_confluent/README.md new file mode 100644 index 0000000000..23fb266de2 --- /dev/null +++ b/examples/mongodbatlas_stream_privatelink_endpoint/gcp_confluent/README.md @@ -0,0 +1,13 @@ +# MongoDB Atlas Provider - GCP Confluent Privatelink for Atlas Streams + +This example shows how to use GCP Private Service Connect for Atlas Streams with Confluent Cloud. + +You must set the following variables: + +- `project_id`: Unique 24-hexadecimal digit string that identifies your project +- `atlas_client_id`: MongoDB Atlas Service Account Client ID +- `atlas_client_secret`: MongoDB Atlas Service Account Client Secret +- `gcp_region`: GCP region where your Confluent cluster is located +- `confluent_dns_domain`: DNS domain for your Confluent cluster +- `confluent_dns_subdomains`: List of DNS subdomains for your Confluent cluster +- `service_attachment_uris`: List of GCP service attachment URIs from your Confluent Cloud cluster diff --git a/examples/mongodbatlas_stream_privatelink_endpoint/gcp_confluent/main.tf b/examples/mongodbatlas_stream_privatelink_endpoint/gcp_confluent/main.tf new file mode 100644 index 0000000000..7eee2683da --- /dev/null +++ b/examples/mongodbatlas_stream_privatelink_endpoint/gcp_confluent/main.tf @@ -0,0 +1,35 @@ +resource "mongodbatlas_stream_privatelink_endpoint" "gcp_confluent" { + project_id = var.project_id + + provider_name = "GCP" + vendor = "CONFLUENT" + region = var.gcp_region + + dns_domain = var.confluent_dns_domain + dns_sub_domain = var.confluent_dns_subdomains + + service_attachment_uris = [ + "projects/my-project/regions/us-west1/serviceAttachments/confluent-attachment-1", + "projects/my-project/regions/us-west1/serviceAttachments/confluent-attachment-2" + ] +} + +data "mongodbatlas_stream_privatelink_endpoint" "gcp_confluent" { + project_id = var.project_id + id = mongodbatlas_stream_privatelink_endpoint.gcp_confluent.id +} + +output "privatelink_endpoint_id" { + description = "The ID of the MongoDB Atlas Stream Private Link Endpoint" + value = mongodbatlas_stream_privatelink_endpoint.gcp_confluent.id +} + +output "privatelink_endpoint_state" { + description = "The state of the MongoDB Atlas Stream Private Link Endpoint" + value = data.mongodbatlas_stream_privatelink_endpoint.gcp_confluent.state +} + +output "service_attachment_uris" { + description = "The GCP service attachment URIs used for the private link" + value = mongodbatlas_stream_privatelink_endpoint.gcp_confluent.service_attachment_uris +} diff --git a/examples/mongodbatlas_stream_privatelink_endpoint/gcp_confluent/variables.tf b/examples/mongodbatlas_stream_privatelink_endpoint/gcp_confluent/variables.tf new file mode 100644 index 0000000000..36ffb59245 --- /dev/null +++ b/examples/mongodbatlas_stream_privatelink_endpoint/gcp_confluent/variables.tf @@ -0,0 +1,22 @@ +variable "project_id" { + description = "The MongoDB Atlas project ID" + type = string +} + +variable "gcp_region" { + description = "The GCP region where resources will be created" + type = string + default = "us-west1" +} + +variable "confluent_dns_domain" { + description = "The DNS domain for the Confluent cluster" + type = string + default = "example.confluent.cloud" +} + +variable "confluent_dns_subdomains" { + description = "List of DNS subdomains for the Confluent cluster" + type = list(string) + default = ["subdomain1", "subdomain2"] +} diff --git a/examples/mongodbatlas_stream_privatelink_endpoint/gcp_confluent/versions.tf b/examples/mongodbatlas_stream_privatelink_endpoint/gcp_confluent/versions.tf new file mode 100644 index 0000000000..29ba160d79 --- /dev/null +++ b/examples/mongodbatlas_stream_privatelink_endpoint/gcp_confluent/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.0" + required_providers { + mongodbatlas = { + source = "mongodb/mongodbatlas" + version = "~> 2.1" + } + } +} diff --git a/internal/service/streamprivatelinkendpoint/model.go b/internal/service/streamprivatelinkendpoint/model.go index 26afe60736..fc0f699da9 100644 --- a/internal/service/streamprivatelinkendpoint/model.go +++ b/internal/service/streamprivatelinkendpoint/model.go @@ -14,6 +14,7 @@ const ( VendorConfluent = "CONFLUENT" VendorMSK = "MSK" VendorS3 = "S3" + ProviderGCP = "GCP" ) func NewTFModel(ctx context.Context, projectID string, apiResp *admin.StreamsPrivateLinkConnection) (*TFModel, diag.Diagnostics) { @@ -39,6 +40,12 @@ func NewTFModel(ctx context.Context, projectID string, apiResp *admin.StreamsPri } result.DnsSubDomain = subdomain + serviceAttachmentUris, diagsServiceAttachment := types.ListValueFrom(ctx, types.StringType, apiResp.GcpServiceAttachmentUris) + if diagsServiceAttachment.HasError() { + return nil, diagsServiceAttachment + } + result.ServiceAttachmentUris = serviceAttachmentUris + return result, nil } @@ -46,8 +53,15 @@ func NewAtlasReq(ctx context.Context, plan *TFModel) (*admin.StreamsPrivateLinkC diags := diag.Diagnostics{} if plan.Vendor.ValueString() == VendorConfluent { - if plan.ServiceEndpointId.IsNull() { - diags.AddError(fmt.Sprintf("service_endpoint_id is required for vendor %s", VendorConfluent), "") + // Validate that exactly one of service_endpoint_id or service_attachment_uris is provided + hasServiceEndpointID := !plan.ServiceEndpointId.IsNull() && plan.ServiceEndpointId.ValueString() != "" + hasServiceAttachmentUris := !plan.ServiceAttachmentUris.IsNull() && len(plan.ServiceAttachmentUris.Elements()) > 0 + + if !hasServiceEndpointID && !hasServiceAttachmentUris { + diags.AddError(fmt.Sprintf("Either service_endpoint_id or service_attachment_uris must be provided for vendor %s", VendorConfluent), "") + } + if hasServiceEndpointID && hasServiceAttachmentUris { + diags.AddError("Only one of service_endpoint_id or service_attachment_uris can be provided", "") } if plan.DnsDomain.IsNull() { diags.AddError(fmt.Sprintf("dns_domain is required for vendor %s", VendorConfluent), "") @@ -97,6 +111,16 @@ func NewAtlasReq(ctx context.Context, plan *TFModel) (*admin.StreamsPrivateLinkC } result.DnsSubDomain = &dnsSubdomains } + + if !plan.ServiceAttachmentUris.IsNull() { + var serviceAttachmentUris []string + diags := plan.ServiceAttachmentUris.ElementsAs(ctx, &serviceAttachmentUris, false) + if diags.HasError() { + return nil, diags + } + result.GcpServiceAttachmentUris = &serviceAttachmentUris + } + return result, nil } diff --git a/internal/service/streamprivatelinkendpoint/model_test.go b/internal/service/streamprivatelinkendpoint/model_test.go index c421eaff13..8cb15fe891 100644 --- a/internal/service/streamprivatelinkendpoint/model_test.go +++ b/internal/service/streamprivatelinkendpoint/model_test.go @@ -69,6 +69,7 @@ func TestStreamPrivatelinkEndpointSDKToTFModel(t *testing.T) { ProviderAccountId: types.StringValue(providerAccountID), Region: types.StringValue(region), ServiceEndpointId: types.StringValue(serviceEndpointID), + ServiceAttachmentUris: types.ListNull(types.StringType), State: types.StringValue(state), Vendor: types.StringValue(vendorConfluent), }, @@ -87,17 +88,18 @@ func TestStreamPrivatelinkEndpointSDKToTFModel(t *testing.T) { }, projectID: projectID, expectedTFModel: &streamprivatelinkendpoint.TFModel{ - Id: types.StringValue(id), - Arn: types.StringValue(arn), - DnsDomain: types.StringValue(dnsDomain), - DnsSubDomain: types.ListNull(types.StringType), - ProjectId: types.StringValue(projectID), - InterfaceEndpointId: types.StringValue(interfaceEndpointID), - Provider: types.StringValue(constant.AWS), - Region: types.StringValue(region), - ServiceEndpointId: types.StringValue(serviceEndpointID), - State: types.StringValue(state), - Vendor: types.StringValue(vendorConfluent), + Id: types.StringValue(id), + Arn: types.StringValue(arn), + DnsDomain: types.StringValue(dnsDomain), + DnsSubDomain: types.ListNull(types.StringType), + ProjectId: types.StringValue(projectID), + InterfaceEndpointId: types.StringValue(interfaceEndpointID), + Provider: types.StringValue(constant.AWS), + Region: types.StringValue(region), + ServiceEndpointId: types.StringValue(serviceEndpointID), + ServiceAttachmentUris: types.ListNull(types.StringType), + State: types.StringValue(state), + Vendor: types.StringValue(vendorConfluent), }, }, "SDK response without arn": { @@ -120,13 +122,14 @@ func TestStreamPrivatelinkEndpointSDKToTFModel(t *testing.T) { types.StringValue(dnsSubDomain), types.StringValue(dnsSubDomain), }), - ProjectId: types.StringValue(projectID), - InterfaceEndpointId: types.StringValue(interfaceEndpointID), - Provider: types.StringValue(constant.AWS), - Region: types.StringValue(region), - ServiceEndpointId: types.StringValue(serviceEndpointID), - State: types.StringValue(state), - Vendor: types.StringValue(vendorConfluent), + ProjectId: types.StringValue(projectID), + InterfaceEndpointId: types.StringValue(interfaceEndpointID), + Provider: types.StringValue(constant.AWS), + Region: types.StringValue(region), + ServiceEndpointId: types.StringValue(serviceEndpointID), + ServiceAttachmentUris: types.ListNull(types.StringType), + State: types.StringValue(state), + Vendor: types.StringValue(vendorConfluent), }, }, "SDK response with vendor S3": { @@ -138,20 +141,52 @@ func TestStreamPrivatelinkEndpointSDKToTFModel(t *testing.T) { }, projectID: projectID, expectedTFModel: &streamprivatelinkendpoint.TFModel{ - Id: types.StringValue(id), - Provider: types.StringValue(constant.AWS), - Vendor: types.StringValue(vendorS3), - Region: types.StringValue(region), - ProjectId: types.StringValue(projectID), - DnsSubDomain: types.ListNull(types.StringType), + Id: types.StringValue(id), + Provider: types.StringValue(constant.AWS), + Vendor: types.StringValue(vendorS3), + Region: types.StringValue(region), + ProjectId: types.StringValue(projectID), + DnsSubDomain: types.ListNull(types.StringType), + ServiceAttachmentUris: types.ListNull(types.StringType), + }, + }, + "SDK response with GCP Confluent": { + SDKResp: &admin.StreamsPrivateLinkConnection{ + Id: &id, + DnsDomain: &dnsDomain, + DnsSubDomain: conversion.Pointer([]string{dnsSubDomain, dnsSubDomain}), + GcpServiceAttachmentUris: conversion.Pointer([]string{"projects/test-project/regions/us-west1/serviceAttachments/test-attachment-1", "projects/test-project/regions/us-west1/serviceAttachments/test-attachment-2"}), + Provider: constant.GCP, + Region: ®ion, + State: &state, + Vendor: &vendorConfluent, + }, + projectID: projectID, + expectedTFModel: &streamprivatelinkendpoint.TFModel{ + Id: types.StringValue(id), + DnsDomain: types.StringValue(dnsDomain), + DnsSubDomain: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue(dnsSubDomain), + types.StringValue(dnsSubDomain), + }), + ServiceAttachmentUris: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("projects/test-project/regions/us-west1/serviceAttachments/test-attachment-1"), + types.StringValue("projects/test-project/regions/us-west1/serviceAttachments/test-attachment-2"), + }), + ProjectId: types.StringValue(projectID), + Provider: types.StringValue(constant.GCP), + Region: types.StringValue(region), + State: types.StringValue(state), + Vendor: types.StringValue(vendorConfluent), }, }, "Empty SDK response": { SDKResp: &admin.StreamsPrivateLinkConnection{}, expectedTFModel: &streamprivatelinkendpoint.TFModel{ - ProjectId: types.StringValue(""), - Provider: types.StringValue(""), - DnsSubDomain: types.ListNull(types.StringType), + ProjectId: types.StringValue(""), + Provider: types.StringValue(""), + DnsSubDomain: types.ListNull(types.StringType), + ServiceAttachmentUris: types.ListNull(types.StringType), }, }, } @@ -190,6 +225,7 @@ func TestStreamPrivatelinkEndpointTFModelToSDK(t *testing.T) { ProviderAccountId: types.StringValue(providerAccountID), Region: types.StringValue(region), ServiceEndpointId: types.StringValue(serviceEndpointID), + ServiceAttachmentUris: types.ListNull(types.StringType), State: types.StringValue(state), Vendor: types.StringValue(vendorConfluent), }, @@ -206,16 +242,17 @@ func TestStreamPrivatelinkEndpointTFModelToSDK(t *testing.T) { }, "TF state without dns subdomains": { tfModel: &streamprivatelinkendpoint.TFModel{ - Id: types.StringValue(id), - Arn: types.StringValue(arn), - DnsDomain: types.StringValue(dnsDomain), - ProjectId: types.StringValue(projectID), - InterfaceEndpointId: types.StringValue(interfaceEndpointID), - Provider: types.StringValue(constant.AWS), - Region: types.StringValue(region), - ServiceEndpointId: types.StringValue(serviceEndpointID), - State: types.StringValue(state), - Vendor: types.StringValue(vendorConfluent), + Id: types.StringValue(id), + Arn: types.StringValue(arn), + DnsDomain: types.StringValue(dnsDomain), + ProjectId: types.StringValue(projectID), + InterfaceEndpointId: types.StringValue(interfaceEndpointID), + Provider: types.StringValue(constant.AWS), + Region: types.StringValue(region), + ServiceEndpointId: types.StringValue(serviceEndpointID), + ServiceAttachmentUris: types.ListNull(types.StringType), + State: types.StringValue(state), + Vendor: types.StringValue(vendorConfluent), }, expectedSDKReq: &admin.StreamsPrivateLinkConnection{ Arn: &arn, @@ -242,6 +279,7 @@ func TestStreamPrivatelinkEndpointTFModelToSDK(t *testing.T) { ProviderAccountId: types.StringValue(providerAccountID), Region: types.StringValue(region), ServiceEndpointId: types.StringValue(serviceEndpointID), + ServiceAttachmentUris: types.ListNull(types.StringType), State: types.StringValue(state), Vendor: types.StringValue(vendorConfluent), }, @@ -257,11 +295,12 @@ func TestStreamPrivatelinkEndpointTFModelToSDK(t *testing.T) { }, "TF state with s3 vendor": { tfModel: &streamprivatelinkendpoint.TFModel{ - Id: types.StringValue(id), - Provider: types.StringValue(constant.AWS), - Vendor: types.StringValue(vendorS3), - Region: types.StringValue(region), - ServiceEndpointId: types.StringValue(serviceEndpointIDS3), + Id: types.StringValue(id), + Provider: types.StringValue(constant.AWS), + Vendor: types.StringValue(vendorS3), + Region: types.StringValue(region), + ServiceEndpointId: types.StringValue(serviceEndpointIDS3), + ServiceAttachmentUris: types.ListNull(types.StringType), }, expectedSDKReq: &admin.StreamsPrivateLinkConnection{ Provider: constant.AWS, @@ -270,6 +309,33 @@ func TestStreamPrivatelinkEndpointTFModelToSDK(t *testing.T) { ServiceEndpointId: &serviceEndpointIDS3, }, }, + "TF state with GCP Confluent": { + tfModel: &streamprivatelinkendpoint.TFModel{ + Id: types.StringValue(id), + DnsDomain: types.StringValue(dnsDomain), + DnsSubDomain: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue(dnsSubDomain), + types.StringValue(dnsSubDomain), + }), + ServiceAttachmentUris: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("projects/test-project/regions/us-west1/serviceAttachments/test-attachment-1"), + types.StringValue("projects/test-project/regions/us-west1/serviceAttachments/test-attachment-2"), + }), + Provider: types.StringValue(constant.GCP), + Region: types.StringValue(region), + State: types.StringValue(state), + Vendor: types.StringValue(vendorConfluent), + }, + expectedSDKReq: &admin.StreamsPrivateLinkConnection{ + DnsDomain: &dnsDomain, + DnsSubDomain: conversion.Pointer([]string{dnsSubDomain, dnsSubDomain}), + GcpServiceAttachmentUris: conversion.Pointer([]string{"projects/test-project/regions/us-west1/serviceAttachments/test-attachment-1", "projects/test-project/regions/us-west1/serviceAttachments/test-attachment-2"}), + Provider: constant.GCP, + Region: ®ion, + State: &state, + Vendor: &vendorConfluent, + }, + }, } for testName, tc := range testCases { @@ -282,3 +348,110 @@ func TestStreamPrivatelinkEndpointTFModelToSDK(t *testing.T) { }) } } + +func TestStreamPrivatelinkEndpointValidation(t *testing.T) { + testCases := map[string]struct { + tfModel *streamprivatelinkendpoint.TFModel + expectError bool + errorCount int + }{ + "GCP Confluent with all required fields": { + tfModel: &streamprivatelinkendpoint.TFModel{ + Provider: types.StringValue(constant.GCP), + Vendor: types.StringValue(streamprivatelinkendpoint.VendorConfluent), + ServiceAttachmentUris: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("projects/test-project/regions/us-west1/serviceAttachments/test-attachment")}), + ServiceEndpointId: types.StringNull(), + DnsDomain: types.StringValue("example.com"), + Region: types.StringValue("us-west1"), + }, + expectError: false, + errorCount: 0, + }, + "GCP Confluent missing service_attachment_uris": { + tfModel: &streamprivatelinkendpoint.TFModel{ + Provider: types.StringValue(constant.GCP), + Vendor: types.StringValue(streamprivatelinkendpoint.VendorConfluent), + ServiceEndpointId: types.StringNull(), + DnsDomain: types.StringValue("example.com"), + Region: types.StringValue("us-west1"), + }, + expectError: true, + errorCount: 1, + }, + "GCP Confluent missing dns_domain": { + tfModel: &streamprivatelinkendpoint.TFModel{ + Provider: types.StringValue(constant.GCP), + Vendor: types.StringValue(streamprivatelinkendpoint.VendorConfluent), + ServiceAttachmentUris: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("projects/test-project/regions/us-west1/serviceAttachments/test-attachment")}), + ServiceEndpointId: types.StringNull(), + DnsDomain: types.StringNull(), + Region: types.StringValue("us-west1"), + }, + expectError: true, + errorCount: 1, + }, + "GCP Confluent missing region": { + tfModel: &streamprivatelinkendpoint.TFModel{ + Provider: types.StringValue(constant.GCP), + Vendor: types.StringValue(streamprivatelinkendpoint.VendorConfluent), + ServiceAttachmentUris: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("projects/test-project/regions/us-west1/serviceAttachments/test-attachment")}), + ServiceEndpointId: types.StringNull(), + DnsDomain: types.StringValue("example.com"), + Region: types.StringNull(), + }, + expectError: true, + errorCount: 1, + }, + "AWS Confluent": { + tfModel: &streamprivatelinkendpoint.TFModel{ + Provider: types.StringValue(constant.AWS), + Vendor: types.StringValue(streamprivatelinkendpoint.VendorConfluent), + ServiceEndpointId: types.StringValue("vpce-12345"), + DnsDomain: types.StringValue("example.com"), + Region: types.StringValue("us-west-2"), + }, + expectError: false, + errorCount: 0, + }, + "Both service_endpoint_id and service_attachment_uris provided": { + tfModel: &streamprivatelinkendpoint.TFModel{ + Provider: types.StringValue(constant.GCP), + Vendor: types.StringValue(streamprivatelinkendpoint.VendorConfluent), + ServiceAttachmentUris: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("projects/test-project/regions/us-west1/serviceAttachments/test-attachment")}), + ServiceEndpointId: types.StringValue("vpce-12345"), + DnsDomain: types.StringValue("example.com"), + Region: types.StringValue("us-west1"), + }, + expectError: true, + errorCount: 1, + }, + "Neither service_endpoint_id nor service_attachment_uris provided": { + tfModel: &streamprivatelinkendpoint.TFModel{ + Provider: types.StringValue(constant.GCP), + Vendor: types.StringValue(streamprivatelinkendpoint.VendorConfluent), + ServiceEndpointId: types.StringNull(), + DnsDomain: types.StringValue("example.com"), + Region: types.StringValue("us-west1"), + }, + expectError: true, + errorCount: 1, + }, + } + + for testName, tc := range testCases { + t.Run(testName, func(t *testing.T) { + _, diags := streamprivatelinkendpoint.NewAtlasReq(t.Context(), tc.tfModel) + + if tc.expectError { + if !diags.HasError() { + t.Errorf("Expected validation errors but got none") + } + if len(diags.Errors()) != tc.errorCount { + t.Errorf("Expected %d validation errors but got %d", tc.errorCount, len(diags.Errors())) + } + } else if diags.HasError() { + t.Errorf("Expected no validation errors but got: %v", diags.Errors()) + } + }) + } +} diff --git a/internal/service/streamprivatelinkendpoint/resource_schema.go b/internal/service/streamprivatelinkendpoint/resource_schema.go index dee222a28c..5f417b641b 100644 --- a/internal/service/streamprivatelinkendpoint/resource_schema.go +++ b/internal/service/streamprivatelinkendpoint/resource_schema.go @@ -4,6 +4,8 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -49,7 +51,7 @@ func ResourceSchema(ctx context.Context) schema.Schema { }, "provider_name": schema.StringAttribute{ Required: true, - MarkdownDescription: "Provider where the endpoint is deployed. Valid values are AWS and AZURE.", + MarkdownDescription: "Provider where the endpoint is deployed. Valid values are AWS, AZURE, and GCP.", }, "region": schema.StringAttribute{ Optional: true, @@ -60,6 +62,14 @@ func ResourceSchema(ctx context.Context) schema.Schema { Optional: true, MarkdownDescription: "For AZURE EVENTHUB, this is the [namespace endpoint ID](https://learn.microsoft.com/en-us/rest/api/eventhub/namespaces/get). For AWS CONFLUENT cluster, this is the [VPC Endpoint service name](https://docs.confluent.io/cloud/current/networking/private-links/aws-privatelink.html).", }, + "service_attachment_uris": schema.ListAttribute{ + Optional: true, + MarkdownDescription: "List of GCP service attachment URIs for Confluent vendor. Required for GCP provider with CONFLUENT vendor.", + ElementType: types.StringType, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + }, "state": schema.StringAttribute{ Computed: true, MarkdownDescription: "Status of the connection.", @@ -70,7 +80,9 @@ func ResourceSchema(ctx context.Context) schema.Schema { * **AWS**: MSK, CONFLUENT, and S3 - * **Azure**: EVENTHUB and CONFLUENT`, + * **Azure**: EVENTHUB and CONFLUENT + + * **GCP**: CONFLUENT`, }, "arn": schema.StringAttribute{ Optional: true, @@ -92,6 +104,7 @@ type TFModel struct { ProviderAccountId types.String `tfsdk:"provider_account_id"` Region types.String `tfsdk:"region"` ServiceEndpointId types.String `tfsdk:"service_endpoint_id"` + ServiceAttachmentUris types.List `tfsdk:"service_attachment_uris"` State types.String `tfsdk:"state"` Vendor types.String `tfsdk:"vendor"` Arn types.String `tfsdk:"arn"` diff --git a/templates/data-sources/stream_privatelink_endpoint.md.tmpl b/templates/data-sources/stream_privatelink_endpoint.md.tmpl index a2808f93fb..22edeedc23 100644 --- a/templates/data-sources/stream_privatelink_endpoint.md.tmpl +++ b/templates/data-sources/stream_privatelink_endpoint.md.tmpl @@ -17,6 +17,9 @@ subcategory: "Streams" ### AWS S3 Privatelink {{ tffile (printf "examples/%s/s3/main.tf" .Name )}} +### GCP Confluent Privatelink +{{ tffile (printf "examples/%s/gcp_confluent/main.tf" .Name )}} + {{ .SchemaMarkdown | trimspace }} For more information see: [MongoDB Atlas API - Streams Privatelink](https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/operation/operation-createprivatelinkconnection) Documentation. diff --git a/templates/data-sources/stream_privatelink_endpoints.md.tmpl b/templates/data-sources/stream_privatelink_endpoints.md.tmpl index 9c8bd8cebb..45fbbcd395 100644 --- a/templates/data-sources/stream_privatelink_endpoints.md.tmpl +++ b/templates/data-sources/stream_privatelink_endpoints.md.tmpl @@ -17,6 +17,9 @@ subcategory: "Streams" ### AWS S3 Privatelink {{ tffile (printf "examples/mongodbatlas_stream_privatelink_endpoint/s3/main.tf" )}} +### GCP Confluent Privatelink +{{ tffile (printf "examples/mongodbatlas_stream_privatelink_endpoint/gcp_confluent/main.tf" )}} + {{ .SchemaMarkdown | trimspace }} For more information see: [MongoDB Atlas API - Streams Privatelink](https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/operation/operation-createprivatelinkconnection) Documentation. diff --git a/templates/resources/stream_privatelink_endpoint.md.tmpl b/templates/resources/stream_privatelink_endpoint.md.tmpl index 0905191b17..0c81af0dd1 100644 --- a/templates/resources/stream_privatelink_endpoint.md.tmpl +++ b/templates/resources/stream_privatelink_endpoint.md.tmpl @@ -17,11 +17,15 @@ subcategory: "Streams" ### AWS S3 Privatelink {{ tffile (printf "examples/%s/s3/main.tf" .Name )}} +### GCP Confluent Privatelink +{{ tffile (printf "examples/%s/gcp_confluent/main.tf" .Name )}} + ### Further Examples - [AWS Confluent PrivateLink](https://github.com/mongodb/terraform-provider-mongodbatlas/tree/v2.1.0/examples/mongodbatlas_stream_privatelink_endpoint/confluent_serverless) - [Confluent Dedicated Cluster](https://github.com/mongodb/terraform-provider-mongodbatlas/tree/v2.1.0/examples/mongodbatlas_stream_privatelink_endpoint/confluent_dedicated_cluster) - [AWS MSK PrivateLink](https://github.com/mongodb/terraform-provider-mongodbatlas/tree/v2.1.0/examples/mongodbatlas_stream_privatelink_endpoint/aws_msk_cluster) - [AWS S3 PrivateLink](https://github.com/mongodb/terraform-provider-mongodbatlas/tree/v2.1.0/examples/mongodbatlas_stream_privatelink_endpoint/s3) +- [GCP Confluent PrivateLink](https://github.com/mongodb/terraform-provider-mongodbatlas/tree/v2.1.0/examples/mongodbatlas_stream_privatelink_endpoint/gcp_confluent) - [Azure PrivateLink](https://github.com/mongodb/terraform-provider-mongodbatlas/tree/v2.1.0/examples/mongodbatlas_stream_privatelink_endpoint/azure) {{ .SchemaMarkdown | trimspace }}