Skip to content

Commit 0a8dc32

Browse files
authored
feat(instance): stop rebooting and stop/starting the instance (#708)
1 parent 113e11f commit 0a8dc32

16 files changed

+94613
-5664
lines changed

docs/resources/instance_server.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ to find either the right `label` or the right local image `ID` for a given `type
143143
- `placement_group_id` - (Optional) The [placement group](https://developers.scaleway.com/en/products/instance/api/#placement-groups-d8f653) the server is attached to.
144144

145145

146-
~> **Important:** Updates to `placement_group_id` may trigger a stop/start of the server.
146+
~> **Important:** When updating `placement_group_id` the `state` must be set to `stopped`, otherwise it will fail.
147147

148148
- `root_volume` - (Optional) Root [volume](https://developers.scaleway.com/en/products/instance/api/#volumes-7e8a39) attached to the server on creation.
149149
- `size_in_gb` - (Required) Size of the root volume in gigabytes.
@@ -152,12 +152,14 @@ to find either the right `label` or the right local image `ID` for a given `type
152152
Updates to this field will recreate a new resource.
153153
- `delete_on_termination` - (Defaults to `true`) Forces deletion of the root volume on instance termination.
154154

155-
~> **Important:** Updates to `root_volume.size_in_gb` will trigger a stop/start of the server.
155+
~> **Important:** Updates to `root_volume.size_in_gb` will be ignored after the creation of the server.
156156

157157
- `additional_volume_ids` - (Optional) The [additional volumes](https://developers.scaleway.com/en/products/instance/api/#volumes-7e8a39)
158158
attached to the server. Updates to this field will trigger a stop/start of the server.
159159

160-
~> **Important:** If this field contains local volumes, updates will trigger a stop/start of the server.
160+
~> **Important:** If this field contains local volumes, the `state` must be set to `stopped`, otherwise it will fail.
161+
162+
~> **Important:** If this field contains local volumes, you have to first detach them, in one apply, and then delete the volume in another apply.
161163

162164
- `enable_ipv6` - (Defaults to `false`) Determines if IPv6 is enabled for the server.
163165

scaleway/helpers.go

-35
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
package scaleway
22

33
import (
4-
"bytes"
54
"encoding/json"
65
"fmt"
76
"net"
87
"net/http"
98
"regexp"
109
"strings"
11-
"text/template"
1210
"time"
1311

14-
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1512
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
16-
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
1713
"github.com/scaleway/scaleway-sdk-go/namegenerator"
1814
"github.com/scaleway/scaleway-sdk-go/scw"
1915
"golang.org/x/xerrors"
@@ -302,37 +298,6 @@ func isUUID(s string) bool {
302298
return UUIDRegex.MatchString(s)
303299
}
304300

305-
// newTemplateFunc takes a go template string and returns a function that can be called to execute template.
306-
func newTemplateFunc(tplStr string) func(data interface{}) string {
307-
t := template.Must(template.New("tpl").Parse(tplStr))
308-
return func(tplParams interface{}) string {
309-
buffer := bytes.Buffer{}
310-
err := t.Execute(&buffer, tplParams)
311-
if err != nil {
312-
panic(err) // lintignore:R009
313-
}
314-
return buffer.String()
315-
}
316-
}
317-
318-
// testAccGetResourceAttr can be used in acceptance tests to extract value from state and store it in dest
319-
func testAccGetResourceAttr(resourceName string, attrName string, dest *string) resource.TestCheckFunc {
320-
return func(state *terraform.State) error {
321-
r, exist := state.RootModule().Resources[resourceName]
322-
if !exist {
323-
return fmt.Errorf("unknown ressource %s", resourceName)
324-
}
325-
326-
a, exist := r.Primary.Attributes[attrName]
327-
if !exist {
328-
return fmt.Errorf("unknown ressource %s", resourceName)
329-
}
330-
331-
*dest = a
332-
return nil
333-
}
334-
}
335-
336301
func flattenTime(date *time.Time) interface{} {
337302
if date != nil {
338303
return date.Format(time.RFC3339)

scaleway/resource_instance_server.go

+43-26
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,6 @@ func resourceScalewayInstanceServer() *schema.Resource {
152152
Computed: true,
153153
Description: "The IPv6 prefix length routed to the server.",
154154
},
155-
"disable_dynamic_ip": {
156-
Type: schema.TypeBool,
157-
Optional: true,
158-
Default: false,
159-
Description: "Disable dynamic IP on the server",
160-
},
161155
"enable_dynamic_ip": {
162156
Type: schema.TypeBool,
163157
Optional: true,
@@ -494,8 +488,10 @@ func resourceScalewayInstanceServerUpdate(ctx context.Context, d *schema.Resourc
494488
return diag.FromErr(err)
495489
}
496490

497-
// This variable will be set to true if any state change requires a server reboot.
498-
var forceReboot bool
491+
wantedState := d.Get("state").(string)
492+
isStopped := wantedState == InstanceServerStateStopped
493+
494+
var warnings diag.Diagnostics
499495

500496
////
501497
// Construct UpdateServerRequest
@@ -534,24 +530,36 @@ func resourceScalewayInstanceServerUpdate(ctx context.Context, d *schema.Resourc
534530
volumes["0"] = &instance.VolumeTemplate{ID: expandZonedID(d.Get("root_volume.0.volume_id")).ID, Name: newRandomName("vol")} // name is ignored by the API, any name will work here
535531

536532
for i, volumeID := range raw.([]interface{}) {
537-
// TODO: this will be refactored soon, before next release
538-
// in the meantime it will throw an error if the volume is already attached somewhere
533+
// local volumes can only be added when the instance is stopped
534+
if !isStopped {
535+
volumeResp, err := instanceAPI.GetVolume(&instance.GetVolumeRequest{
536+
Zone: zone,
537+
VolumeID: expandZonedID(volumeID).ID,
538+
})
539+
if err != nil {
540+
return diag.FromErr(err)
541+
}
542+
if volumeResp.Volume.VolumeType == instance.VolumeVolumeTypeLSSD {
543+
return diag.FromErr(fmt.Errorf("instance must be stopped to change local volumes"))
544+
}
545+
}
539546
volumes[strconv.Itoa(i+1)] = &instance.VolumeTemplate{
540547
ID: expandZonedID(volumeID).ID,
541548
Name: newRandomName("vol"), // name is ignored by the API, any name will work here
542549
}
543550
}
544551

545552
updateRequest.Volumes = &volumes
546-
forceReboot = true
547553
}
548554

549555
if d.HasChange("placement_group_id") {
550556
placementGroupID := expandZonedID(d.Get("placement_group_id")).ID
551557
if placementGroupID == "" {
552558
updateRequest.PlacementGroup = &instance.NullableStringValue{Null: true}
553559
} else {
554-
forceReboot = true
560+
if !isStopped {
561+
return diag.FromErr(fmt.Errorf("instance must be stopped to change placement group"))
562+
}
555563
updateRequest.PlacementGroup = &instance.NullableStringValue{Value: placementGroupID}
556564
}
557565
}
@@ -597,12 +605,22 @@ func resourceScalewayInstanceServerUpdate(ctx context.Context, d *schema.Resourc
597605
if d.HasChanges("boot_type") {
598606
bootType := instance.BootType(d.Get("boot_type").(string))
599607
updateRequest.BootType = &bootType
600-
forceReboot = true
608+
if !isStopped {
609+
warnings = append(warnings, diag.Diagnostic{
610+
Severity: diag.Warning,
611+
Summary: "instance may need to be rebooted to use the new boot type",
612+
})
613+
}
601614
}
602615

603616
if d.HasChanges("bootscript_id") {
604617
updateRequest.Bootscript = expandStringPtr(d.Get("bootscript_id").(string))
605-
forceReboot = true
618+
if !isStopped {
619+
warnings = append(warnings, diag.Diagnostic{
620+
Severity: diag.Warning,
621+
Summary: "instance may need to be rebooted to use the new bootscript",
622+
})
623+
}
606624
}
607625

608626
////
@@ -626,7 +644,12 @@ func resourceScalewayInstanceServerUpdate(ctx context.Context, d *schema.Resourc
626644
// cloud init script is set in user data
627645
if cloudInit, ok := d.GetOk("cloud_init"); ok {
628646
userDataRequests.UserData["cloud-init"] = bytes.NewBufferString(cloudInit.(string))
629-
forceReboot = true // instance must reboot when cloud init script change
647+
if !isStopped {
648+
warnings = append(warnings, diag.Diagnostic{
649+
Severity: diag.Warning,
650+
Summary: "instance may need to be rebooted to use the new cloud init config",
651+
})
652+
}
630653
}
631654

632655
err := instanceAPI.SetAllServerUserData(userDataRequests)
@@ -639,29 +662,23 @@ func resourceScalewayInstanceServerUpdate(ctx context.Context, d *schema.Resourc
639662
// Apply changes
640663
////
641664

642-
if forceReboot {
643-
err = reachState(ctx, instanceAPI, zone, ID, InstanceServerStateStopped)
644-
if err != nil {
645-
return diag.FromErr(err)
646-
}
647-
}
648-
_, err = instanceAPI.UpdateServer(updateRequest, scw.WithContext(ctx))
665+
targetState, err := serverStateExpand(d.Get("state").(string))
649666
if err != nil {
650667
return diag.FromErr(err)
651668
}
652669

653-
targetState, err := serverStateExpand(d.Get("state").(string))
670+
// reach expected state
671+
err = reachState(ctx, instanceAPI, zone, ID, targetState)
654672
if err != nil {
655673
return diag.FromErr(err)
656674
}
657675

658-
// reach expected state
659-
err = reachState(ctx, instanceAPI, zone, ID, targetState)
676+
_, err = instanceAPI.UpdateServer(updateRequest)
660677
if err != nil {
661678
return diag.FromErr(err)
662679
}
663680

664-
return resourceScalewayInstanceServerRead(ctx, d, m)
681+
return append(warnings, resourceScalewayInstanceServerRead(ctx, d, m)...)
665682
}
666683

667684
func resourceScalewayInstanceServerDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {

0 commit comments

Comments
 (0)