Skip to content

Commit 5752328

Browse files
committed
feat(instance_image): support block snapshots
1 parent ad60c88 commit 5752328

File tree

11 files changed

+3192
-43
lines changed

11 files changed

+3192
-43
lines changed

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ require (
2323
github.com/nats-io/jwt/v2 v2.7.2
2424
github.com/nats-io/nats.go v1.37.0
2525
github.com/robfig/cron/v3 v3.0.1
26-
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30.0.20241028153617-2a48843b5fcb
27-
github.com/stretchr/testify v1.10.0
28-
golang.org/x/crypto v0.29.0
26+
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30.0.20241129094524-023aa8142bc1
27+
github.com/stretchr/testify v1.9.0
28+
golang.org/x/crypto v0.28.0
2929
gopkg.in/dnaeon/go-vcr.v3 v3.2.0
3030
)
3131

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,10 @@ github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXq
242242
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
243243
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
244244
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
245-
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30.0.20241028153617-2a48843b5fcb h1:OsRpbw60numCy/+3FS7UhZzkdiTu6OZwq29bb4b3gNo=
246-
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30.0.20241028153617-2a48843b5fcb/go.mod h1:3jrRJM7638J+P33hKy9MBvfOBxNo8pEGNQQoIv65Ihg=
245+
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30.0.20241128142000-730918071fbc h1:XFg47wtUnYt+8Wbzsoke/Y9z8ozVtQiJYKUygENp0dY=
246+
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30.0.20241128142000-730918071fbc/go.mod h1:kAoejOVBg1E/aVAR6IwKWEmbLCEg2IXklzPAkxzAaXA=
247+
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30.0.20241129094524-023aa8142bc1 h1:0OKzyRfLH+dWSPOBvwbhNcBTbEiuNkv8mdYGev1+/1g=
248+
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30.0.20241129094524-023aa8142bc1/go.mod h1:kAoejOVBg1E/aVAR6IwKWEmbLCEg2IXklzPAkxzAaXA=
247249
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
248250
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
249251
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=

internal/services/block/snapshot.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ func ResourceBlockSnapshotDelete(ctx context.Context, d *schema.ResourceData, m
156156
return diag.FromErr(err)
157157
}
158158

159-
_, err = waitForBlockSnapshot(ctx, api, zone, id, d.Timeout(schema.TimeoutDelete))
159+
_, err = waitForBlockSnapshotToBeAvailable(ctx, api, zone, id, d.Timeout(schema.TimeoutDelete))
160160
if err != nil {
161161
return diag.FromErr(err)
162162
}

internal/services/block/waiters.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,22 @@ func waitForBlockSnapshot(ctx context.Context, blockAPI *block.API, zone scw.Zon
4040

4141
return snapshot, err
4242
}
43+
44+
func waitForBlockSnapshotToBeAvailable(ctx context.Context, blockAPI *block.API, zone scw.Zone, id string, timeout time.Duration) (*block.Snapshot, error) {
45+
retryInterval := defaultBlockRetryInterval
46+
if transport.DefaultWaitRetryInterval != nil {
47+
retryInterval = *transport.DefaultWaitRetryInterval
48+
}
49+
50+
terminalStatus := block.SnapshotStatusAvailable
51+
snapshot, err := blockAPI.WaitForSnapshot(&block.WaitForSnapshotRequest{
52+
Zone: zone,
53+
SnapshotID: id,
54+
RetryInterval: &retryInterval,
55+
Timeout: scw.TimeDurationPtr(timeout),
56+
57+
TerminalStatus: &terminalStatus,
58+
}, scw.WithContext(ctx))
59+
60+
return snapshot, err
61+
}

