Skip to content

Commit

Permalink
provider: private connection support on all clouds (#183)
Browse files Browse the repository at this point in the history
Previously, the private_endpoint_connection resource only supported AWS
private link connections. This commit updates the provider code to use
the new CC API Private Endpoint Connections methods. These cloud-neutral
methods will work for all cloud providers and cluster types, except
Serverless clusters on Azure as that configuration is not yet
supported.
  • Loading branch information
carloruiz authored Mar 13, 2024
1 parent 10ccf48 commit 1a3b84f
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 62 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Notes

- The `private_endpoint_connection` resource can now be used to create private
endpoint connections on every supported cloud-provider and cluster type,
except Serverless clusters on Azure as that configuration is not yet
available.

### Fixed

- Renamed example files to the correct name so they are automatically included
Expand Down
8 changes: 4 additions & 4 deletions docs/resources/private_endpoint_connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
page_title: "cockroach_private_endpoint_connection Resource - terraform-provider-cockroach"
subcategory: ""
description: |-
AWS PrivateLink Endpoint Connection.
Private Endpoint Connection.
---

# cockroach_private_endpoint_connection (Resource)

AWS PrivateLink Endpoint Connection.
Private Endpoint Connection.



Expand All @@ -18,13 +18,13 @@ AWS PrivateLink Endpoint Connection.
### Required

- `cluster_id` (String)
- `endpoint_id` (String) Client side ID of the PrivateLink connection.
- `endpoint_id` (String) Client side ID of the Private Endpoint Connection.

### Read-Only

- `cloud_provider` (String) Cloud provider associated with this connection.
- `id` (String) Used with `terraform import`. Format is "<cluster ID>:<endpoint ID>".
- `region_name` (String) Cloud provider region code associated with this connection.
- `service_id` (String) Server side ID of the PrivateLink connection.
- `service_id` (String) Server side ID of the Private Endpoint Connection.


Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
variable "cluster_id" {
type = string
type = string
description = "the id for the CockroachDB Cloud cluster"
}

resource "cockroach_private_endpoint_connection" "cockroach" {
cluster_id = var.cluster_id
endpoint_id = "endpoint id assigned by consumer AWS"
cloud_provider = "AWS"
region_name = "AWS region in which the endpoint was created"
endpoint_id = "the endpoint id assigned by cloud provider to the client-side of the connection"
}
59 changes: 28 additions & 31 deletions internal/provider/private_endpoint_connection_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func (r *privateEndpointConnectionResource) Schema(
_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse,
) {
resp.Schema = schema.Schema{
MarkdownDescription: "AWS PrivateLink Endpoint Connection.",
MarkdownDescription: "Private Endpoint Connection.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
Expand Down Expand Up @@ -76,14 +76,14 @@ func (r *privateEndpointConnectionResource) Schema(
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Description: "Client side ID of the PrivateLink connection.",
Description: "Client side ID of the Private Endpoint Connection.",
},
"service_id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
Description: "Server side ID of the PrivateLink connection.",
Description: "Server side ID of the Private Endpoint Connection.",
},
"cluster_id": schema.StringAttribute{
Required: true,
Expand Down Expand Up @@ -129,7 +129,8 @@ func (r *privateEndpointConnectionResource) Create(
return
}

cluster, _, err := r.provider.service.GetCluster(ctx, plan.ClusterID.ValueString())
svc := r.provider.service
cluster, _, err := svc.GetCluster(ctx, plan.ClusterID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error getting cluster",
Expand All @@ -138,30 +139,22 @@ func (r *privateEndpointConnectionResource) Create(
return
}

if cluster.CloudProvider != client.CLOUDPROVIDERTYPE_AWS {
resp.Diagnostics.AddError(
"Incompatible cluster cloud provider",
"Private endpoint services are only available for AWS clusters",
)
return
}

connectionStateRequest := client.SetAwsEndpointConnectionStateRequest{
Status: client.SETAWSENDPOINTCONNECTIONSTATUSTYPE_AVAILABLE,
addRequest := client.AddPrivateEndpointConnectionRequest{
EndpointId: plan.EndpointID.ValueString(),
}

_, _, err = r.provider.service.SetAwsEndpointConnectionState(ctx, plan.ClusterID.ValueString(), plan.EndpointID.ValueString(), &connectionStateRequest)
_, _, err = svc.AddPrivateEndpointConnection(ctx, cluster.Id, &addRequest)
if err != nil {
resp.Diagnostics.AddError(
"Error establishing AWS Endpoint Connection",
fmt.Sprintf("Could not establish AWS Endpoint Connection: %s", formatAPIErrorMessage(err)),
"Error establishing Private Endpoint Connection",
fmt.Sprintf("Could not establish Private Endpoint Connection: %s", formatAPIErrorMessage(err)),
)
return
}

var connection client.AwsEndpointConnection
var connection client.PrivateEndpointConnection
err = sdk_resource.RetryContext(ctx, endpointConnectionCreateTimeout,
waitForEndpointConnectionCreatedFunc(ctx, cluster.Id, plan.EndpointID.ValueString(), r.provider.service, &connection))
waitForEndpointConnectionCreatedFunc(ctx, cluster.Id, plan.EndpointID.ValueString(), svc, &connection))
if err != nil {
resp.Diagnostics.AddError(
"Error accepting private endpoint connection",
Expand Down Expand Up @@ -192,7 +185,7 @@ func (r *privateEndpointConnectionResource) Read(
return
}

connections, _, err := r.provider.service.ListAwsEndpointConnections(ctx, state.ClusterID.ValueString())
connections, _, err := r.provider.service.ListPrivateEndpointConnections(ctx, state.ClusterID.ValueString())
if err != nil {
diags.AddError("Unable to get endpoint connection status",
fmt.Sprintf("Unexpected error retrieving endpoint status: %s", formatAPIErrorMessage(err)))
Expand All @@ -212,14 +205,14 @@ func (r *privateEndpointConnectionResource) Read(
}

func loadEndpointConnectionIntoTerraformState(
apiConnection *client.AwsEndpointConnection, state *PrivateEndpointConnection,
apiConnection *client.PrivateEndpointConnection, state *PrivateEndpointConnection,
) {
state.EndpointID = types.StringValue(apiConnection.GetEndpointId())
state.ID = types.StringValue(fmt.Sprintf(
privateEndpointConnectionIDFmt,
state.ClusterID.ValueString(),
apiConnection.GetEndpointId()))
state.ServiceID = types.StringValue(apiConnection.GetServiceId())
state.ServiceID = types.StringValue(apiConnection.GetEndpointServiceId())
state.CloudProvider = types.StringValue(string(apiConnection.GetCloudProvider()))
state.RegionName = types.StringValue(apiConnection.GetRegionName())
}
Expand All @@ -240,13 +233,11 @@ func (r *privateEndpointConnectionResource) Delete(
return
}

_, httpResp, err := r.provider.service.SetAwsEndpointConnectionState(
httpResp, err := r.provider.service.DeletePrivateEndpointConnection(
ctx,
state.ClusterID.ValueString(),
state.EndpointID.ValueString(),
&client.SetAwsEndpointConnectionStateRequest{
Status: client.SETAWSENDPOINTCONNECTIONSTATUSTYPE_REJECTED,
})
)
if err != nil && httpResp != nil && httpResp.StatusCode != http.StatusNotFound {
diags.AddError("Couldn't delete connection",
fmt.Sprintf("Unexpected error occurred while setting connection status: %s", formatAPIErrorMessage(err)))
Expand Down Expand Up @@ -284,10 +275,10 @@ func waitForEndpointConnectionCreatedFunc(
ctx context.Context,
clusterID, endpointID string,
cl client.Service,
connection *client.AwsEndpointConnection,
connection *client.PrivateEndpointConnection,
) sdk_resource.RetryFunc {
return func() *sdk_resource.RetryError {
connections, httpResp, err := cl.ListAwsEndpointConnections(ctx, clusterID)
connections, httpResp, err := cl.ListPrivateEndpointConnections(ctx, clusterID)
if err != nil {
if httpResp != nil && httpResp.StatusCode < http.StatusInternalServerError {
return sdk_resource.NonRetryableError(fmt.Errorf("error getting endpoint connections: %s", formatAPIErrorMessage(err)))
Expand All @@ -299,10 +290,16 @@ func waitForEndpointConnectionCreatedFunc(
for _, *connection = range connections.GetConnections() {
if connection.GetEndpointId() == endpointID {
switch status := connection.GetStatus(); status {
case client.AWSENDPOINTCONNECTIONSTATUSTYPE_AVAILABLE:
case client.PRIVATEENDPOINTCONNECTIONSTATUS_AVAILABLE:
return nil
case client.AWSENDPOINTCONNECTIONSTATUSTYPE_PENDING,
client.AWSENDPOINTCONNECTIONSTATUSTYPE_PENDING_ACCEPTANCE:
case client.PRIVATEENDPOINTCONNECTIONSTATUS_PENDING,
client.PRIVATEENDPOINTCONNECTIONSTATUS_PENDING_ACCEPTANCE,
client.PRIVATEENDPOINTCONNECTIONSTATUS_REJECTED:
// Note: A REJECTED state means the user previously called
// DeletePrivateEndpointConnection() on an existing
// connection. A user can re-attach a rejected connection
// by calling AddPrivateEndpointConnection() with the same
// endpointId.
return sdk_resource.RetryableError(fmt.Errorf("endpoint connection is not ready yet"))
default:
return sdk_resource.NonRetryableError(fmt.Errorf("endpoint connection failed with state: %s", status))
Expand Down
38 changes: 15 additions & 23 deletions internal/provider/private_endpoint_connection_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func TestAccServerlessPrivateEndpointConnectionResource(t *testing.T) {
}

func TestIntegrationPrivateEndpointConnectionResource(t *testing.T) {
clusterName := fmt.Sprintf("aws-connection-%s", GenerateRandomString(5))
clusterName := fmt.Sprintf("private-connection-%s", GenerateRandomString(5))
clusterID := uuid.Nil.String()
endpointID := "endpoint-id"
if os.Getenv(CockroachAPIKey) == "" {
Expand All @@ -70,15 +70,15 @@ func TestIntegrationPrivateEndpointConnectionResource(t *testing.T) {
},
},
}
connection := client.AwsEndpointConnection{
RegionName: "us-east-1",
CloudProvider: "AWS",
Status: client.AWSENDPOINTCONNECTIONSTATUSTYPE_AVAILABLE,
EndpointId: endpointID,
ServiceId: "service-id",
connection := client.PrivateEndpointConnection{
RegionName: &services.Services[0].RegionName,
CloudProvider: "AWS",
Status: client.PRIVATEENDPOINTCONNECTIONSTATUS_AVAILABLE,
EndpointId: endpointID,
EndpointServiceId: "service-id",
}
connections := &client.AwsEndpointConnections{
Connections: []client.AwsEndpointConnection{connection},
connections := &client.PrivateEndpointConnections{
Connections: []client.PrivateEndpointConnection{connection},
}

zeroSpendLimit := int32(0)
Expand Down Expand Up @@ -151,27 +151,19 @@ func TestIntegrationPrivateEndpointConnectionResource(t *testing.T) {
s.EXPECT().ListPrivateEndpointServices(gomock.Any(), clusterID).
Return(services, nil, nil).
Times(2)
available := client.SETAWSENDPOINTCONNECTIONSTATUSTYPE_AVAILABLE
s.EXPECT().SetAwsEndpointConnectionState(
s.EXPECT().AddPrivateEndpointConnection(
gomock.Any(),
clusterID,
endpointID,
&client.SetAwsEndpointConnectionStateRequest{
Status: available,
}).
&client.AddPrivateEndpointConnectionRequest{EndpointId: endpointID}).
Return(&connection, nil, nil)
s.EXPECT().ListAwsEndpointConnections(gomock.Any(), clusterID).
s.EXPECT().ListPrivateEndpointConnections(gomock.Any(), clusterID).
Return(connections, nil, nil).
Times(3)
rejected := client.SETAWSENDPOINTCONNECTIONSTATUSTYPE_REJECTED
s.EXPECT().SetAwsEndpointConnectionState(
s.EXPECT().DeletePrivateEndpointConnection(
gomock.Any(),
clusterID,
endpointID,
&client.SetAwsEndpointConnectionStateRequest{
Status: rejected,
}).
Return(&connection, nil, nil)
endpointID).
Return(nil, nil)
s.EXPECT().DeleteCluster(gomock.Any(), clusterID)

testPrivateEndpointConnectionResource(
Expand Down

0 comments on commit 1a3b84f

Please sign in to comment.