Skip to content

Commit c539c20

Browse files
authored
feat(instance): migrate server when changing type (#2044)
* feat(instance): migrate server when changing type * check local volume size during plan * add 'replace_on_type_change' option * lint * update doc
1 parent 5ee169a commit c539c20

File tree

7 files changed

+6393
-9
lines changed

7 files changed

+6393
-9
lines changed

docs/resources/instance_server.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,10 @@ The following arguments are supported:
169169

170170
- `type` - (Required) The commercial type of the server.
171171
You find all the available types on the [pricing page](https://www.scaleway.com/en/pricing/).
172-
Updates to this field will recreate a new resource.
172+
Updates to this field will migrate the server, local storage constraint must be respected. [More info](https://www.scaleway.com/en/docs/compute/instances/api-cli/migrating-instances/).
173+
Use `replace_on_type_change` to trigger replacement instead of migration.
174+
175+
~> **Important:** If `type` change and migration occurs, the server will be stopped and changed backed to its original state. It will be started again if it was running.
173176

174177
- `image` - (Optional) The UUID or the label of the base image used by the server. You can use [this endpoint](https://api-marketplace.scaleway.com/images?page=1&per_page=100)
175178
to find either the right `label` or the right local image `ID` for a given `type`. Optional when creating an instance with an existing root volume.
@@ -227,6 +230,8 @@ attached to the server. Updates to this field will trigger a stop/start of the s
227230

228231
- `boot_type` - The boot Type of the server. Possible values are: `local`, `bootscript` or `rescue`.
229232

233+
- `replace_on_type_change` - (Defaults to false) If true, the server will be replaced if `type` is changed. Otherwise, the server will migrate.
234+
230235
- `bootscript_id` - The ID of the bootscript to use (set boot_type to `bootscript`).
231236

232237
- `zone` - (Defaults to [provider](../index.md#zone) `zone`) The [zone](../guides/regions_and_zones.md#zones) in which the server should be created.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ require (
1616
github.com/nats-io/jwt/v2 v2.4.1
1717
github.com/nats-io/nats.go v1.27.1
1818
github.com/robfig/cron/v3 v3.0.1
19-
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.18.0.20230707094537-b73e88c770db
19+
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.18.0.20230710160943-efc922e1f661
2020
github.com/stretchr/testify v1.8.4
2121
)
2222

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
173173
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
174174
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
175175
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
176-
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.18.0.20230707094537-b73e88c770db h1:6clfb6rrE1ZVHlwoFTJmnf8OSR1EbuH7vQ0S0kzR0cA=
177-
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.18.0.20230707094537-b73e88c770db/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
176+
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.18.0.20230710160943-efc922e1f661 h1:/FNBbwxEzMriXGudqXTGOg0nIeNO+0xpqXChZk6auBE=
177+
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.18.0.20230710160943-efc922e1f661/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
178178
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
179179
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
180180
github.com/skeema/knownhosts v1.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0=

scaleway/resource_instance_server.go

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/google/go-cmp/cmp"
1212
"github.com/hashicorp/terraform-plugin-log/tflog"
1313
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
14+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
1415
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1516
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1617
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
@@ -54,10 +55,15 @@ func resourceScalewayInstanceServer() *schema.Resource {
5455
"type": {
5556
Type: schema.TypeString,
5657
Required: true,
57-
ForceNew: true,
5858
Description: "The instance type of the server", // TODO: link to scaleway pricing in the doc
5959
DiffSuppressFunc: diffSuppressFuncIgnoreCase,
6060
},
61+
"replace_on_type_change": {
62+
Type: schema.TypeBool,
63+
Optional: true,
64+
Default: false,
65+
Description: "Delete and re-create server if type change",
66+
},
6167
"tags": {
6268
Type: schema.TypeList,
6369
Elem: &schema.Schema{
@@ -271,10 +277,13 @@ func resourceScalewayInstanceServer() *schema.Resource {
271277
"organization_id": organizationIDSchema(),
272278
"project_id": projectIDSchema(),
273279
},
274-
CustomizeDiff: customizeDiffLocalityCheck(
275-
"placement_group_id",
276-
"additional_volume_ids.#",
277-
"ip_id",
280+
CustomizeDiff: customdiff.All(
281+
customizeDiffLocalityCheck(
282+
"placement_group_id",
283+
"additional_volume_ids.#",
284+
"ip_id",
285+
),
286+
customDiffInstanceServerType,
278287
),
279288
}
280289
}
@@ -943,6 +952,13 @@ func resourceScalewayInstanceServerUpdate(ctx context.Context, d *schema.Resourc
943952
return diag.FromErr(err)
944953
}
945954

955+
if d.HasChange("type") {
956+
err := resourceScalewayInstanceServerMigrate(ctx, d, instanceAPI, zone, id)
957+
if err != nil {
958+
return diag.FromErr(err)
959+
}
960+
}
961+
946962
return append(warnings, resourceScalewayInstanceServerRead(ctx, d, meta)...)
947963
}
948964

@@ -1018,3 +1034,94 @@ func resourceScalewayInstanceServerDelete(ctx context.Context, d *schema.Resourc
10181034

10191035
return nil
10201036
}
1037+
1038+
func instanceServerCanMigrate(api *instance.API, server *instance.Server, requestedType string) error {
1039+
var localVolumeSize scw.Size
1040+
1041+
for _, volume := range server.Volumes {
1042+
if volume.VolumeType == instance.VolumeServerVolumeTypeLSSD {
1043+
localVolumeSize += volume.Size
1044+
}
1045+
}
1046+
1047+
serverType, err := api.GetServerType(&instance.GetServerTypeRequest{
1048+
Zone: server.Zone,
1049+
Name: requestedType,
1050+
})
1051+
if err != nil {
1052+
return err
1053+
}
1054+
1055+
if serverType.VolumesConstraint != nil &&
1056+
(localVolumeSize > serverType.VolumesConstraint.MaxSize) ||
1057+
(localVolumeSize < serverType.VolumesConstraint.MinSize) {
1058+
return fmt.Errorf("local volume total size does not respect type constraint, expected beteween (%dGB, %dGB), got %sGB",
1059+
serverType.VolumesConstraint.MinSize/scw.GB,
1060+
serverType.VolumesConstraint.MaxSize/scw.GB,
1061+
localVolumeSize/scw.GB)
1062+
}
1063+
1064+
return nil
1065+
}
1066+
1067+
func customDiffInstanceServerType(_ context.Context, diff *schema.ResourceDiff, meta interface{}) error {
1068+
if !diff.HasChange("type") || diff.Id() == "" {
1069+
return nil
1070+
}
1071+
1072+
if diff.Get("replace_on_type_change").(bool) {
1073+
return diff.ForceNew("type")
1074+
}
1075+
1076+
instanceAPI, zone, id, err := instanceAPIWithZoneAndID(meta, diff.Id())
1077+
if err != nil {
1078+
return err
1079+
}
1080+
1081+
_, newValue := diff.GetChange("type")
1082+
newType := newValue.(string)
1083+
1084+
resp, err := instanceAPI.GetServer(&instance.GetServerRequest{
1085+
Zone: zone,
1086+
ServerID: id,
1087+
})
1088+
if err != nil {
1089+
return fmt.Errorf("failed to check server type change: %w", err)
1090+
}
1091+
1092+
err = instanceServerCanMigrate(instanceAPI, resp.Server, newType)
1093+
if err != nil {
1094+
return fmt.Errorf("cannot change server type: %w", err)
1095+
}
1096+
1097+
return nil
1098+
}
1099+
1100+
func resourceScalewayInstanceServerMigrate(ctx context.Context, d *schema.ResourceData, instanceAPI *instance.API, zone scw.Zone, id string) error {
1101+
server, err := waitForInstanceServer(ctx, instanceAPI, zone, id, d.Timeout(schema.TimeoutUpdate))
1102+
if err != nil {
1103+
return fmt.Errorf("failed to wait for server before changing server type: %w", err)
1104+
}
1105+
beginningState := server.State
1106+
1107+
err = reachState(ctx, instanceAPI, zone, id, instance.ServerStateStopped)
1108+
if err != nil {
1109+
return fmt.Errorf("failed to stop server before changing server type: %w", err)
1110+
}
1111+
1112+
_, err = instanceAPI.UpdateServer(&instance.UpdateServerRequest{
1113+
Zone: zone,
1114+
ServerID: id,
1115+
CommercialType: expandStringPtr(d.Get("type")),
1116+
})
1117+
if err != nil {
1118+
return fmt.Errorf("failed to change server type server")
1119+
}
1120+
1121+
err = reachState(ctx, instanceAPI, zone, id, beginningState)
1122+
if err != nil {
1123+
return fmt.Errorf("failed to start server after changing server type: %w", err)
1124+
}
1125+
1126+
return nil
1127+
}

scaleway/resource_instance_server_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package scaleway
22

33
import (
44
"fmt"
5+
"regexp"
56
"strings"
67
"testing"
78

@@ -335,6 +336,7 @@ func TestAccScalewayInstanceServer_Basic(t *testing.T) {
335336
name = "test"
336337
image = "${data.scaleway_marketplace_image.ubuntu.id}"
337338
type = "DEV1-S"
339+
replace_on_type_change = true
338340
339341
tags = [ "terraform-test", "scaleway_instance_server", "basic" ]
340342
}`,
@@ -1316,3 +1318,89 @@ func TestAccScalewayInstanceServer_PrivateNetwork(t *testing.T) {
13161318
},
13171319
})
13181320
}
1321+
1322+
func TestAccScalewayInstanceServer_Migrate(t *testing.T) {
1323+
tt := NewTestTools(t)
1324+
defer tt.Cleanup()
1325+
resource.ParallelTest(t, resource.TestCase{
1326+
PreCheck: func() { testAccPreCheck(t) },
1327+
ProviderFactories: tt.ProviderFactories,
1328+
CheckDestroy: testAccCheckScalewayInstanceServerDestroy(tt),
1329+
Steps: []resource.TestStep{
1330+
{
1331+
Config: `
1332+
resource "scaleway_instance_server" "main" {
1333+
image = "ubuntu_jammy"
1334+
type = "PRO2-XXS"
1335+
1336+
}`,
1337+
Check: resource.ComposeTestCheckFunc(
1338+
testAccCheckScalewayInstancePrivateNICsExists(tt, "scaleway_instance_server.main"),
1339+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "type", "PRO2-XXS"),
1340+
),
1341+
},
1342+
{
1343+
Config: `
1344+
resource "scaleway_instance_server" "main" {
1345+
image = "ubuntu_jammy"
1346+
type = "PRO2-XS"
1347+
1348+
}`,
1349+
Check: resource.ComposeTestCheckFunc(
1350+
testAccCheckScalewayInstancePrivateNICsExists(tt, "scaleway_instance_server.main"),
1351+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "type", "PRO2-XS"),
1352+
),
1353+
},
1354+
{
1355+
Config: `
1356+
resource "scaleway_instance_server" "main" {
1357+
image = "ubuntu_jammy"
1358+
type = "PRO2-XXS"
1359+
1360+
}`,
1361+
Check: resource.ComposeTestCheckFunc(
1362+
testAccCheckScalewayInstancePrivateNICsExists(tt, "scaleway_instance_server.main"),
1363+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "type", "PRO2-XXS"),
1364+
),
1365+
},
1366+
},
1367+
})
1368+
}
1369+
1370+
func TestAccScalewayInstanceServer_MigrateInvalidLocalVolumeSize(t *testing.T) {
1371+
tt := NewTestTools(t)
1372+
defer tt.Cleanup()
1373+
resource.ParallelTest(t, resource.TestCase{
1374+
PreCheck: func() { testAccPreCheck(t) },
1375+
ProviderFactories: tt.ProviderFactories,
1376+
CheckDestroy: testAccCheckScalewayInstanceServerDestroy(tt),
1377+
Steps: []resource.TestStep{
1378+
{
1379+
Config: `
1380+
resource "scaleway_instance_server" "main" {
1381+
image = "ubuntu_jammy"
1382+
type = "DEV1-L"
1383+
1384+
}`,
1385+
Check: resource.ComposeTestCheckFunc(
1386+
testAccCheckScalewayInstancePrivateNICsExists(tt, "scaleway_instance_server.main"),
1387+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "type", "DEV1-L"),
1388+
),
1389+
},
1390+
{
1391+
Config: `
1392+
resource "scaleway_instance_server" "main" {
1393+
image = "ubuntu_jammy"
1394+
type = "DEV1-S"
1395+
1396+
}`,
1397+
Check: resource.ComposeTestCheckFunc(
1398+
testAccCheckScalewayInstancePrivateNICsExists(tt, "scaleway_instance_server.main"),
1399+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "type", "DEV1-S"),
1400+
),
1401+
ExpectError: regexp.MustCompile("cannot change server type"),
1402+
PlanOnly: true,
1403+
},
1404+
},
1405+
})
1406+
}

scaleway/testdata/instance-server-migrate-invalid-local-volume-size.cassette.yaml

Lines changed: 1577 additions & 0 deletions
Large diffs are not rendered by default.

scaleway/testdata/instance-server-migrate.cassette.yaml

Lines changed: 4607 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)