@@ -10,6 +10,7 @@ import (
1010 "github.com/scylladb/terraform-provider-scylladbcloud/internal/scylla"
1111 "github.com/scylladb/terraform-provider-scylladbcloud/internal/scylla/model"
1212
13+ "github.com/hashicorp/go-cty/cty"
1314 "github.com/hashicorp/terraform-plugin-log/tflog"
1415 "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1516 "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -40,14 +41,19 @@ func ResourceCluster() *schema.Resource {
4041 Delete : schema .DefaultTimeout (clusterDeleteTimeout ),
4142 },
4243
43- SchemaVersion : 1 ,
44+ SchemaVersion : 2 ,
4445
4546 StateUpgraders : []schema.StateUpgrader {
4647 {
4748 Version : 0 ,
4849 Type : resourceClusterV0 ().CoreConfigSchema ().ImpliedType (),
4950 Upgrade : resourceClusterUpgradeV0 ,
5051 },
52+ {
53+ Version : 1 ,
54+ Type : resourceClusterV1 ().CoreConfigSchema ().ImpliedType (),
55+ Upgrade : resourceClusterUpgradeV1 ,
56+ },
5157 },
5258
5359 Schema : map [string ]* schema.Schema {
@@ -76,10 +82,24 @@ func ResourceCluster() *schema.Resource {
7682 Type : schema .TypeString ,
7783 },
7884 "node_count" : {
79- Description : "Node count" ,
85+ Description : "Current node count (computed)" ,
86+ Computed : true ,
87+ Type : schema .TypeInt ,
88+ },
89+ "min_nodes" : {
90+ Description : "Minimum number of nodes" ,
8091 Required : true ,
81- ForceNew : true ,
8292 Type : schema .TypeInt ,
93+ ValidateDiagFunc : func (v interface {}, path cty.Path ) diag.Diagnostics {
94+ value := v .(int )
95+ if value < 3 {
96+ return diag .Errorf ("min_nodes must be at least 3, got %d" , value )
97+ }
98+ if value % 3 != 0 {
99+ return diag .Errorf ("min_nodes must be divisible by 3, got %d" , value )
100+ }
101+ return nil
102+ },
83103 },
84104 "byoa_id" : {
85105 Description : "BYOA credential ID (only for AWS)" ,
@@ -187,7 +207,7 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta int
187207 ClusterName : d .Get ("name" ).(string ),
188208 BroadcastType : "PRIVATE" ,
189209 ReplicationFactor : 3 ,
190- NumberOfNodes : int64 (d .Get ("node_count " ).(int )),
210+ NumberOfNodes : int64 (d .Get ("min_nodes " ).(int )),
191211 UserAPIInterface : d .Get ("user_api_interface" ).(string ),
192212 EnableDNSAssociation : d .Get ("enable_dns" ).(bool ),
193213 }
@@ -275,7 +295,7 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta int
275295 return diag .Errorf ("error creating cluster: %s" , err )
276296 }
277297
278- if err := WaitForCluster (ctx , scyllaClient , cr .ID ); err != nil {
298+ if err := WaitForClusterRequestID (ctx , scyllaClient , cr .ID ); err != nil {
279299 return diag .Errorf ("error waiting for cluster: %s" , err )
280300 }
281301
@@ -303,12 +323,16 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta int
303323func resourceClusterRead (ctx context.Context , d * schema.ResourceData , meta interface {}) diag.Diagnostics {
304324 scyllaClient := meta .(* scylla.Client )
305325
306- clusterID , err := strconv . ParseInt ( d . Id (), 10 , 64 )
307- if err != nil {
308- return diag . Errorf ( "error reading id=%q: %s" , d . Id (), err )
326+ clusterID , diags := parseClusterID ( d )
327+ if diags != nil {
328+ return diags
309329 }
310330
311- reqs , err := scyllaClient .ListClusterRequest (ctx , clusterID , "CREATE_CLUSTER" )
331+ reqs , err := scyllaClient .ListClusterRequest (
332+ ctx ,
333+ clusterID ,
334+ scylla.ListClusterRequestParams {Type : "CREATE_CLUSTER" },
335+ )
312336 switch {
313337 case scylla .IsDeletedErr (err ):
314338 _ = d .Set ("status" , "DELETED" )
@@ -321,7 +345,7 @@ func resourceClusterRead(ctx context.Context, d *schema.ResourceData, meta inter
321345 _ = d .Set ("request_id" , reqs [0 ].ID )
322346
323347 if reqs [0 ].Status != "COMPLETED" {
324- if err := WaitForCluster (ctx , scyllaClient , reqs [0 ].ID ); err != nil {
348+ if err := WaitForClusterRequestID (ctx , scyllaClient , reqs [0 ].ID ); err != nil {
325349 return diag .Errorf ("error waiting for cluster: %s" , err )
326350 }
327351 }
@@ -371,6 +395,9 @@ func setClusterKVs(d *schema.ResourceData, cluster *model.Cluster, providerName,
371395 _ = d .Set ("cloud" , providerName )
372396 _ = d .Set ("region" , cluster .Region .ExternalID )
373397 _ = d .Set ("node_count" , len (model .NodesByStatus (cluster .Nodes , "ACTIVE" )))
398+ if _ , ok := d .GetOk ("min_nodes" ); ! ok {
399+ _ = d .Set ("min_nodes" , len (model .NodesByStatus (cluster .Nodes , "ACTIVE" )))
400+ }
374401 _ = d .Set ("user_api_interface" , cluster .UserAPIInterface )
375402 _ = d .Set ("node_type" , instanceExternalID )
376403 _ = d .Set ("node_dns_names" , model .NodesDNSNames (cluster .Nodes ))
@@ -398,17 +425,91 @@ func setClusterKVs(d *schema.ResourceData, cluster *model.Cluster, providerName,
398425}
399426
400427func resourceClusterUpdate (ctx context.Context , d * schema.ResourceData , meta interface {}) diag.Diagnostics {
401- // Scylla Cloud API does not support updating a cluster,
402- // thus the update always fails
403- return diag .Errorf (`updating "scylla_cluster" resource is not supported` )
428+ scyllaClient := meta .(* scylla.Client )
429+
430+ // Currently, only min_nodes is updatable.
431+ if ! d .HasChange ("min_nodes" ) {
432+ return nil
433+ }
434+
435+ // There are three scenarios:
436+ // - scale-out: newMinNodes > oldMinNodes
437+ // - scale-in: newMinNodes < oldMinNodes
438+ // - no-op: newMinNodes == oldMinNodes
439+ //
440+ // The no-op case is already handled above by checking `d.HasChange()`.
441+ //
442+ // Scale-out is easy: we just request more nodes.
443+ //
444+ // Scale-in is more complicated. It may happen that it's currently
445+ // not possible, because after the scale-in there would be not enough
446+ // disk space. Such an update should fail, meaning that the old value
447+ // of min_nodes should not be changed. A user can try again later.
448+ //
449+ // Note that it's not possible to update min_nodes and defer
450+ // scale-in until later, e.g., when there is enough disk space,
451+ // because min_nodes controls the resize API behavior rather
452+ // than controlling the desired state. If such a behavior is needed,
453+ // please consider X Cloud. More:
454+ // https://www.scylladb.com/product/scylladb-xcloud/
455+
456+ oldMinNodesI , newMinNodesI := d .GetChange ("min_nodes" )
457+
458+ oldMinNodes , newMinNodes := oldMinNodesI .(int ), newMinNodesI .(int )
459+
460+ clusterID , diags := parseClusterID (d )
461+ if diags != nil {
462+ return diags
463+ }
464+
465+ tflog .Debug (ctx , "Updating cluster min_nodes" , map [string ]interface {}{
466+ "cluster_id" : clusterID ,
467+ "old" : oldMinNodes ,
468+ "new" : newMinNodes ,
469+ })
470+
471+ cluster , err := scyllaClient .GetCluster (ctx , clusterID )
472+ if err != nil {
473+ if scylla .IsClusterDeletedErr (err ) {
474+ d .SetId ("" )
475+ return nil // cluster was deleted
476+ }
477+ return diag .Errorf ("error reading cluster: %s" , err )
478+ }
479+
480+ if n := len (cluster .Datacenters ); n > 1 {
481+ return diag .Errorf ("multi-datacenter clusters are not currently supported: %d" , n )
482+ }
483+
484+ // Resize will fail if there is any ongoing cluster request.
485+ if err := waitForNoInProgressRequests (ctx , scyllaClient , cluster .ID ); err != nil {
486+ return diag .Errorf ("error waiting for no in-progress cluster requests: %s" , err )
487+ }
488+
489+ resizeRequest , err := scyllaClient .ResizeCluster (
490+ ctx ,
491+ cluster .ID ,
492+ cluster .Datacenter .ID ,
493+ cluster .Datacenter .InstanceID ,
494+ newMinNodes ,
495+ )
496+ if err != nil {
497+ return diag .Errorf ("error resizing cluster: %s" , err )
498+ }
499+
500+ if err := WaitForClusterRequestID (ctx , scyllaClient , resizeRequest .ID ); err != nil {
501+ return diag .Errorf ("error waiting for cluster resize: %s" , err )
502+ }
503+
504+ return resourceClusterRead (ctx , d , meta )
404505}
405506
406507func resourceClusterDelete (ctx context.Context , d * schema.ResourceData , meta interface {}) diag.Diagnostics {
407508 c := meta .(* scylla.Client )
408509
409- clusterID , err := strconv . ParseInt ( d . Id (), 10 , 64 )
410- if err != nil {
411- return diag . Errorf ( "error reading id=%q: %s" , d . Id (), err )
510+ clusterID , diags := parseClusterID ( d )
511+ if diags != nil {
512+ return diags
412513 }
413514
414515 name , ok := d .GetOk ("name" )
@@ -431,7 +532,7 @@ func resourceClusterDelete(ctx context.Context, d *schema.ResourceData, meta int
431532 return nil
432533}
433534
434- func WaitForCluster (ctx context.Context , c * scylla.Client , requestID int64 ) error {
535+ func WaitForClusterRequestID (ctx context.Context , c * scylla.Client , requestID int64 ) error {
435536 t := time .NewTicker (clusterPollInterval )
436537 defer t .Stop ()
437538
@@ -455,46 +556,32 @@ func WaitForCluster(ctx context.Context, c *scylla.Client, requestID int64) erro
455556 return nil
456557}
457558
458- func resourceClusterUpgradeV0 (ctx context.Context , rawState map [string ]any , meta any ) (map [string ]any , error ) {
459- var (
460- scyllaClient = meta .(* scylla.Client )
461- cloud , cloudOK = rawState ["cloud" ].(string )
462- nodeType , nodeTypeOK = rawState ["node_type" ].(string )
463- region , regionOK = rawState ["region" ].(string )
464- )
465-
466- if ! cloudOK {
467- return nil , fmt .Errorf (`"cloud" is undefined` )
468- }
469-
470- if ! nodeTypeOK {
471- return nil , fmt .Errorf (`"node_type" is undefined` )
472- }
559+ func waitForNoInProgressRequests (ctx context.Context , c * scylla.Client , clusterID int64 ) error {
560+ t := time .NewTicker (clusterPollInterval )
561+ defer t .Stop ()
473562
474- if ! regionOK {
475- return nil , fmt .Errorf (`"region" is undefined` )
476- }
563+ for range t .C {
564+ reqs , err := c .ListClusterRequest (
565+ ctx ,
566+ clusterID ,
567+ scylla.ListClusterRequestParams {Status : "IN_PROGRESS" },
568+ )
569+ if err != nil {
570+ return fmt .Errorf ("error reading cluster requests: %w" , err )
571+ }
477572
478- p := scyllaClient . Meta . ProviderByName ( cloud )
479- if p == nil {
480- return nil , fmt . Errorf ( `unrecognized value %q for "cloud"` , cloud )
573+ if len ( reqs ) == 0 {
574+ break
575+ }
481576 }
482577
483- mr := p .RegionByName (region )
484- if mr == nil {
485- return nil , fmt .Errorf (`unrecognized value %q for "region"` , region )
486- }
578+ return nil
579+ }
487580
488- instances , err := scyllaClient .ListCloudProviderInstancesPerRegion (ctx , p .CloudProvider .ID , mr .ID )
581+ func parseClusterID (d * schema.ResourceData ) (int64 , diag.Diagnostics ) {
582+ clusterID , err := strconv .ParseInt (d .Id (), 10 , 64 )
489583 if err != nil {
490- return nil , fmt .Errorf ("failed to list cloud provider instances for region %q: %s" , region , err )
584+ return 0 , diag .Errorf ("error reading id= %q: %s" , d . Id () , err )
491585 }
492-
493- mi := p .InstanceByNameFromInstances (nodeType , instances )
494- if mi == nil {
495- return nil , fmt .Errorf (`unrecognized value %q for "node_type"` , nodeType )
496- }
497-
498- rawState ["node_disk_size" ] = int (mi .TotalStorage )
499- return rawState , nil
586+ return clusterID , nil
500587}
0 commit comments