4
4
"context"
5
5
"fmt"
6
6
"net/http"
7
+ "time"
7
8
8
9
"github.com/datastax/astra-client-go/v2/astra"
9
10
"github.com/hashicorp/terraform-plugin-framework/attr"
@@ -13,6 +14,7 @@ import (
13
14
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
14
15
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
15
16
"github.com/hashicorp/terraform-plugin-framework/types"
17
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
16
18
)
17
19
18
20
type CDCResource struct {
@@ -155,6 +157,14 @@ func (r *CDCResource) Create(ctx context.Context, req resource.CreateRequest, re
155
157
return
156
158
}
157
159
160
+ // wait for the database to be active before after CDC
161
+ if err := waitForDatabaseActive (ctx , astraClient , plan .DatabaseID .ValueString ()); err != nil {
162
+ resp .Diagnostics .AddError (
163
+ fmt .Sprintf ("failed to wait for database '%s' to be active" , plan .DatabaseID .ValueString ()),
164
+ err .Error ())
165
+ return
166
+ }
167
+
158
168
plan .setDataTopics ()
159
169
160
170
diags = resp .State .Set (ctx , & plan )
@@ -192,8 +202,49 @@ func (r *CDCResource) Read(ctx context.Context, req resource.ReadRequest, resp *
192
202
}
193
203
194
204
func (r * CDCResource ) Update (ctx context.Context , req resource.UpdateRequest , resp * resource.UpdateResponse ) {
195
- // CDC resources are typically immutable, so this might not be implemented.
196
- resp .Diagnostics .AddError ("Not Implemented" , "Update is not supported for CDC resources." )
205
+ var plan CDCResourceModel
206
+ diags := req .Plan .Get (ctx , & plan )
207
+ resp .Diagnostics .Append (diags ... )
208
+ if resp .Diagnostics .HasError () {
209
+ return
210
+ }
211
+
212
+ astraClient := r .clients .astraClient
213
+
214
+ // wait for the database to be active before updating CDC
215
+ if err := waitForDatabaseActive (ctx , astraClient , plan .DatabaseID .ValueString ()); err != nil {
216
+ resp .Diagnostics .AddError (
217
+ fmt .Sprintf ("failed to wait for database '%s' to be active" , plan .DatabaseID .ValueString ()),
218
+ err .Error ())
219
+ return
220
+ }
221
+
222
+ cdcRequestBody := createUpdateCDCRequestBody (& plan )
223
+ cdcResponse , err := astraClient .UpdateCDCWithResponse (ctx , cdcRequestBody .DatabaseID , cdcRequestBody )
224
+ if err != nil {
225
+ resp .Diagnostics .AddError (
226
+ "failed to enable CDC" ,
227
+ err .Error ())
228
+ return
229
+ } else if cdcResponse .StatusCode () != http .StatusNoContent {
230
+ errString := fmt .Sprintf ("failed to update CDC for DB '%s' with status code '%v', message: '%s'" ,
231
+ plan .DatabaseID .ValueString (), cdcResponse .StatusCode (), string (cdcResponse .Body ))
232
+ resp .Diagnostics .AddError ("failed to update CDC" , errString )
233
+ return
234
+ }
235
+
236
+ // wait for the database to be active before after CDC
237
+ if err := waitForDatabaseActive (ctx , astraClient , plan .DatabaseID .ValueString ()); err != nil {
238
+ resp .Diagnostics .AddError (
239
+ fmt .Sprintf ("failed to wait for database '%s' to be active" , plan .DatabaseID .ValueString ()),
240
+ err .Error ())
241
+ return
242
+ }
243
+
244
+ plan .setDataTopics ()
245
+
246
+ diags = resp .State .Set (ctx , & plan )
247
+ resp .Diagnostics .Append (diags ... )
197
248
}
198
249
199
250
func (r * CDCResource ) Delete (ctx context.Context , req resource.DeleteRequest , resp * resource.DeleteResponse ) {
@@ -279,6 +330,30 @@ func createEnableCDCRequestBody(tfData *CDCResourceModel) astra.EnableCDCJSONReq
279
330
return reqData
280
331
}
281
332
333
+ func createUpdateCDCRequestBody (tfData * CDCResourceModel ) astra.UpdateCDCJSONRequestBody {
334
+ reqData := astra.UpdateCDCJSONRequestBody {
335
+ DatabaseID : tfData .DatabaseID .ValueString (),
336
+ DatabaseName : tfData .DatabaseName .ValueString (),
337
+ }
338
+ for _ , table := range tfData .Tables {
339
+ nextTable := TableJSON {
340
+ KeyspaceName : table .Keyspace .ValueString (),
341
+ TableName : table .Table .ValueString (),
342
+ }
343
+ reqData .Tables = append (reqData .Tables , nextTable )
344
+ }
345
+ for _ , region := range tfData .Regions {
346
+ nextRegion := RegionJSON {
347
+ DatacenterRegion : region .Region .ValueString (),
348
+ DatacenterID : region .DatacenterID .ValueString (),
349
+ StreamingClusterName : region .StreamingCluster .ValueString (),
350
+ StreamingTenantName : region .StreamingTenant .ValueString (),
351
+ }
352
+ reqData .Regions = append (reqData .Regions , nextRegion )
353
+ }
354
+ return reqData
355
+ }
356
+
282
357
// copyResponseDataToResourceState copies the data from the REST endpoing response to the Terraform resource state model.
283
358
func copyResponseDataToResourceState (tfData * CDCResourceModel , respData * astra.ListCDCResponse ) {
284
359
tfData .DatabaseID = types .StringValue (respData .DatabaseID )
@@ -344,3 +419,38 @@ func (m *CDCResourceModel) setDataTopics() {
344
419
345
420
m .DataTopics = types .MapValueMust (types.MapType {ElemType : types .StringType }, dataTopicsMap )
346
421
}
422
+
423
+ var (
424
+ cdcUpdateTimeout = time .Duration (2 * time .Minute )
425
+ )
426
+
427
+ // waitForDatabaseActive waits for the database to reach the ACTIVE state. Will return an error if the database reaches a terminal state (ERROR, TERMINATED, or TERMINATING),
428
+ // or if the request returns an unexpected HTTP status code, or if the request times out.
429
+ func waitForDatabaseActive (ctx context.Context , client * astra.ClientWithResponses , databaseID string ) error {
430
+ return retry .RetryContext (ctx , cdcUpdateTimeout , func () * retry.RetryError {
431
+ res , err := client .GetDatabaseWithResponse (ctx , astra .DatabaseIdParam (databaseID ))
432
+ // Errors sending request should be retried and are assumed to be transient
433
+ if err != nil || res .StatusCode () >= http .StatusInternalServerError {
434
+ return retry .RetryableError (fmt .Errorf ("error getting database status: %s" , string (res .Body )))
435
+ }
436
+
437
+ // don't retry on unexpected HTTP errors
438
+ if res .StatusCode () == http .StatusUnauthorized {
439
+ return retry .NonRetryableError (fmt .Errorf ("user not authorized. Effective role must have 'View DB' permission on the database (or on all DBs in the current org)" ))
440
+ } else if res .StatusCode () > http .StatusOK || res .JSON200 == nil {
441
+ return retry .NonRetryableError (fmt .Errorf ("unexpected response fetching database, status code: %d, message %s" , res .StatusCode (), string (res .Body )))
442
+ }
443
+
444
+ // Success fetching database
445
+ dbStatus := res .JSON200 .Status
446
+ switch dbStatus {
447
+ case astra .ERROR , astra .TERMINATED , astra .TERMINATING :
448
+ // If the database reached a terminal state it will never become active
449
+ return retry .NonRetryableError (fmt .Errorf ("database failed to reach active status: status='%s'" , dbStatus ))
450
+ case astra .ACTIVE :
451
+ return nil
452
+ default :
453
+ return retry .RetryableError (fmt .Errorf ("waiting database to be active but is '%s'" , dbStatus ))
454
+ }
455
+ })
456
+ }
0 commit comments