Skip to content

Commit

Permalink
INTMDB-465 - PrivateLink Endpoint Service & Connection Strings (#943)
Browse files Browse the repository at this point in the history
* INTMDB-384 Fix PrivateLink test flakiness (#895)

* Using different endpointID values for the endpoint service adl tests

* Removed parallelization with private link endpoint test which had overlap with others

* Reverted changes to the endpointID

* Waiting for Clusters to be idle on pes create

* Formatted and added Deleted as a target status

* Removed unintended changes

* Updated Delete to use TimeoutDelete

* Utilizing error when ClusterListAdvancedRefreshFunc fails

* Updated RefreshFunc to just return PENDING if cluster is not IDLE

* Updated StateChanegConf for endpoints vs. cluster status

* Fixed Pending/Target status' for PrivateEndpointServiceLinkDelete

* Added INITIATING back to list of Pending statuses on delete

* Added serverless instance list refresh func & utlizing it on serverless endpoint service

* Removed unintentional changes to tests and using a proper error message string

* Fixed error message wording to reference proper resource

* Removed extra quote on serverless instance

* WaitForStateContext inline with conditional

* Added documentation on completion condition for endponit service creation & deletion
  • Loading branch information
evertsd authored Dec 1, 2022
1 parent 06baa82 commit 75f3310
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 60 deletions.
41 changes: 38 additions & 3 deletions mongodbatlas/resource_mongodbatlas_advanced_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const (
errorClusterAdvancedSetting = "error setting `%s` for MongoDB ClusterAdvanced (%s): %s"
errorAdvancedClusterAdvancedConfUpdate = "error updating Advanced Configuration Option form MongoDB Cluster (%s): %s"
errorAdvancedClusterAdvancedConfRead = "error reading Advanced Configuration Option form MongoDB Cluster (%s): %s"
errorAdvancedClusterListStatus = "error awaiting MongoDB ClusterAdvanced List IDLE: %s"
)

var upgradeRequestCtxKey acCtxKey = "upgradeRequest"
Expand Down Expand Up @@ -949,7 +950,7 @@ func flattenAdvancedReplicationSpecs(ctx context.Context, apiObjects []*matlas.A

var tfMapObject map[string]interface{}

if len(tfMapObjects) > 0 {
if len(tfMapObjects) > i {
tfMapObject = tfMapObjects[i].(map[string]interface{})
}

Expand Down Expand Up @@ -1011,7 +1012,7 @@ func flattenAdvancedReplicationSpecRegionConfigs(ctx context.Context, apiObjects
continue
}

if len(tfMapObjects) > 0 {
if len(tfMapObjects) > i {
tfMapObject := tfMapObjects[i].(map[string]interface{})
tfList = append(tfList, flattenAdvancedReplicationSpecRegionConfig(apiObject, tfMapObject))
} else {
Expand Down Expand Up @@ -1106,7 +1107,9 @@ func resourceClusterAdvancedRefreshFunc(ctx context.Context, name, projectID str

if err != nil && c == nil && resp == nil {
return nil, "", err
} else if err != nil {
}

if err != nil {
if resp.StatusCode == 404 {
return "", "DELETED", nil
}
Expand All @@ -1124,6 +1127,38 @@ func resourceClusterAdvancedRefreshFunc(ctx context.Context, name, projectID str
}
}

func resourceClusterListAdvancedRefreshFunc(ctx context.Context, projectID string, client *matlas.Client) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
clusters, resp, err := client.AdvancedClusters.List(ctx, projectID, nil)

if err != nil && strings.Contains(err.Error(), "reset by peer") {
return nil, "REPEATING", nil
}

if err != nil && clusters == nil && resp == nil {
return nil, "", err
}

if err != nil {
if resp.StatusCode == 404 {
return "", "DELETED", nil
}
if resp.StatusCode == 503 {
return "", "PENDING", nil
}
return nil, "", err
}

for i := range clusters.Results {
if clusters.Results[i].StateName != "IDLE" {
return clusters.Results[i], "PENDING", nil
}
}

return clusters, "IDLE", nil
}
}

func replicationSpecsHashSet(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@ import (
"fmt"
"log"
"net/http"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
matlas "go.mongodb.org/atlas/mongodbatlas"
)

const (
Expand Down Expand Up @@ -94,9 +92,9 @@ func resourceMongoDBAtlasPrivateEndpointRegionalModeUpdate(ctx context.Context,
log.Println("[INFO] Waiting for MongoDB Clusters' Private Endpoints to be updated")

stateConf := &resource.StateChangeConf{
Pending: []string{"PENDING", "REPEATING"},
Target: []string{"APPLIED"},
Refresh: resourcePrivateEndpointRegionalModeRefreshFunc(ctx, conn, projectID),
Pending: []string{"REPEATING", "PENDING"},
Target: []string{"IDLE", "DELETED"},
Refresh: resourceClusterListAdvancedRefreshFunc(ctx, projectID, conn),
Timeout: 1 * time.Hour,
MinTimeout: 5 * time.Second,
Delay: 3 * time.Second,
Expand Down Expand Up @@ -143,48 +141,3 @@ func resourceMongoDBAtlasPrivateEndpointRegionalModeImportState(ctx context.Cont

return []*schema.ResourceData{d}, nil
}

func resourcePrivateEndpointRegionalModeRefreshFunc(ctx context.Context, client *matlas.Client, projectID string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
clusters, resp, err := client.Clusters.List(ctx, projectID, nil)

if err != nil {
// For our purposes, no clusters is equivalent to all changes having been APPLIED
if resp != nil && resp.StatusCode == http.StatusNotFound {
return "", "APPLIED", nil
}

return nil, "REPEATING", err
}

for i := range clusters {
if clusters[i].StateName == "UPDATING" {
return clusters, "PENDING", nil
}

s, resp, err := client.Clusters.Status(ctx, projectID, clusters[i].Name)

if err != nil && strings.Contains(err.Error(), "reset by peer") {
return nil, "REPEATING", nil
}

if err != nil {
if resp.StatusCode == 404 {
// The cluster no longer exists, consider this equivalent to status APPLIED
continue
}
if resp.StatusCode == 503 {
return "", "PENDING", nil
}
return nil, "REPEATING", err
}

if s.ChangeStatus == matlas.ChangeStatusPending {
return clusters, "PENDING", nil
}
}

// If all clusters were properly read, and none are PENDING, all changes have been APPLIED.
return clusters, "APPLIED", nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,20 @@ func resourceMongoDBAtlasPrivateEndpointServiceLinkCreate(ctx context.Context, d
return diag.FromErr(fmt.Errorf(errorServiceEndpointAdd, endpointServiceID, privateLinkID, err))
}

clusterConf := &resource.StateChangeConf{
Pending: []string{"REPEATING", "PENDING"},
Target: []string{"IDLE", "DELETED"},
Refresh: resourceClusterListAdvancedRefreshFunc(ctx, projectID, conn),
Timeout: d.Timeout(schema.TimeoutCreate),
MinTimeout: 5 * time.Second,
Delay: 5 * time.Minute,
}

if _, err = clusterConf.WaitForStateContext(ctx); err != nil {
// error awaiting advanced clusters IDLE should not result in failure to apply changes to this resource
log.Printf(errorAdvancedClusterListStatus, err)
}

d.SetId(encodeStateID(map[string]string{
"project_id": projectID,
"private_link_id": privateLinkID,
Expand Down Expand Up @@ -285,7 +299,7 @@ func resourceMongoDBAtlasPrivateEndpointServiceLinkDelete(ctx context.Context, d
Pending: []string{"NONE", "PENDING_ACCEPTANCE", "PENDING", "DELETING", "INITIATING"},
Target: []string{"REJECTED", "DELETED", "FAILED"},
Refresh: resourceServiceEndpointRefreshFunc(ctx, conn, projectID, providerName, privateLinkID, endpointServiceID),
Timeout: d.Timeout(schema.TimeoutCreate),
Timeout: d.Timeout(schema.TimeoutDelete),
MinTimeout: 5 * time.Second,
Delay: 3 * time.Second,
}
Expand All @@ -295,6 +309,22 @@ func resourceMongoDBAtlasPrivateEndpointServiceLinkDelete(ctx context.Context, d
if err != nil {
return diag.FromErr(fmt.Errorf(errorEndpointDelete, endpointServiceID, err))
}

clusterConf := &resource.StateChangeConf{
Pending: []string{"REPEATING", "PENDING"},
Target: []string{"IDLE", "DELETED"},
Refresh: resourceClusterListAdvancedRefreshFunc(ctx, projectID, conn),
Timeout: d.Timeout(schema.TimeoutDelete),
MinTimeout: 5 * time.Second,
Delay: 5 * time.Minute,
}

_, err = clusterConf.WaitForStateContext(ctx)

if err != nil {
// error awaiting advanced clusters IDLE should not result in failure to apply changes to this resource
log.Printf(errorAdvancedClusterListStatus, err)
}
}

return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,11 @@ func testAccCheckMongoDBAtlasPrivateLinkEndpointServiceADLDestroy(state *terrafo
func testAccMongoDBAtlasPrivateLinkEndpointServiceADLConfig(projectID, endpointID, comment string) string {
return fmt.Sprintf(`
resource "mongodbatlas_privatelink_endpoint_service_adl" "test" {
project_id = "%[1]s"
endpoint_id = "%[2]s"
comment = "%[3]s"
type = "DATA_LAKE"
provider_name = "AWS"
project_id = "%[1]s"
endpoint_id = "%[2]s"
comment = "%[3]s"
type = "DATA_LAKE"
provider_name = "AWS"
}
`, projectID, endpointID, comment)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,20 @@ func resourceMongoDBAtlasPrivateLinkEndpointServiceServerlessCreate(ctx context.
return diag.FromErr(fmt.Errorf(errorServerlessServiceEndpointAdd, endpointID, err))
}

clusterConf := &resource.StateChangeConf{
Pending: []string{"REPEATING", "PENDING"},
Target: []string{"IDLE", "DELETED"},
Refresh: resourceServerlessInstanceListRefreshFunc(ctx, projectID, conn),
Timeout: d.Timeout(schema.TimeoutCreate),
MinTimeout: 5 * time.Second,
Delay: 5 * time.Minute,
}

if _, err = clusterConf.WaitForStateContext(ctx); err != nil {
// error awaiting serverless instances to IDLE should not result in failure to apply changes to this resource
log.Printf(errorServerlessInstanceListStatus, err)
}

d.SetId(encodeStateID(map[string]string{
"project_id": projectID,
"instance_name": instanceName,
Expand Down
34 changes: 34 additions & 0 deletions mongodbatlas/resource_mongodbatlas_serverless_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import (
matlas "go.mongodb.org/atlas/mongodbatlas"
)

const (
errorServerlessInstanceListStatus = "error awaiting serverless instance list status IDLE: %s"
)

func resourceMongoDBAtlasServerlessInstance() *schema.Resource {
return &schema.Resource{
CreateContext: resourceMongoDBAtlasServerlessInstanceCreate,
Expand Down Expand Up @@ -368,6 +372,36 @@ func resourceServerlessInstanceRefreshFunc(ctx context.Context, name, projectID
}
}

func resourceServerlessInstanceListRefreshFunc(ctx context.Context, projectID string, client *matlas.Client) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
c, resp, err := client.ServerlessInstances.List(ctx, projectID, nil)

if err != nil && strings.Contains(err.Error(), "reset by peer") {
return nil, "REPEATING", nil
}

if err != nil && c == nil && resp == nil {
return nil, "", err
} else if err != nil {
if resp.StatusCode == 404 {
return "", "DELETED", nil
}
if resp.StatusCode == 503 {
return "", "PENDING", nil
}
return nil, "", err
}

for i := range c.Results {
if c.Results[i].StateName != "IDLE" {
return c.Results[i], "PENDING", nil
}
}

return c, "IDLE", nil
}
}

func flattenServerlessInstanceLinks(links []*matlas.Link) []map[string]interface{} {
linksList := make([]map[string]interface{}, 0)

Expand Down
2 changes: 1 addition & 1 deletion website/docs/r/privatelink_endpoint_service.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ description: |-
* Project Owner

-> **NOTE:** Groups and projects are synonymous terms. You may find group_id in the official documentation.

-> **NOTE:** Create and delete wait for all clusters on the project to IDLE in order for their operations to complete. This ensures the latest connection strings can be retrieved following creation or deletion of this resource. Default timeout is 2hrs.

## Example with AWS

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Describes a Serverless PrivateLink Endpoint Service
This is the second of two resources required to configure PrivateLink for Serverless, the first is [mongodbatlas_privatelink_endpoint_serverless](https://registry.terraform.io/providers/mongodb/mongodbatlas/latest/docs/resources/privatelink_endpoint_serverless).

-> **NOTE:** Groups and projects are synonymous terms. You may find group_id in the official documentation.
-> **NOTE:** Create waits for all serverless instances on the project to IDLE in order for their operations to complete. This ensures the latest connection strings can be retrieved following creation of this resource. Default timeout is 2hrs.

## Example Usage

Expand Down

0 comments on commit 75f3310

Please sign in to comment.