Skip to content

Commit dacf2a0

Browse files
committed
feat(instance_image): support block snapshots
1 parent 50aef3f commit dacf2a0

File tree

11 files changed

+3196
-47
lines changed

11 files changed

+3196
-47
lines changed

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ 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
26+
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30.0.20241129094524-023aa8142bc1
2727
github.com/stretchr/testify v1.9.0
2828
golang.org/x/crypto v0.28.0
2929
gopkg.in/dnaeon/go-vcr.v3 v3.2.0
@@ -108,9 +108,9 @@ require (
108108
go.opentelemetry.io/otel/trace v1.22.0 // indirect
109109
golang.org/x/mod v0.21.0 // indirect
110110
golang.org/x/net v0.28.0 // indirect
111-
golang.org/x/sync v0.8.0 // indirect
111+
golang.org/x/sync v0.9.0 // indirect
112112
golang.org/x/sys v0.26.0 // indirect
113-
golang.org/x/text v0.19.0 // indirect
113+
golang.org/x/text v0.20.0 // indirect
114114
golang.org/x/time v0.3.0 // indirect
115115
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
116116
google.golang.org/appengine v1.6.8 // indirect

go.sum

Lines changed: 8 additions & 6 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=
@@ -330,8 +332,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
330332
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
331333
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
332334
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
333-
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
334-
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
335+
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
336+
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
335337
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
336338
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
337339
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -361,8 +363,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
361363
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
362364
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
363365
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
364-
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
365-
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
366+
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
367+
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
366368
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
367369
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
368370
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

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()

0 commit comments

Comments
 (0)