@@ -3,12 +3,17 @@ package dedicatedserver
33import (
44 "context"
55 "encoding/base64"
6+ "errors"
7+ "fmt"
68 "strings"
9+ "time"
710
811 "github.com/cenkalti/backoff/v5"
912 "github.com/hashicorp/terraform-plugin-framework-validators/int32validator"
1013 "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
1114 "github.com/hashicorp/terraform-plugin-framework/attr"
15+ "github.com/hashicorp/terraform-plugin-framework/diag"
16+ "github.com/hashicorp/terraform-plugin-framework/path"
1217 "github.com/hashicorp/terraform-plugin-framework/resource"
1318 "github.com/hashicorp/terraform-plugin-framework/resource/schema"
1419 "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
@@ -24,8 +29,8 @@ import (
2429)
2530
2631var (
27- _ resource.Resource = & installationResource {}
28- _ resource.ResourceWithConfigure = & installationResource {}
32+ _ resource.ResourceWithConfigure = & installationResource {}
33+ _ resource.ResourceWithImportState = & installationResource {}
2934)
3035
3136func NewInstallationResource () resource.Resource {
@@ -326,94 +331,72 @@ func (i *installationResource) Create(
326331 }
327332
328333 serverID := plan .DedicatedServerID .ValueString ()
329- result , response , err := i .DedicatedserverAPI .InstallOperatingSystem (ctx , serverID ).
334+ job , response , err := i .DedicatedserverAPI .InstallOperatingSystem (ctx , serverID ).
330335 InstallOperatingSystemOpts (* opts ).Execute ()
331336 if err != nil {
332337 utils .SdkError (ctx , & resp .Diagnostics , err , response )
333338 return
334339 }
335340
336- payload := result .GetPayload ()
337- plan .ID = types .StringValue (result .GetUuid ())
338- plan .Device = types .StringValue (payload .GetDevice ())
339- plan .Timezone = types .StringValue (payload .GetTimezone ())
340- plan .PowerCycle = types .BoolValue (payload .GetPowerCycle ())
341-
342- partitionAttributeTypes := map [string ]attr.Type {
343- "filesystem" : types .StringType ,
344- "mountpoint" : types .StringType ,
345- "size" : types .StringType ,
346- }
347-
348- // Preparing and converting partitions into types.Object to store in the state
349- var partitionsObjects []attr.Value
350- for _ , p := range payload .GetPartitions () {
351- partition := partitionsResourceModel {
352- Filesystem : types .StringValue (p .GetFilesystem ()),
353- Mountpoint : types .StringValue (p .GetMountpoint ()),
354- Size : types .StringValue (p .GetSize ()),
355- }
356-
357- partitionObj , diags := types .ObjectValueFrom (
358- ctx ,
359- partitionAttributeTypes ,
360- partition ,
361- )
362- if diags .HasError () {
363- resp .Diagnostics .Append (diags ... )
364- return
365- }
366-
367- partitionsObjects = append (partitionsObjects , partitionObj )
341+ err = i .waitForJobCompletion (serverID , job .GetUuid (), ctx , resp )
342+ if err != nil {
343+ utils .ReportError (err .Error (), & resp .Diagnostics )
344+ return
368345 }
369346
370- // Convert the slice of partition objects to a types.List and store it in the plan
371- partitionsList , diags := types .ListValueFrom (
372- ctx ,
373- types.ObjectType {
374- AttrTypes : partitionAttributeTypes ,
375- },
376- partitionsObjects ,
377- )
378- if diags .HasError () {
379- resp .Diagnostics .Append (diags ... )
347+ diags := i .syncResourceModelWithSDK (& plan , * job , ctx )
348+ resp .Diagnostics .Append (diags ... )
349+ if resp .Diagnostics .HasError () {
380350 return
381351 }
382- plan .Partitions = partitionsList
383352
384- pollJobStatus := func () (string , error ) {
385- return getJobStatus (serverID , result .GetUuid (), i , ctx , resp )
386- }
353+ resp .Diagnostics .Append (resp .State .Set (ctx , plan )... )
354+ }
387355
388- _ , err = backoff .Retry (context .TODO (), pollJobStatus , backoff .WithBackOff (backoff .NewExponentialBackOff ()))
389- if err != nil {
390- utils .GeneralError (& resp .Diagnostics , ctx , err )
356+ func (i * installationResource ) ImportState (
357+ ctx context.Context ,
358+ req resource.ImportStateRequest ,
359+ resp * resource.ImportStateResponse ,
360+ ) {
361+ // Retrieve import ID and save to id attribute
362+ resource .ImportStatePassthroughID (ctx , path .Root ("dedicated_server_id" ), req , resp )
363+ }
364+
365+ func (i * installationResource ) Read (
366+ ctx context.Context ,
367+ req resource.ReadRequest ,
368+ resp * resource.ReadResponse ,
369+ ) {
370+ var state installationResourceModel
371+ resp .Diagnostics .Append (req .State .Get (ctx , & state )... )
372+ if resp .Diagnostics .HasError () {
373+ return
391374 }
392375
393- resp .Diagnostics .Append (resp .State .Set (ctx , plan )... )
394- }
376+ serverID := state .DedicatedServerID .ValueString ()
395377
396- func getJobStatus ( serverID string , jobID string , i * installationResource , ctx context. Context , resp * resource. CreateResponse ) ( string , error ) {
397- request := i . DedicatedserverAPI . GetJob ( ctx , serverID , jobID )
378+ result , response , err := i . DedicatedserverAPI . GetJobList ( ctx , serverID ).
379+ Offset ( 0 ). Limit ( 1 ). Type_ ( "install" ). Status ( "FINISHED" ). Execute ( )
398380
399- result , response , err := request .Execute ()
400381 if err != nil {
401382 utils .SdkError (ctx , & resp .Diagnostics , err , response )
383+ return
402384 }
403385
404- status := result .GetStatus ()
405- if status != "FINISHED" {
406- return "" , backoff .RetryAfter (30 )
386+ jobs := result .GetJobs ()
387+
388+ if len (jobs ) == 0 {
389+ utils .ReportError (fmt .Sprintf ("No installation jobs found for server %s" , serverID ), & resp .Diagnostics )
390+ return
407391 }
408392
409- return status , nil
410- }
393+ diags := i .syncResourceModelWithSDK (& state , jobs [0 ], ctx )
394+ resp .Diagnostics .Append (diags ... )
395+ if resp .Diagnostics .HasError () {
396+ return
397+ }
411398
412- func (i * installationResource ) Read (
413- _ context.Context ,
414- _ resource.ReadRequest ,
415- _ * resource.ReadResponse ,
416- ) {
399+ resp .Diagnostics .Append (resp .State .Set (ctx , state )... )
417400}
418401
419402func (i * installationResource ) Update (
@@ -429,3 +412,92 @@ func (i *installationResource) Delete(
429412 _ * resource.DeleteResponse ,
430413) {
431414}
415+
416+ // isJobFinished checks if the job status is "FINISHED".
417+ func (i * installationResource ) isJobFinished (serverID , jobID string , ctx context.Context , resp * resource.CreateResponse ) bool {
418+ // Fetch the job status
419+ result , response , err := i .DedicatedserverAPI .GetJob (ctx , serverID , jobID ).Execute ()
420+ if err != nil {
421+ utils .SdkError (ctx , & resp .Diagnostics , err , response )
422+ return false // Return false indicating the status couldn't be fetched
423+ }
424+
425+ // Return true if the job status is finished
426+ return result .GetStatus () == "FINISHED"
427+ }
428+
429+ // waitForJobCompletion handles polling with retry and timeout.
430+ func (i * installationResource ) waitForJobCompletion (serverID , jobID string , ctx context.Context , resp * resource.CreateResponse ) error {
431+ // Create a constant backoff with a 30-second retry interval
432+ bo := backoff .NewConstantBackOff (30 * time .Second )
433+
434+ // Set the retry limit to 120 retries (60 minutes total)
435+ retryCount := 0
436+ maxRetries := 120
437+
438+ // Start polling and retrying
439+ for {
440+ if retryCount >= maxRetries {
441+ return errors .New ("timed out waiting for job to finish after 60 minutes" )
442+ }
443+
444+ // Call the function to check if the job is finished
445+ if i .isJobFinished (serverID , jobID , ctx , resp ) {
446+ // Job is finished, exit the loop
447+ return nil
448+ }
449+
450+ // Sleep for the backoff interval before retrying
451+ time .Sleep (bo .NextBackOff ())
452+ retryCount ++
453+ }
454+ }
455+
456+ func (i * installationResource ) syncResourceModelWithSDK (
457+ state * installationResourceModel ,
458+ job dedicatedserver.ServerJob ,
459+ ctx context.Context ,
460+ ) diag.Diagnostics {
461+ var diags diag.Diagnostics
462+ payload := job .GetPayload ()
463+ state .ID = types .StringValue (job .GetUuid ())
464+ state .Device = types .StringValue (payload .GetDevice ())
465+ state .OperatingSystemID = types .StringValue (payload .GetOperatingSystemId ())
466+ state .PowerCycle = types .BoolValue (payload .GetPowerCycle ())
467+ state .Timezone = types .StringValue (payload .GetTimezone ())
468+
469+ partitionAttributeTypes := map [string ]attr.Type {
470+ "filesystem" : types .StringType ,
471+ "mountpoint" : types .StringType ,
472+ "size" : types .StringType ,
473+ }
474+
475+ // Preparing and converting partitions into types.Object to store in the state
476+ var partitionsObjects []attr.Value
477+ for _ , p := range payload .GetPartitions () {
478+ partition := partitionsResourceModel {
479+ Filesystem : types .StringValue (p .GetFilesystem ()),
480+ Mountpoint : types .StringValue (p .GetMountpoint ()),
481+ Size : types .StringValue (p .GetSize ()),
482+ }
483+
484+ partitionObj , partitionDiags := types .ObjectValueFrom (ctx , partitionAttributeTypes , partition )
485+ diags .Append (partitionDiags ... )
486+ partitionsObjects = append (partitionsObjects , partitionObj )
487+ }
488+
489+ // Convert the slice of partition objects to a types.List and store it in the plan
490+ partitionsList , listDiags := types .ListValueFrom (ctx , types.ObjectType {AttrTypes : partitionAttributeTypes }, partitionsObjects )
491+ diags .Append (listDiags ... )
492+ state .Partitions = partitionsList
493+
494+ if state .Raid .IsNull () || state .Raid .IsUnknown () {
495+ state .Raid = types .ObjectNull (map [string ]attr.Type {
496+ "level" : types .Int64Type ,
497+ "number_of_disks" : types .Int64Type ,
498+ "type" : types .StringType ,
499+ })
500+ }
501+
502+ return diags
503+ }
0 commit comments