Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 34 additions & 6 deletions acceptance_tests/private_endpoint_acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,36 @@ package acceptance_tests

import (
"fmt"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

// TestAccPrivateEndpointServiceDisable tests enabling and disabling private endpoint service for a cluster
// TestAccPrivateEndpointServiceEnableDisable tests enabling, reading, and disabling private endpoint service for a cluster.
func TestAccPrivateEndpointServiceEnableDisable(t *testing.T) {
resourceName := "disable_private_endpoint_service"
resourceName := randomStringWithPrefix("tf_acc_private_endpoint_service_")
dataSourceName := randomStringWithPrefix("tf_acc_private_endpoints_ds_")
resourceReference := "couchbase-capella_private_endpoint_service." + resourceName
resource.ParallelTest(t, resource.TestCase{
dataSourceReference := "data.couchbase-capella_private_endpoints." + dataSourceName

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: globalProtoV6ProviderFactory,
Steps: []resource.TestStep{
// First enable the service
{
Config: testAccPrivateEndpointServiceEnableConfig(resourceName, true),
Config: testAccPrivateEndpointsDataSourceNoEndpointConfig(resourceName, dataSourceName),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(resourceReference, "organization_id", globalOrgId),
resource.TestCheckResourceAttr(resourceReference, "project_id", globalProjectId),
resource.TestCheckResourceAttr(resourceReference, "cluster_id", globalClusterId),
resource.TestCheckResourceAttr(resourceReference, "enabled", "true"),
resource.TestCheckResourceAttr(dataSourceReference, "organization_id", globalOrgId),
resource.TestCheckResourceAttr(dataSourceReference, "project_id", globalProjectId),
resource.TestCheckResourceAttr(dataSourceReference, "cluster_id", globalClusterId),
resource.TestMatchResourceAttr(dataSourceReference, "private_endpoint_dns", regexp.MustCompile(`^private-endpoint\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$`)),
resource.TestCheckResourceAttr(dataSourceReference, "data.#", "0"),
),
},
// Then disable it
{
Config: testAccPrivateEndpointServiceEnableConfig(resourceName, false),
Check: resource.ComposeAggregateTestCheckFunc(
Expand Down Expand Up @@ -52,3 +59,24 @@ func testAccPrivateEndpointServiceEnableConfig(resourceName string, enabled bool
}
`, globalProviderBlock, resourceName, globalOrgId, globalProjectId, globalClusterId, enabled)
}

func testAccPrivateEndpointsDataSourceNoEndpointConfig(serviceResourceName, dataSourceName string) string {
return fmt.Sprintf(`
%[1]s

resource "couchbase-capella_private_endpoint_service" "%[2]s" {
organization_id = "%[4]s"
project_id = "%[5]s"
cluster_id = "%[6]s"
enabled = true
}

data "couchbase-capella_private_endpoints" "%[3]s" {
organization_id = "%[4]s"
project_id = "%[5]s"
cluster_id = "%[6]s"

depends_on = [couchbase-capella_private_endpoint_service.%[2]s]
}
`, globalProviderBlock, serviceResourceName, dataSourceName, globalOrgId, globalProjectId, globalClusterId)
}
2 changes: 2 additions & 0 deletions docs/data-sources/private_endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ data "couchbase-capella_private_endpoints" "list_endpoints" {
### Read-Only

- `data` (Attributes List) (see [below for nested schema](#nestedatt--data))
- `private_endpoint_dns` (String)

<a id="nestedatt--data"></a>
### Nested Schema for `data`
Expand All @@ -43,4 +44,5 @@ Read-Only:
- `id` (String)
- `organization_id` (String) The GUID4 ID of the organization.
- `project_id` (String) The GUID4 ID of the project.
- `service_name` (String)
- `status` (String)
1 change: 1 addition & 0 deletions docs/resources/private_endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ resource "couchbase-capella_private_endpoints" "accept_endpoint" {

### Read-Only

- `private_endpoint_dns` (String)
- `service_name` (String)
- `status` (String)

Expand Down
3 changes: 2 additions & 1 deletion internal/api/private_endpoint_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ package api
// GetPrivateEndpointServiceStatusResponse is the response received from the Capella V4 Public API
// when getting private endpoint service status.
type GetPrivateEndpointServiceStatusResponse struct {
Enabled bool `json:"enabled"`
Enabled bool `json:"enabled"`
PrivateDns string `json:"privateDns"`
}
3 changes: 2 additions & 1 deletion internal/api/private_endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ type GetPrivateEndpointResponse struct {

// GetPrivateEndpointsResponse is a list of private endpoints.
type GetPrivateEndpointsResponse struct {
Endpoints []GetPrivateEndpointResponse `json:"endpoints"`
PrivateEndpointDNS string `json:"privateEndpointDNS"`
Endpoints []GetPrivateEndpointResponse `json:"endpoints"`
}
37 changes: 37 additions & 0 deletions internal/datasources/private_endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,20 @@ func (p *PrivateEndpoints) Read(ctx context.Context, req datasource.ReadRequest,
return
}

privateEndpointDNS := privateEndpointsResp.PrivateEndpointDNS
if privateEndpointDNS == "" {
privateEndpointDNS, err = p.getPrivateEndpointDNS(ctx, organizationId, projectId, clusterId)
if err != nil {
resp.Diagnostics.AddError(
"Error Reading Capella Private Endpoint DNS",
"Could not read private endpoint DNS in cluster "+state.ClusterId.String()+": "+api.ParseError(err),
)
return
}
}

state.PrivateEndpointDNS = types.StringValue(privateEndpointDNS)

for _, e := range privateEndpointsResp.Endpoints {
endpointData := providerschema.PrivateEndpointData{}
endpointData.Id = types.StringValue(e.Id)
Expand All @@ -106,6 +120,29 @@ func (p *PrivateEndpoints) Read(ctx context.Context, req datasource.ReadRequest,
}
}

func (p *PrivateEndpoints) getPrivateEndpointDNS(ctx context.Context, organizationId, projectId, clusterId string) (string, error) {
url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/privateEndpointService", p.HostURL, organizationId, projectId, clusterId)
cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK}
response, err := p.ClientV1.ExecuteWithRetry(
ctx,
cfg,
nil,
p.Token,
nil,
)
if err != nil {
return "", err
}

privateEndpointServiceStatus := api.GetPrivateEndpointServiceStatusResponse{}
err = json.Unmarshal(response.Body, &privateEndpointServiceStatus)
if err != nil {
return "", err
}

return privateEndpointServiceStatus.PrivateDns, nil
}

// Configure adds the provider configured client to the private endpoint data source.
func (p *PrivateEndpoints) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
Expand Down
2 changes: 2 additions & 0 deletions internal/datasources/private_endpoints_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func PrivateEndpointsSchema() schema.Schema {
capellaschema.AddAttr(attrs, "organization_id", privateEndpointsBuilder, requiredString())
capellaschema.AddAttr(attrs, "project_id", privateEndpointsBuilder, requiredString())
capellaschema.AddAttr(attrs, "cluster_id", privateEndpointsBuilder, requiredString())
capellaschema.AddAttr(attrs, "private_endpoint_dns", privateEndpointsBuilder, computedString(), "GetPrivateEndpointsResponse")

dataAttrs := make(map[string]schema.Attribute)
capellaschema.AddAttr(dataAttrs, "id", privateEndpointsBuilder, computedString())
Expand All @@ -23,6 +24,7 @@ func PrivateEndpointsSchema() schema.Schema {
capellaschema.AddAttr(dataAttrs, "cluster_id", privateEndpointsBuilder, computedString())
capellaschema.AddAttr(dataAttrs, "cloud_provider", privateEndpointsBuilder, computedString())
capellaschema.AddAttr(dataAttrs, "status", privateEndpointsBuilder, computedString())
capellaschema.AddAttr(dataAttrs, "service_name", privateEndpointsBuilder, computedString())

capellaschema.AddAttr(attrs, "data", privateEndpointsBuilder, &schema.ListNestedAttribute{
Computed: true,
Expand Down
62 changes: 50 additions & 12 deletions internal/resources/private_endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,31 +276,38 @@ func initializePrivateEndpointPlan(plan providerschema.PrivateEndpoint) provider
if plan.Status.IsNull() || plan.Status.IsUnknown() {
plan.Status = types.StringNull()
}
if plan.ServiceName.IsNull() || plan.ServiceName.IsUnknown() {
plan.ServiceName = types.StringNull()
}
if plan.PrivateEndpointDNS.IsNull() || plan.PrivateEndpointDNS.IsUnknown() {
plan.PrivateEndpointDNS = types.StringNull()
}
return plan
}

// getPrivateEndpointState morphs private endpoint status to terraform schema.
func (p *PrivateEndpoint) getPrivateEndpointState(ctx context.Context, organizationId, projectId, clusterId, endpointId string) (*providerschema.PrivateEndpoint, error) {
status, serviceName, err := p.getPrivateEndpointStatus(ctx, organizationId, projectId, clusterId, endpointId)
status, serviceName, privateEndpointDNS, err := p.getPrivateEndpointStatus(ctx, organizationId, projectId, clusterId, endpointId)
if err != nil {
return nil, err
}

state := providerschema.PrivateEndpoint{
EndpointId: types.StringValue(endpointId),
Status: types.StringValue(status),
ClusterId: types.StringValue(clusterId),
ProjectId: types.StringValue(projectId),
OrganizationId: types.StringValue(organizationId),
ServiceName: types.StringValue(serviceName),
EndpointId: types.StringValue(endpointId),
Status: types.StringValue(status),
ClusterId: types.StringValue(clusterId),
ProjectId: types.StringValue(projectId),
OrganizationId: types.StringValue(organizationId),
ServiceName: types.StringValue(serviceName),
PrivateEndpointDNS: types.StringValue(privateEndpointDNS),
}

return &state, nil
}

// There is currently no V4 endpoint to get a single private endpoint. We have to loop through the entire list to find
// the desired private endpoint.
func (p *PrivateEndpoint) getPrivateEndpointStatus(ctx context.Context, organizationId, projectId, clusterId, endpointId string) (string, string, error) {
func (p *PrivateEndpoint) getPrivateEndpointStatus(ctx context.Context, organizationId, projectId, clusterId, endpointId string) (string, string, string, error) {
url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/privateEndpointService/endpoints", p.HostURL, organizationId, projectId, clusterId)
cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK}
response, err := p.ClientV1.ExecuteWithRetry(
Expand All @@ -311,20 +318,51 @@ func (p *PrivateEndpoint) getPrivateEndpointStatus(ctx context.Context, organiza
nil,
)
if err != nil {
return "", "", err
return "", "", "", err
}

privateEndpointsResp := api.GetPrivateEndpointsResponse{}
err = json.Unmarshal(response.Body, &privateEndpointsResp)
if err != nil {
return "", "", err
return "", "", "", err
}

for _, e := range privateEndpointsResp.Endpoints {
if e.Id == endpointId {
return e.Status, e.ServiceName, nil
privateEndpointDNS := privateEndpointsResp.PrivateEndpointDNS
if privateEndpointDNS == "" {
privateEndpointDNS, err = p.getPrivateEndpointDNS(ctx, organizationId, projectId, clusterId)
if err != nil {
return "", "", "", err
}
}

return e.Status, e.ServiceName, privateEndpointDNS, nil
}
}

return "", "", errors.ErrNotFound
return "", "", "", errors.ErrNotFound
}

func (p *PrivateEndpoint) getPrivateEndpointDNS(ctx context.Context, organizationId, projectId, clusterId string) (string, error) {
url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/privateEndpointService", p.HostURL, organizationId, projectId, clusterId)
cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK}
response, err := p.ClientV1.ExecuteWithRetry(
ctx,
cfg,
nil,
p.Token,
nil,
)
if err != nil {
return "", err
}

privateEndpointServiceStatus := api.GetPrivateEndpointServiceStatusResponse{}
err = json.Unmarshal(response.Body, &privateEndpointServiceStatus)
if err != nil {
return "", err
}

return privateEndpointServiceStatus.PrivateDns, nil
}
1 change: 1 addition & 0 deletions internal/resources/private_endpoints_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func PrivateEndpointsSchema() schema.Schema {
capellaschema.AddAttr(attrs, "endpoint_id", privateEndpointsBuilder, stringAttribute([]string{required, requiresReplace}))
capellaschema.AddAttr(attrs, "status", privateEndpointsBuilder, stringAttribute([]string{computed}))
capellaschema.AddAttr(attrs, "service_name", privateEndpointsBuilder, stringAttribute([]string{computed}))
capellaschema.AddAttr(attrs, "private_endpoint_dns", privateEndpointsBuilder, stringAttribute([]string{computed}), "GetPrivateEndpointsResponse")

return schema.Schema{
MarkdownDescription: "This resource allows you to manage private endpoints for an operational cluster. Private endpoints allow you to securely connect your Cloud Service Provider's private network (VPC/VNET) to your operational cluster without exposing traffic to the public internet.",
Expand Down
12 changes: 8 additions & 4 deletions internal/schema/private_endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,18 @@ type PrivateEndpoint struct {

// ServiceName is the name of the private endpoint service.
ServiceName types.String `tfsdk:"service_name"`

// PrivateEndpointDNS is the DNS name used to connect to the cluster through private endpoints.
PrivateEndpointDNS types.String `tfsdk:"private_endpoint_dns"`
}

// PrivateEndpoints defines a structure used by the LIST endpoint for private endpoints.
type PrivateEndpoints struct {
ClusterId types.String `tfsdk:"cluster_id"`
ProjectId types.String `tfsdk:"project_id"`
OrganizationId types.String `tfsdk:"organization_id"`
Data []PrivateEndpointData `tfsdk:"data"`
ClusterId types.String `tfsdk:"cluster_id"`
ProjectId types.String `tfsdk:"project_id"`
OrganizationId types.String `tfsdk:"organization_id"`
PrivateEndpointDNS types.String `tfsdk:"private_endpoint_dns"`
Data []PrivateEndpointData `tfsdk:"data"`
}

// PrivateEndpointData defines a single private endpoint.
Expand Down
Loading