internal/services/instance/helpers_instance.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ func reachState(ctx context.Context, api *BlockAndInstanceAPI, zone scw.Zone, se
179179
if err != nil {
180180
return err
181181
}
182-
} else if volume.State != instance.VolumeServerStateAvailable {
182+
} else if volume.State != nil && *volume.State != instance.VolumeServerStateAvailable {
183183
_, err = api.WaitForVolume(&instance.WaitForVolumeRequest{
184184
Zone: zone,
185185
VolumeID: volume.ID,
@@ -407,22 +407,24 @@ func (ph *privateNICsHandler) get(key string) (interface{}, error) {
407407
}, nil
408408
}
409409

410-
func getSnapshotsFromIDs(ctx context.Context, snapIDs []interface{}, instanceAPI *instance.API) ([]*instance.GetSnapshotResponse, error) {
411-
snapResponses := []*instance.GetSnapshotResponse(nil)
410+
func getSnapshotsFromIDs(ctx context.Context, snapIDs []interface{}, api *BlockAndInstanceAPI) ([]*UnknownSnapshot, error) {
411+
snapResponses := []*UnknownSnapshot(nil)
412412
for _, snapID := range snapIDs {
413413
zone, id, err := zonal.ParseID(snapID.(string))
414414
if err != nil {
415415
return nil, err
416416
}
417-
snapshot, err := instanceAPI.GetSnapshot(&instance.GetSnapshotRequest{
417+
418+
unknownSnapshot, err := api.GetUnknownSnapshot(&GetUnknownSnapshotRequest{
418419
Zone: zone,
419420
SnapshotID: id,
420421
}, scw.WithContext(ctx))
421422
if err != nil {
422-
return snapResponses, fmt.Errorf("extra volumes : could not find snapshot with id %s", snapID)
423+
return nil, fmt.Errorf("extra volumes : could not find snapshot with id %s: %w", snapID, err)
423424
}
424-
snapResponses = append(snapResponses, snapshot)
425+
snapResponses = append(snapResponses, unknownSnapshot)
425426
}
427+
426428
return snapResponses, nil
427429
}
428430

internal/services/instance/helpers_instance_block.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ func (volume *UnknownVolume) IsAttached() bool {
8383
return volume.ServerID != nil && *volume.ServerID != ""
8484
}
8585

86+
type UnknownSnapshot struct {
87+
Zone scw.Zone
88+
ID string
89+
Name string
90+
VolumeType instance.VolumeVolumeType
91+
}
92+
8693
func (api *BlockAndInstanceAPI) GetUnknownVolume(req *GetUnknownVolumeRequest, opts ...scw.RequestOption) (*UnknownVolume, error) {
8794
getVolumeResponse, err := api.API.GetVolume(&instance.GetVolumeRequest{
8895
Zone: req.Zone,
@@ -161,6 +168,50 @@ func (api *BlockAndInstanceAPI) ResizeUnknownVolume(req *ResizeUnknownVolumeRequ
161168
return err
162169
}
163170

171+
type GetUnknownSnapshotRequest struct {
172+
Zone scw.Zone
173+
SnapshotID string
174+
}
175+
176+
func (api *BlockAndInstanceAPI) GetUnknownSnapshot(req *GetUnknownSnapshotRequest, opts ...scw.RequestOption) (*UnknownSnapshot, error) {
177+
getSnapshotResponse, err := api.GetSnapshot(&instance.GetSnapshotRequest{
178+
Zone: req.Zone,
179+
SnapshotID: req.SnapshotID,
180+
}, opts...)
181+
notFoundErr := &scw.ResourceNotFoundError{}
182+
if err != nil && !errors.As(err, &notFoundErr) {
183+
return nil, err
184+
}
185+
186+
if getSnapshotResponse != nil {
187+
snap := &UnknownSnapshot{
188+
Zone: getSnapshotResponse.Snapshot.Zone,
189+
ID: getSnapshotResponse.Snapshot.ID,
190+
Name: getSnapshotResponse.Snapshot.Name,
191+
VolumeType: getSnapshotResponse.Snapshot.VolumeType,
192+
}
193+
194+
return snap, nil
195+
}
196+
197+
blockSnapshot, err := api.blockAPI.GetSnapshot(&block.GetSnapshotRequest{
198+
Zone: req.Zone,
199+
SnapshotID: req.SnapshotID,
200+
}, opts...)
201+
if err != nil {
202+
return nil, err
203+
}
204+
205+
snap := &UnknownSnapshot{
206+
Zone: blockSnapshot.Zone,
207+
ID: blockSnapshot.ID,
208+
Name: blockSnapshot.Name,
209+
VolumeType: instance.VolumeVolumeTypeSbsSnapshot,
210+
}
211+
212+
return snap, nil
213+
}
214+
164215
// newAPIWithZone returns a new instance API and the zone for a Create request
165216
func instanceAndBlockAPIWithZone(d *schema.ResourceData, m interface{}) (*BlockAndInstanceAPI, scw.Zone, error) {
166217
instanceAPI := instance.NewAPI(meta.ExtractScwClient(m))

internal/services/instance/image.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ func ResourceImage() *schema.Resource {
176176
}
177177

178178
func ResourceInstanceImageCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
179-
instanceAPI, zone, err := newAPIWithZone(d, m)
179+
api, zone, err := instanceAndBlockAPIWithZone(d, m)
180180
if err != nil {
181181
return diag.FromErr(err)
182182
}
@@ -192,7 +192,7 @@ func ResourceInstanceImageCreate(ctx context.Context, d *schema.ResourceData, m
192192

193193
extraVolumesIDs, volumesExist := d.GetOk("additional_volume_ids")
194194
if volumesExist {
195-
snapResponses, err := getSnapshotsFromIDs(ctx, extraVolumesIDs.([]interface{}), instanceAPI)
195+
snapResponses, err := getSnapshotsFromIDs(ctx, extraVolumesIDs.([]interface{}), api)
196196
if err != nil {
197197
return diag.FromErr(err)
198198
}
@@ -206,14 +206,14 @@ func ResourceInstanceImageCreate(ctx context.Context, d *schema.ResourceData, m
206206
req.Public = types.ExpandBoolPtr(types.GetBool(d, "public"))
207207
}
208208

209-
res, err := instanceAPI.CreateImage(req, scw.WithContext(ctx))
209+
res, err := api.CreateImage(req, scw.WithContext(ctx))
210210
if err != nil {
211211
return diag.FromErr(err)
212212
}
213213

214214
d.SetId(zonal.NewIDString(zone, res.Image.ID))
215215

216-
_, err = instanceAPI.WaitForImage(&instanceSDK.WaitForImageRequest{
216+
_, err = api.WaitForImage(&instanceSDK.WaitForImageRequest{
217217
ImageID: res.Image.ID,
218218
Zone: zone,
219219
RetryInterval: transport.DefaultWaitRetryInterval,
@@ -262,7 +262,7 @@ func ResourceInstanceImageRead(ctx context.Context, d *schema.ResourceData, m in
262262
}
263263

264264
func ResourceInstanceImageUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
265-
instanceAPI, zone, id, err := NewAPIWithZoneAndID(m, d.Id())
265+
api, zone, id, err := instanceAndBlockAPIWithZoneAndID(m, d.Id())
266266
if err != nil {
267267
return diag.FromErr(err)
268268
}
@@ -283,7 +283,7 @@ func ResourceInstanceImageUpdate(ctx context.Context, d *schema.ResourceData, m
283283
}
284284
req.Tags = types.ExpandUpdatedStringsPtr(d.Get("tags"))
285285

286-
image, err := instanceAPI.GetImage(&instanceSDK.GetImageRequest{
286+
image, err := api.GetImage(&instanceSDK.GetImageRequest{
287287
Zone: zone,
288288
ImageID: id,
289289
}, scw.WithContext(ctx))
@@ -292,7 +292,7 @@ func ResourceInstanceImageUpdate(ctx context.Context, d *schema.ResourceData, m
292292
}
293293

294294
if d.HasChange("additional_volume_ids") {
295-
snapResponses, err := getSnapshotsFromIDs(ctx, d.Get("additional_volume_ids").([]interface{}), instanceAPI)
295+
snapResponses, err := getSnapshotsFromIDs(ctx, d.Get("additional_volume_ids").([]interface{}), api)
296296
if err != nil {
297297
return diag.FromErr(err)
298298
}
@@ -315,17 +315,17 @@ func ResourceInstanceImageUpdate(ctx context.Context, d *schema.ResourceData, m
315315
req.Arch = image.Image.Arch
316316
}
317317

318-
_, err = waitForImage(ctx, instanceAPI, zone, id, d.Timeout(schema.TimeoutUpdate))
318+
_, err = waitForImage(ctx, api.API, zone, id, d.Timeout(schema.TimeoutUpdate))
319319
if err != nil {
320320
return diag.FromErr(err)
321321
}
322322

323-
_, err = instanceAPI.UpdateImage(req, scw.WithContext(ctx))
323+
_, err = api.UpdateImage(req, scw.WithContext(ctx))
324324
if err != nil {
325325
return diag.FromErr(fmt.Errorf("couldn't update image: %s", err))
326326
}
327327

328-
_, err = waitForImage(ctx, instanceAPI, zone, id, d.Timeout(schema.TimeoutUpdate))
328+
_, err = waitForImage(ctx, api.API, zone, id, d.Timeout(schema.TimeoutUpdate))
329329
if err != nil {
330330
return diag.FromErr(err)
331331
}

internal/services/instance/image_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,89 @@ func TestAccImage_BlockVolume(t *testing.T) {
109109
})
110110
}
111111

112+
func TestAccImage_ExternalBlockVolume(t *testing.T) {
113+
tt := acctest.NewTestTools(t)
114+
defer tt.Cleanup()
115+
resource.ParallelTest(t, resource.TestCase{
116+
PreCheck: func() { acctest.PreCheck(t) },
117+
ProviderFactories: tt.ProviderFactories,
118+
CheckDestroy: resource.ComposeTestCheckFunc(
119+
isImageDestroyed(tt),
120+
isSnapshotDestroyed(tt),
121+
isVolumeDestroyed(tt),
122+
),
123+
Steps: []resource.TestStep{
124+
{
125+
Config: `
126+
resource "scaleway_block_volume" "main" {
127+
size_in_gb = 50
128+
iops = 5000
129+
}
130+
131+
resource "scaleway_block_volume" "additional1" {
132+
size_in_gb = 50
133+
iops = 5000
134+
}
135+
136+
resource "scaleway_block_snapshot" "main" {
137+
volume_id = scaleway_block_volume.main.id
138+
}
139+
140+
resource "scaleway_block_snapshot" "additional1" {
141+
volume_id = scaleway_block_volume.additional1.id
142+
}
143+
144+
resource "scaleway_instance_image" "main" {
145+
name = "tf-test-image-external-block-volume"
146+
root_volume_id = scaleway_block_snapshot.main.id
147+
additional_volume_ids = [scaleway_block_snapshot.additional1.id]
148+
}
149+
`,
150+
Check: resource.ComposeTestCheckFunc(
151+
instancechecks.DoesImageExists(tt, "scaleway_instance_image.main"),
152+
resource.TestCheckResourceAttrPair("scaleway_instance_image.main", "root_volume_id", "scaleway_block_snapshot.main", "id"),
153+
resource.TestCheckResourceAttr("scaleway_instance_image.main", "architecture", "x86_64"),
154+
resource.TestCheckResourceAttr("scaleway_instance_image.main", "additional_volume_ids.#", "1"),
155+
resource.TestCheckResourceAttrPair("scaleway_instance_image.main", "additional_volume_ids.0", "scaleway_block_snapshot.additional1", "id"),
156+
),
157+
},
158+
{
159+
Config: `
160+
resource "scaleway_block_volume" "main" {
161+
size_in_gb = 50
162+
iops = 5000
163+
}
164+
165+
resource "scaleway_block_volume" "additional1" {
166+
size_in_gb = 50
167+
iops = 5000
168+
}
169+
170+
resource "scaleway_block_snapshot" "main" {
171+
volume_id = scaleway_block_volume.main.id
172+
}
173+
174+
resource "scaleway_block_snapshot" "additional1" {
175+
volume_id = scaleway_block_volume.additional1.id
176+
}
177+
178+
resource "scaleway_instance_image" "main" {
179+
name = "tf-test-image-external-block-volume"
180+
root_volume_id = scaleway_block_snapshot.main.id
181+
additional_volume_ids = []
182+
}
183+
`,
184+
Check: resource.ComposeTestCheckFunc(
185+
instancechecks.DoesImageExists(tt, "scaleway_instance_image.main"),
186+
resource.TestCheckResourceAttrPair("scaleway_instance_image.main", "root_volume_id", "scaleway_block_snapshot.main", "id"),
187+
resource.TestCheckResourceAttr("scaleway_instance_image.main", "architecture", "x86_64"),
188+
resource.TestCheckResourceAttr("scaleway_instance_image.main", "additional_volume_ids.#", "0"),
189+
),
190+
},
191+
},
192+
})
193+
}
194+
112195
func TestAccImage_Server(t *testing.T) {
113196
tt := acctest.NewTestTools(t)
114197
defer tt.Cleanup()

internal/services/instance/server.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,7 @@ func ResourceInstanceServerRead(ctx context.Context, d *schema.ResourceData, m i
691691
}
692692

693693
var additionalVolumesIDs []string
694-
for i, volume := range sortVolumeServer(server.Volumes) {
694+
for i, serverVolume := range sortVolumeServer(server.Volumes) {
695695
if i == 0 {
696696
rootVolume := map[string]interface{}{}
697697

@@ -701,31 +701,31 @@ func ResourceInstanceServerRead(ctx context.Context, d *schema.ResourceData, m i
701701
}
702702

703703
vol, err := api.GetUnknownVolume(&GetUnknownVolumeRequest{
704-
VolumeID: volume.ID,
704+
VolumeID: serverVolume.ID,
705705
Zone: server.Zone,
706706
})
707707
if err != nil {
708-
return diag.FromErr(fmt.Errorf("failed to read instance volume %s: %w", volume.ID, err))
708+
return diag.FromErr(fmt.Errorf("failed to read instance volume %s: %w", serverVolume.ID, err))
709709
}
710710

711711
rootVolume["volume_id"] = zonal.NewID(zone, vol.ID).String()
712712
if vol.Size != nil {
713713
rootVolume["size_in_gb"] = int(uint64(*vol.Size) / gb)
714-
} else {
715-
rootVolume["size_in_gb"] = int(uint64(volume.Size) / gb)
714+
} else if serverVolume.Size != nil {
715+
rootVolume["size_in_gb"] = int(uint64(*serverVolume.Size) / gb)
716716
}
717717
if vol.IsBlockVolume() {
718718
rootVolume["sbs_iops"] = types.FlattenUint32Ptr(vol.Iops)
719719
}
720720
_, rootVolumeAttributeSet := d.GetOk("root_volume") // Related to https://github.com/hashicorp/terraform-plugin-sdk/issues/142
721721
rootVolume["delete_on_termination"] = d.Get("root_volume.0.delete_on_termination").(bool) || !rootVolumeAttributeSet
722-
rootVolume["volume_type"] = volume.VolumeType
723-
rootVolume["boot"] = volume.Boot
724-
rootVolume["name"] = volume.Name
722+
rootVolume["volume_type"] = serverVolume.VolumeType
723+
rootVolume["boot"] = serverVolume.Boot
724+
rootVolume["name"] = serverVolume.Name
725725

726726
_ = d.Set("root_volume", []map[string]interface{}{rootVolume})
727727
} else {
728-
additionalVolumesIDs = append(additionalVolumesIDs, zonal.NewID(zone, volume.ID).String())
728+
additionalVolumesIDs = append(additionalVolumesIDs, zonal.NewID(zone, serverVolume.ID).String())
729729
}
730730
}
731731

@@ -1144,8 +1144,8 @@ func instanceServerCanMigrate(api *instanceSDK.API, server *instanceSDK.Server,
11441144
var localVolumeSize scw.Size
11451145

11461146
for _, volume := range server.Volumes {
1147-
if volume.VolumeType == instanceSDK.VolumeServerVolumeTypeLSSD {
1148-
localVolumeSize += volume.Size
1147+
if volume.VolumeType == instanceSDK.VolumeServerVolumeTypeLSSD && volume.Size != nil {
1148+
localVolumeSize += *volume.Size
11491149
}
11501150
}
11511151

0 commit comments

Comments
 (0)