Skip to content

Commit 35c751c

Browse files
authored
feat(instance_server): support sbs_volume as root volume (#2641)
* feat(instance_server): support sbs_volume as root volume * support IOPS configuration * record tests * record instance tests * record ipam tests * record lb test * record vpcgw tests * lint * record vpc route cassette * feat: warn on IOPS update fail * add doc * add iops update test
1 parent 86ae5dc commit 35c751c

File tree

60 files changed

+54183
-34219
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+54183
-34219
lines changed

docs/resources/instance_server.md

+16-1
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,20 @@ resource "scaleway_instance_server" "from_snapshot" {
163163
}
164164
```
165165

166+
#### Using Scaleway Block Storage (SBS) volume
167+
168+
```terraform
169+
resource "scaleway_instance_server" "server" {
170+
type = "PLAY2-MICRO"
171+
image = "ubuntu_jammy"
172+
root_volume {
173+
volume_type = "sbs_volume"
174+
sbs_iops = 15000
175+
size_in_gb = 50
176+
}
177+
}
178+
```
179+
166180
## Argument Reference
167181

168182
The following arguments are supported:
@@ -198,8 +212,9 @@ To retrieve more information by label please use: ```scw marketplace image get l
198212
To find the right size use [this endpoint](https://www.scaleway.com/en/developers/api/instance/#path-instances-list-all-instances) and
199213
check the `volumes_constraint.{min|max}_size` (in bytes) for your `commercial_type`.
200214
Updates to this field will recreate a new resource.
201-
- `volume_type` - (Optional) Volume type of root volume, can be `b_ssd` or `l_ssd`, default value depends on server type
215+
- `volume_type` - (Optional) Volume type of root volume, can be `b_ssd`, `l_ssd` or `sbs_volume`, default value depends on server type
202216
- `delete_on_termination` - (Defaults to `true`) Forces deletion of the root volume on instance termination.
217+
- `sbs_iops` - (Optional) Choose IOPS of your sbs volume, has to be used with `sbs_volume` for root volume type.
203218

204219
~> **Important:** Updates to `root_volume.size_in_gb` will be ignored after the creation of the server.
205220

internal/services/instance/helpers_instance_block.go

+15
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
77
block "github.com/scaleway/scaleway-sdk-go/api/block/v1alpha1"
88
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
9+
"github.com/scaleway/scaleway-sdk-go/api/marketplace/v2"
910
"github.com/scaleway/scaleway-sdk-go/scw"
1011
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality/zonal"
1112
"github.com/scaleway/terraform-provider-scaleway/v2/internal/meta"
@@ -29,6 +30,10 @@ type UnknownVolume struct {
2930
ServerID *string
3031
Boot *bool
3132

33+
// Iops is set for Block volume only, use IsBlockVolume
34+
// Can be nil if not available in the Block API.
35+
Iops *uint32
36+
3237
InstanceVolumeType instance.VolumeVolumeType
3338
}
3439

@@ -112,6 +117,9 @@ func (api *BlockAndInstanceAPI) GetUnknownVolume(req *GetUnknownVolumeRequest, o
112117
Size: &blockVolume.Size,
113118
InstanceVolumeType: instance.VolumeVolumeTypeSbsVolume,
114119
}
120+
if blockVolume.Specs != nil {
121+
vol.Iops = blockVolume.Specs.PerfIops
122+
}
115123
for _, ref := range blockVolume.References {
116124
if ref.ProductResourceType == "instance_server" {
117125
vol.ServerID = &ref.ProductResourceID
@@ -152,3 +160,10 @@ func instanceAndBlockAPIWithZoneAndID(m interface{}, zonedID string) (*BlockAndI
152160
blockAPI: blockAPI,
153161
}, zone, ID, nil
154162
}
163+
164+
func volumeTypeToMarketplaceFilter(volumeType any) marketplace.LocalImageType {
165+
if volumeType != nil && instance.VolumeVolumeType(volumeType.(string)) == instance.VolumeVolumeTypeSbsVolume {
166+
return marketplace.LocalImageTypeInstanceSbs
167+
}
168+
return marketplace.LocalImageTypeInstanceLocal
169+
}

internal/services/instance/server.go

+82-8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
1818
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1919
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
20+
block "github.com/scaleway/scaleway-sdk-go/api/block/v1alpha1"
2021
instanceSDK "github.com/scaleway/scaleway-sdk-go/api/instance/v1"
2122
"github.com/scaleway/scaleway-sdk-go/api/marketplace/v2"
2223
"github.com/scaleway/scaleway-sdk-go/scw"
@@ -150,6 +151,12 @@ func ResourceServer() *schema.Resource {
150151
Description: "Volume ID of the root volume",
151152
ExactlyOneOf: []string{"image", "root_volume.0.volume_id"},
152153
},
154+
"sbs_iops": {
155+
Type: schema.TypeInt,
156+
Computed: true,
157+
Optional: true,
158+
Description: "SBS Volume IOPS, only with volume_type as sbs_volume",
159+
},
153160
},
154161
},
155162
},
@@ -374,7 +381,7 @@ func ResourceInstanceServerCreate(ctx context.Context, d *schema.ResourceData, m
374381
CommercialType: commercialType,
375382
Zone: zone,
376383
ImageLabel: imageLabel,
377-
Type: marketplace.LocalImageTypeInstanceLocal,
384+
Type: volumeTypeToMarketplaceFilter(d.Get("root_volume.0.volume_type")),
378385
})
379386
if err != nil {
380387
return diag.FromErr(fmt.Errorf("could not get image '%s': %s", zonal.NewID(zone, imageLabel), err))
@@ -466,6 +473,18 @@ func ResourceInstanceServerCreate(ctx context.Context, d *schema.ResourceData, m
466473
return diag.FromErr(err)
467474
}
468475

476+
////
477+
// Configure Block Volume
478+
////
479+
var diags diag.Diagnostics
480+
481+
if iops, ok := d.GetOk("root_volume.0.sbs_iops"); ok {
482+
updateDiags := ResourceInstanceServerUpdateRootVolumeIOPS(ctx, api, zone, res.Server.ID, types.ExpandUint32Ptr(iops))
483+
if len(updateDiags) > 0 {
484+
diags = append(diags, updateDiags...)
485+
}
486+
}
487+
469488
////
470489
// Set user data
471490
////
@@ -544,7 +563,7 @@ func ResourceInstanceServerCreate(ctx context.Context, d *schema.ResourceData, m
544563
}
545564
}
546565

