-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cluster-connection): implement conterpart for cluster connection…
…s api
- Loading branch information
Dmitry Kropachev
committed
Feb 10, 2024
1 parent
b35ed02
commit 0df4034
Showing
6 changed files
with
478 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,367 @@ | ||
package provider | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
"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/scylladb/terraform-provider-scylladbcloud/internal/scylla" | ||
"github.com/scylladb/terraform-provider-scylladbcloud/internal/scylla/model" | ||
) | ||
|
||
const ( | ||
clusterConnectionRetryTimeout = 40 * time.Minute | ||
clusterConnectionDeleteTimeout = 90 * time.Minute | ||
clusterConnectionRetryDelay = 5 * time.Second | ||
) | ||
|
||
func ResourceClusterConnection() *schema.Resource { | ||
return &schema.Resource{ | ||
CreateContext: resourceClusterConnectionCreate, | ||
ReadContext: resourceClusterConnectionRead, | ||
UpdateContext: resourceClusterConnectionUpdate, | ||
DeleteContext: resourceClusterConnectionDelete, | ||
|
||
Importer: &schema.ResourceImporter{ | ||
StateContext: schema.ImportStatePassthroughContext, | ||
}, | ||
|
||
Timeouts: &schema.ResourceTimeout{ | ||
Create: schema.DefaultTimeout(clusterConnectionRetryTimeout), | ||
Update: schema.DefaultTimeout(clusterConnectionRetryTimeout), | ||
Delete: schema.DefaultTimeout(clusterConnectionDeleteTimeout), | ||
}, | ||
|
||
Schema: map[string]*schema.Schema{ | ||
"id": { | ||
Description: "Cluster connection ID", | ||
Computed: true, | ||
Type: schema.TypeString, | ||
}, | ||
"region": { | ||
Description: "Region name", | ||
Computed: true, | ||
Type: schema.TypeString, | ||
}, | ||
"name": { | ||
Description: "Cluster Connection Name", | ||
Optional: true, | ||
Type: schema.TypeString, | ||
}, | ||
"cluster_id": { | ||
Description: "Cluster ID", | ||
Required: true, | ||
Type: schema.TypeInt, | ||
}, | ||
"datacenter": { | ||
Description: "Cluster datacenter name", | ||
Required: true, | ||
ForceNew: true, | ||
Type: schema.TypeString, | ||
}, | ||
"cidrlist": { | ||
Description: "List of CIDRs to route to the cluster connection", | ||
Required: true, | ||
Type: schema.TypeList, | ||
Elem: &schema.Schema{ | ||
Type: schema.TypeString, | ||
}, | ||
}, | ||
"type": { | ||
Description: "Connection Type", | ||
Required: true, | ||
ForceNew: true, | ||
Type: schema.TypeString, | ||
}, | ||
"status": { | ||
Description: "Connection Status", | ||
Computed: true, | ||
Optional: true, | ||
Type: schema.TypeString, | ||
}, | ||
"external_id": { | ||
Description: "ID of the cloud resource that represents connection", | ||
Computed: true, | ||
Type: schema.TypeString, | ||
}, | ||
"data": { | ||
Description: "Connection Data", | ||
Required: true, | ||
Type: schema.TypeMap, | ||
Elem: &schema.Schema{ | ||
Required: true, | ||
Computed: true, | ||
Type: schema.TypeString, | ||
}, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func resourceClusterConnectionCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | ||
var ( | ||
c = meta.(*scylla.Client) | ||
dcName = d.Get("datacenter").(string) | ||
cidrListVal, cidrListOK = d.GetOk("cidrlist") | ||
clusterID = d.Get("cluster_id").(int) | ||
r = &model.ClusterConnectionCreateRequest{ | ||
Name: d.Get("name").(string), | ||
Data: convertData(d.Get("data").(map[string]interface{})), | ||
Type: d.Get("type").(string), | ||
} | ||
p *scylla.CloudProvider | ||
) | ||
|
||
dcs, err := c.ListDataCenters(ctx, int64(clusterID)) | ||
if err != nil { | ||
return diag.Errorf("error reading clusters: %s", err) | ||
} | ||
|
||
for _, dc := range dcs { | ||
if strings.EqualFold(dc.Name, dcName) { | ||
r.ClusterDCID = dc.ID | ||
p = c.Meta.ProviderByID(dc.CloudProviderID) | ||
if p == nil { | ||
return diag.Errorf("unable to find cloud provider with id=%d", dc.CloudProviderID) | ||
} | ||
break | ||
} | ||
} | ||
|
||
if r.ClusterDCID == 0 { | ||
return diag.Errorf("unable to find %q datacenter", dcName) | ||
} | ||
|
||
if !cidrListOK { | ||
return diag.Errorf(`"cidrlist" is required for %q cloud`, p.CloudProvider.Name) | ||
} | ||
|
||
if len(cidrListVal.([]any)) == 0 { | ||
return diag.Errorf(`"cidrlist" cannot be empty`) | ||
} | ||
|
||
r.CIDRList, err = CIDRList(cidrListVal) | ||
if err != nil { | ||
return diag.Errorf(`"cidrlist" must be a list of strings`) | ||
} | ||
|
||
conn, err := c.CreateClusterConnection(ctx, int64(clusterID), r) | ||
if err != nil { | ||
return diag.Errorf("error creating cluster connection: %s", err) | ||
} | ||
d.SetId(strconv.FormatInt(conn.ID, 10)) | ||
err = waitForClusterConnection(ctx, c, int64(clusterID), conn.ID, "ACTIVE") | ||
if err != nil { | ||
return diag.Errorf(err.Error()) | ||
} | ||
conn, err = c.GetClusterConnection(ctx, int64(clusterID), conn.ID) | ||
if err != nil { | ||
return diag.Errorf("error reading cluster connection %d: %s", conn.ID, err) | ||
} | ||
_ = d.Set("external_id", conn.ExternalID) | ||
return nil | ||
} | ||
|
||
func resourceClusterConnectionRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | ||
var ( | ||
c = meta.(*scylla.Client) | ||
connIDStr = d.Id() | ||
dc *model.Datacenter | ||
) | ||
|
||
if connIDStr == "" { | ||
return nil | ||
} | ||
|
||
connectionID, err := strconv.ParseInt(connIDStr, 10, 64) | ||
if err != nil { | ||
return diag.Errorf("failed to parse connection id %q: %s", connIDStr, err) | ||
} | ||
|
||
clusterID := int64(d.Get("cluster_id").(int)) | ||
|
||
cluster, connection, err := getClusterAndConnection(ctx, c, clusterID, connectionID) | ||
if err != nil { | ||
return diag.Errorf(err.Error()) | ||
} | ||
|
||
if connection == nil || cluster == nil { | ||
d.SetId("") | ||
_ = d.Set("cluster_id", 0) | ||
return nil | ||
} | ||
|
||
for id := range cluster.Datacenters { | ||
if cluster.Datacenters[id].ID == connection.ClusterDCID { | ||
dc = &cluster.Datacenters[id] | ||
break | ||
} | ||
} | ||
|
||
if dc == nil { | ||
d.SetId("") | ||
_ = d.Set("cluster_id", 0) | ||
return nil | ||
} | ||
|
||
_ = d.Set("datacenter", dc.Name) | ||
_ = d.Set("external_id", connection.ExternalID) | ||
_ = d.Set("region", dc.Region.ExternalID) | ||
_ = d.Set("cluster_id", connection.ClusterID) | ||
_ = d.Set("cidrlist", connection.CIDRList) | ||
_ = d.Set("name", connection.Name) | ||
_ = d.Set("data", convertFromData(connection.Data)) | ||
_ = d.Set("type", connection.Type) | ||
return nil | ||
} | ||
|
||
func resourceClusterConnectionUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | ||
var ( | ||
c = meta.(*scylla.Client) | ||
clusterID = d.Get("cluster_id").(int) | ||
) | ||
connID, err := strconv.ParseInt(d.Id(), 10, 64) | ||
if err != nil { | ||
return diag.Errorf("failed to parse connection id %q: %s", d.Id(), err) | ||
} | ||
req := model.ClusterConnectionUpdateRequest{ | ||
Name: d.Get("name").(string), | ||
CIDRList: convertListOfString(d.Get("cidrlist").([]interface{})), | ||
Status: d.Get("status").(string), | ||
} | ||
|
||
err = c.UpdateClusterConnections(ctx, int64(clusterID), connID, &req) | ||
if err != nil { | ||
return diag.Errorf("error updating cluster connection: %s", err) | ||
} | ||
err = waitForClusterConnection(ctx, c, int64(clusterID), connID, req.Status) | ||
if err != nil { | ||
return diag.Errorf(err.Error()) | ||
} | ||
|
||
conn, err := c.GetClusterConnection(ctx, int64(clusterID), connID) | ||
if err != nil { | ||
return diag.Errorf("error reading cluster connection %d: %s", connID, err) | ||
} | ||
_ = d.Set("external_id", conn.ExternalID) | ||
return nil | ||
} | ||
|
||
func resourceClusterConnectionDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | ||
var ( | ||
c = meta.(*scylla.Client) | ||
clusterID = d.Get("cluster_id").(int) | ||
connIDStr = d.Id() | ||
) | ||
|
||
if connIDStr == "" { | ||
return nil | ||
} | ||
|
||
connID, err := strconv.ParseInt(connIDStr, 10, 64) | ||
if err != nil { | ||
return diag.Errorf("failed to parse connection id %q: %s", connIDStr, err) | ||
} | ||
|
||
if err = c.DeleteClusterConnection(ctx, int64(clusterID), connID); err != nil { | ||
if scylla.IsClusterConnectionDeletedErr(err) { | ||
return nil // cluster was already deleted | ||
} | ||
return diag.Errorf("error deleting cluster connection: %s", err) | ||
} | ||
err = waitForClusterConnection(ctx, c, int64(clusterID), connID, "DELETED") | ||
if err != nil { | ||
return diag.Errorf("error waiting for cluster connection to become deleted: %s", err) | ||
} | ||
return nil | ||
} | ||
|
||
func convertData(mapVal map[string]interface{}) map[string]string { | ||
out := make(map[string]string, len(mapVal)) | ||
for key, val := range mapVal { | ||
out[key] = val.(string) | ||
} | ||
return out | ||
} | ||
|
||
func convertFromData(mapVal map[string]string) map[string]interface{} { | ||
out := make(map[string]interface{}, len(mapVal)) | ||
for key, val := range mapVal { | ||
out[key] = val | ||
} | ||
return out | ||
} | ||
|
||
func convertListOfString(mapVal []interface{}) []string { | ||
out := make([]string, len(mapVal)) | ||
for key, val := range mapVal { | ||
out[key] = val.(string) | ||
} | ||
return out | ||
} | ||
|
||
func waitForClusterConnection(ctx context.Context, c *scylla.Client, clusterID, connectionID int64, targetStatus string) error { | ||
stateConf := &retry.StateChangeConf{ | ||
Pending: []string{"PENDING", "INIT", "DELETING"}, | ||
Target: []string{targetStatus}, | ||
Refresh: func() (interface{}, string, error) { | ||
conn, err := c.GetClusterConnection(context.Background(), clusterID, connectionID) | ||
switch { | ||
case err == nil: | ||
return conn, conn.Status, nil | ||
case scylla.IsNotFound(err), scylla.IsClusterConnectionDeletedErr(err): | ||
return nil, "DELETED", nil | ||
default: | ||
return nil, "", err | ||
} | ||
}, | ||
Delay: clusterConnectionRetryDelay, | ||
Timeout: clusterConnectionRetryTimeout, | ||
} | ||
|
||
_, err := stateConf.WaitForStateContext(ctx) | ||
if err != nil { | ||
return fmt.Errorf("error waiting for cluster connection to become %q: %s", targetStatus, err) | ||
} | ||
return nil | ||
} | ||
|
||
func getClusterAndConnection(ctx context.Context, c *scylla.Client, clusterID, connectionID int64) (cluster *model.Cluster, connection *model.ClusterConnection, err error) { | ||
if clusterID != 0 { | ||
cluster, err = c.GetCluster(ctx, clusterID) | ||
if err != nil { | ||
if scylla.IsNotFound(err) { | ||
return nil, nil, nil | ||
} | ||
return nil, nil, fmt.Errorf("error reading cluster %d: %s", clusterID, err) | ||
} | ||
|
||
connection, err = c.GetClusterConnection(ctx, cluster.ID, connectionID) | ||
if err != nil && !scylla.IsNotFound(err) && !scylla.IsClusterConnectionDeletedErr(err) { | ||
return nil, nil, fmt.Errorf("error reading cluster connection %d: %s", connectionID, err) | ||
} | ||
return cluster, connection, nil | ||
} | ||
clusters, err := c.ListClusters(ctx) | ||
if err != nil { | ||
return nil, nil, fmt.Errorf("error reading cluster list: %s", err) | ||
} | ||
|
||
for i := range clusters { | ||
cluster = &clusters[i] | ||
connection, err = c.GetClusterConnection(ctx, cluster.ID, connectionID) | ||
if err == nil { | ||
return cluster, connection, nil | ||
} | ||
if !scylla.IsNotFound(err) && !scylla.IsClusterConnectionDeletedErr(err) { | ||
return nil, nil, fmt.Errorf("error reading cluster connection %d: %s", connectionID, err) | ||
} | ||
} | ||
return nil, nil, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.