547-
return ResourceInstanceServerRead(ctx, d, m)
566+
return append(diags, ResourceInstanceServerRead(ctx, d, m)...)
548567
}
549568

550569
func errorCheck(err error, message string) bool {
@@ -553,12 +572,12 @@ func errorCheck(err error, message string) bool {
553572

554573
//gocyclo:ignore
555574
func ResourceInstanceServerRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
556-
instanceAPI, zone, id, err := NewAPIWithZoneAndID(m, d.Id())
575+
api, zone, id, err := instanceAndBlockAPIWithZoneAndID(m, d.Id())
557576
if err != nil {
558577
return diag.FromErr(err)
559578
}
560579

561-
server, err := waitForServer(ctx, instanceAPI, zone, id, d.Timeout(schema.TimeoutRead))
580+
server, err := waitForServer(ctx, api.API, zone, id, d.Timeout(schema.TimeoutRead))
562581
if err != nil {
563582
if errorCheck(err, "is not found") {
564583
log.Printf("[WARN] instance %s not found droping from state", d.Id())
@@ -670,8 +689,23 @@ func ResourceInstanceServerRead(ctx context.Context, d *schema.ResourceData, m i
670689
rootVolume = vs[0]
671690
}
672691

673-
rootVolume["volume_id"] = zonal.NewID(zone, volume.ID).String()
674-
rootVolume["size_in_gb"] = int(uint64(volume.Size) / gb)
692+
vol, err := api.GetUnknownVolume(&GetUnknownVolumeRequest{
693+
VolumeID: volume.ID,
694+
Zone: volume.Zone,
695+
})
696+
if err != nil {
697+
return diag.FromErr(fmt.Errorf("failed to read instance volume %s: %w", volume.ID, err))
698+
}
699+
700+
rootVolume["volume_id"] = zonal.NewID(zone, vol.ID).String()
701+
if vol.Size != nil {
702+
rootVolume["size_in_gb"] = int(uint64(*vol.Size) / gb)
703+
} else {
704+
rootVolume["size_in_gb"] = int(uint64(volume.Size) / gb)
705+
}
706+
if vol.IsBlockVolume() {
707+
rootVolume["sbs_iops"] = types.FlattenUint32Ptr(vol.Iops)
708+
}
675709
_, rootVolumeAttributeSet := d.GetOk("root_volume") // Related to https://github.com/hashicorp/terraform-plugin-sdk/issues/142
676710
rootVolume["delete_on_termination"] = d.Get("root_volume.0.delete_on_termination").(bool) || !rootVolumeAttributeSet
677711
rootVolume["volume_type"] = volume.VolumeType
@@ -691,7 +725,7 @@ func ResourceInstanceServerRead(ctx context.Context, d *schema.ResourceData, m i
691725
////
692726
// Read server user data
693727
////
694-
allUserData, _ := instanceAPI.GetAllServerUserData(&instanceSDK.GetAllServerUserDataRequest{
728+
allUserData, _ := api.GetAllServerUserData(&instanceSDK.GetAllServerUserDataRequest{
695729
Zone: zone,
696730
ServerID: id,
697731
}, scw.WithContext(ctx))
@@ -713,7 +747,7 @@ func ResourceInstanceServerRead(ctx context.Context, d *schema.ResourceData, m i
713747
////
714748
// Read server private networks
715749
////
716-
ph, err := newPrivateNICHandler(instanceAPI, id, zone)
750+
ph, err := newPrivateNICHandler(api.API, id, zone)
717751
if err != nil {
718752
return diag.FromErr(err)
719753
}
@@ -1034,6 +1068,10 @@ func ResourceInstanceServerUpdate(ctx context.Context, d *schema.ResourceData, m
10341068
}
10351069
}
10361070

1071+
if d.HasChanges("root_volume.0.sbs_iops") {
1072+
warnings = append(warnings, ResourceInstanceServerUpdateRootVolumeIOPS(ctx, api, zone, id, types.ExpandUint32Ptr(d.Get("root_volume.0.sbs_iops")))...)
1073+
}
1074+
10371075
return append(warnings, ResourceInstanceServerRead(ctx, d, m)...)
10381076
}
10391077

@@ -1346,3 +1384,39 @@ func ResourceInstanceServerUpdateIPs(ctx context.Context, d *schema.ResourceData
13461384

13471385
return nil
13481386
}
1387+
1388+
func ResourceInstanceServerUpdateRootVolumeIOPS(ctx context.Context, api *BlockAndInstanceAPI, zone scw.Zone, serverID string, iops *uint32) diag.Diagnostics {
1389+
res, err := api.GetServer(&instanceSDK.GetServerRequest{
1390+
Zone: zone,
1391+
ServerID: serverID,
1392+
}, scw.WithContext(ctx))
1393+
if err != nil {
1394+
return diag.FromErr(err)
1395+
}
1396+
1397+
rootVolume, exists := res.Server.Volumes["0"]
1398+
if exists {
1399+
_, err := api.blockAPI.UpdateVolume(&block.UpdateVolumeRequest{
1400+
Zone: zone,
1401+
VolumeID: rootVolume.ID,
1402+
PerfIops: iops,
1403+
}, scw.WithContext(ctx))
1404+
if err != nil {
1405+
return diag.Diagnostics{{
1406+
Severity: diag.Warning,
1407+
Summary: "Failed to update root_volume iops",
1408+
Detail: err.Error(),
1409+
AttributePath: cty.GetAttrPath("root_volume.0.sbs_iops"),
1410+
}}
1411+
}
1412+
} else {
1413+
return diag.Diagnostics{{
1414+
Severity: diag.Warning,
1415+
Summary: "Failed to find root_volume",
1416+
Detail: "Failed to update root_volume IOPS",
1417+
AttributePath: cty.GetAttrPath("root_volume.0.sbs_iops"),
1418+
}}
1419+
}
1420+
1421+
return nil
1422+
}

internal/services/instance/server_test.go

+84
Original file line numberDiff line numberDiff line change
@@ -1980,6 +1980,90 @@ func TestAccServer_BlockExternal(t *testing.T) {
19801980
})
19811981
}
19821982

1983+
func TestAccServer_BlockExternalRootVolume(t *testing.T) {
1984+
tt := acctest.NewTestTools(t)
1985+
defer tt.Cleanup()
1986+
resource.ParallelTest(t, resource.TestCase{
1987+
PreCheck: func() { acctest.PreCheck(t) },
1988+
ProviderFactories: tt.ProviderFactories,
1989+
CheckDestroy: instancechecks.IsServerDestroyed(tt),
1990+
Steps: []resource.TestStep{
1991+
{
1992+
Config: `
1993+
resource "scaleway_instance_server" "main" {
1994+
name = "tf-tests-instance-block-external-root-volume"
1995+
image = "ubuntu_jammy"
1996+
type = "PLAY2-PICO"
1997+
root_volume {
1998+
volume_type = "sbs_volume"
1999+
size_in_gb = 50
2000+
sbs_iops = 15000
2001+
}
2002+
}`,
2003+
Check: resource.ComposeTestCheckFunc(
2004+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "type", "PLAY2-PICO"),
2005+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "additional_volume_ids.#", "0"),
2006+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.volume_type", string(instanceSDK.VolumeVolumeTypeSbsVolume)),
2007+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.sbs_iops", "15000"),
2008+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.size_in_gb", "50"),
2009+
),
2010+
},
2011+
},
2012+
})
2013+
}
2014+
2015+
func TestAccServer_BlockExternalRootVolumeUpdate(t *testing.T) {
2016+
tt := acctest.NewTestTools(t)
2017+
defer tt.Cleanup()
2018+
resource.ParallelTest(t, resource.TestCase{
2019+
PreCheck: func() { acctest.PreCheck(t) },
2020+
ProviderFactories: tt.ProviderFactories,
2021+
CheckDestroy: instancechecks.IsServerDestroyed(tt),
2022+
Steps: []resource.TestStep{
2023+
{
2024+
Config: `
2025+
resource "scaleway_instance_server" "main" {
2026+
name = "tf-tests-instance-block-external-root-volume-iops-update"
2027+
image = "ubuntu_jammy"
2028+
type = "PLAY2-PICO"
2029+
root_volume {
2030+
volume_type = "sbs_volume"
2031+
size_in_gb = 50
2032+
sbs_iops = 5000
2033+
}
2034+
}`,
2035+
Check: resource.ComposeTestCheckFunc(
2036+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "type", "PLAY2-PICO"),
2037+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "additional_volume_ids.#", "0"),
2038+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.volume_type", string(instanceSDK.VolumeVolumeTypeSbsVolume)),
2039+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.sbs_iops", "5000"),
2040+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.size_in_gb", "50"),
2041+
),
2042+
},
2043+
{
2044+
Config: `
2045+
resource "scaleway_instance_server" "main" {
2046+
name = "tf-tests-instance-block-external-root-volume-iops-update"
2047+
image = "ubuntu_jammy"
2048+
type = "PLAY2-PICO"
2049+
root_volume {
2050+
volume_type = "sbs_volume"
2051+
size_in_gb = 50
2052+
sbs_iops = 15000
2053+
}
2054+
}`,
2055+
Check: resource.ComposeTestCheckFunc(
2056+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "type", "PLAY2-PICO"),
2057+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "additional_volume_ids.#", "0"),
2058+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.volume_type", string(instanceSDK.VolumeVolumeTypeSbsVolume)),
2059+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.sbs_iops", "15000"),
2060+
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.size_in_gb", "50"),
2061+
),
2062+
},
2063+
},
2064+
})
2065+
}
2066+
19832067
func TestAccServer_PrivateNetworkMissingPNIC(t *testing.T) {
19842068
tt := acctest.NewTestTools(t)
19852069
defer tt.Cleanup()

internal/services/instance/testdata/data-source-private-nic-basic.cassette.yaml

+1,023-827
Large diffs are not rendered by default.

internal/services/instance/testdata/data-source-server-basic.cassette.yaml

+1,278-592
Large diffs are not rendered by default.

internal/services/instance/testdata/data-source-servers-basic.cassette.yaml

+1,042-552
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)