Skip to content

Commit

Permalink
feat(instance_server): support sbs_volume as root volume (#2641)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Codelax authored Sep 17, 2024
1 parent 86ae5dc commit 35c751c
Show file tree
Hide file tree
Showing 60 changed files with 54,183 additions and 34,219 deletions.
17 changes: 16 additions & 1 deletion docs/resources/instance_server.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,20 @@ resource "scaleway_instance_server" "from_snapshot" {
}
```

#### Using Scaleway Block Storage (SBS) volume

```terraform
resource "scaleway_instance_server" "server" {
type = "PLAY2-MICRO"
image = "ubuntu_jammy"
root_volume {
volume_type = "sbs_volume"
sbs_iops = 15000
size_in_gb = 50
}
}
```

## Argument Reference

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

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

Expand Down
15 changes: 15 additions & 0 deletions internal/services/instance/helpers_instance_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
block "github.com/scaleway/scaleway-sdk-go/api/block/v1alpha1"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/api/marketplace/v2"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality/zonal"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/meta"
Expand All @@ -29,6 +30,10 @@ type UnknownVolume struct {
ServerID *string
Boot *bool

// Iops is set for Block volume only, use IsBlockVolume
// Can be nil if not available in the Block API.
Iops *uint32

InstanceVolumeType instance.VolumeVolumeType
}

Expand Down Expand Up @@ -112,6 +117,9 @@ func (api *BlockAndInstanceAPI) GetUnknownVolume(req *GetUnknownVolumeRequest, o
Size: &blockVolume.Size,
InstanceVolumeType: instance.VolumeVolumeTypeSbsVolume,
}
if blockVolume.Specs != nil {
vol.Iops = blockVolume.Specs.PerfIops
}
for _, ref := range blockVolume.References {
if ref.ProductResourceType == "instance_server" {
vol.ServerID = &ref.ProductResourceID
Expand Down Expand Up @@ -152,3 +160,10 @@ func instanceAndBlockAPIWithZoneAndID(m interface{}, zonedID string) (*BlockAndI
blockAPI: blockAPI,
}, zone, ID, nil
}

func volumeTypeToMarketplaceFilter(volumeType any) marketplace.LocalImageType {
if volumeType != nil && instance.VolumeVolumeType(volumeType.(string)) == instance.VolumeVolumeTypeSbsVolume {
return marketplace.LocalImageTypeInstanceSbs
}
return marketplace.LocalImageTypeInstanceLocal
}
90 changes: 82 additions & 8 deletions internal/services/instance/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
block "github.com/scaleway/scaleway-sdk-go/api/block/v1alpha1"
instanceSDK "github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/api/marketplace/v2"
"github.com/scaleway/scaleway-sdk-go/scw"
Expand Down Expand Up @@ -150,6 +151,12 @@ func ResourceServer() *schema.Resource {
Description: "Volume ID of the root volume",
ExactlyOneOf: []string{"image", "root_volume.0.volume_id"},
},
"sbs_iops": {
Type: schema.TypeInt,
Computed: true,
Optional: true,
Description: "SBS Volume IOPS, only with volume_type as sbs_volume",
},
},
},
},
Expand Down Expand Up @@ -374,7 +381,7 @@ func ResourceInstanceServerCreate(ctx context.Context, d *schema.ResourceData, m
CommercialType: commercialType,
Zone: zone,
ImageLabel: imageLabel,
Type: marketplace.LocalImageTypeInstanceLocal,
Type: volumeTypeToMarketplaceFilter(d.Get("root_volume.0.volume_type")),
})
if err != nil {
return diag.FromErr(fmt.Errorf("could not get image '%s': %s", zonal.NewID(zone, imageLabel), err))
Expand Down Expand Up @@ -466,6 +473,18 @@ func ResourceInstanceServerCreate(ctx context.Context, d *schema.ResourceData, m
return diag.FromErr(err)
}

////
// Configure Block Volume
////
var diags diag.Diagnostics

if iops, ok := d.GetOk("root_volume.0.sbs_iops"); ok {
updateDiags := ResourceInstanceServerUpdateRootVolumeIOPS(ctx, api, zone, res.Server.ID, types.ExpandUint32Ptr(iops))
if len(updateDiags) > 0 {
diags = append(diags, updateDiags...)
}
}

////
// Set user data
////
Expand Down Expand Up @@ -544,7 +563,7 @@ func ResourceInstanceServerCreate(ctx context.Context, d *schema.ResourceData, m
}
}

return ResourceInstanceServerRead(ctx, d, m)
return append(diags, ResourceInstanceServerRead(ctx, d, m)...)
}

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

//gocyclo:ignore
func ResourceInstanceServerRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
instanceAPI, zone, id, err := NewAPIWithZoneAndID(m, d.Id())
api, zone, id, err := instanceAndBlockAPIWithZoneAndID(m, d.Id())
if err != nil {
return diag.FromErr(err)
}

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

rootVolume["volume_id"] = zonal.NewID(zone, volume.ID).String()
rootVolume["size_in_gb"] = int(uint64(volume.Size) / gb)
vol, err := api.GetUnknownVolume(&GetUnknownVolumeRequest{
VolumeID: volume.ID,
Zone: volume.Zone,
})
if err != nil {
return diag.FromErr(fmt.Errorf("failed to read instance volume %s: %w", volume.ID, err))
}

rootVolume["volume_id"] = zonal.NewID(zone, vol.ID).String()
if vol.Size != nil {
rootVolume["size_in_gb"] = int(uint64(*vol.Size) / gb)
} else {
rootVolume["size_in_gb"] = int(uint64(volume.Size) / gb)
}
if vol.IsBlockVolume() {
rootVolume["sbs_iops"] = types.FlattenUint32Ptr(vol.Iops)
}
_, rootVolumeAttributeSet := d.GetOk("root_volume") // Related to https://github.com/hashicorp/terraform-plugin-sdk/issues/142
rootVolume["delete_on_termination"] = d.Get("root_volume.0.delete_on_termination").(bool) || !rootVolumeAttributeSet
rootVolume["volume_type"] = volume.VolumeType
Expand All @@ -691,7 +725,7 @@ func ResourceInstanceServerRead(ctx context.Context, d *schema.ResourceData, m i
////
// Read server user data
////
allUserData, _ := instanceAPI.GetAllServerUserData(&instanceSDK.GetAllServerUserDataRequest{
allUserData, _ := api.GetAllServerUserData(&instanceSDK.GetAllServerUserDataRequest{
Zone: zone,
ServerID: id,
}, scw.WithContext(ctx))
Expand All @@ -713,7 +747,7 @@ func ResourceInstanceServerRead(ctx context.Context, d *schema.ResourceData, m i
////
// Read server private networks
////
ph, err := newPrivateNICHandler(instanceAPI, id, zone)
ph, err := newPrivateNICHandler(api.API, id, zone)
if err != nil {
return diag.FromErr(err)
}
Expand Down Expand Up @@ -1034,6 +1068,10 @@ func ResourceInstanceServerUpdate(ctx context.Context, d *schema.ResourceData, m
}
}

if d.HasChanges("root_volume.0.sbs_iops") {
warnings = append(warnings, ResourceInstanceServerUpdateRootVolumeIOPS(ctx, api, zone, id, types.ExpandUint32Ptr(d.Get("root_volume.0.sbs_iops")))...)
}

return append(warnings, ResourceInstanceServerRead(ctx, d, m)...)
}

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

return nil
}

func ResourceInstanceServerUpdateRootVolumeIOPS(ctx context.Context, api *BlockAndInstanceAPI, zone scw.Zone, serverID string, iops *uint32) diag.Diagnostics {
res, err := api.GetServer(&instanceSDK.GetServerRequest{
Zone: zone,
ServerID: serverID,
}, scw.WithContext(ctx))
if err != nil {
return diag.FromErr(err)
}

rootVolume, exists := res.Server.Volumes["0"]
if exists {
_, err := api.blockAPI.UpdateVolume(&block.UpdateVolumeRequest{
Zone: zone,
VolumeID: rootVolume.ID,
PerfIops: iops,
}, scw.WithContext(ctx))
if err != nil {
return diag.Diagnostics{{
Severity: diag.Warning,
Summary: "Failed to update root_volume iops",
Detail: err.Error(),
AttributePath: cty.GetAttrPath("root_volume.0.sbs_iops"),
}}
}
} else {
return diag.Diagnostics{{
Severity: diag.Warning,
Summary: "Failed to find root_volume",
Detail: "Failed to update root_volume IOPS",
AttributePath: cty.GetAttrPath("root_volume.0.sbs_iops"),
}}
}

return nil
}
84 changes: 84 additions & 0 deletions internal/services/instance/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1980,6 +1980,90 @@ func TestAccServer_BlockExternal(t *testing.T) {
})
}

func TestAccServer_BlockExternalRootVolume(t *testing.T) {
tt := acctest.NewTestTools(t)
defer tt.Cleanup()
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ProviderFactories: tt.ProviderFactories,
CheckDestroy: instancechecks.IsServerDestroyed(tt),
Steps: []resource.TestStep{
{
Config: `
resource "scaleway_instance_server" "main" {
name = "tf-tests-instance-block-external-root-volume"
image = "ubuntu_jammy"
type = "PLAY2-PICO"
root_volume {
volume_type = "sbs_volume"
size_in_gb = 50
sbs_iops = 15000
}
}`,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("scaleway_instance_server.main", "type", "PLAY2-PICO"),
resource.TestCheckResourceAttr("scaleway_instance_server.main", "additional_volume_ids.#", "0"),
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.volume_type", string(instanceSDK.VolumeVolumeTypeSbsVolume)),
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.sbs_iops", "15000"),
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.size_in_gb", "50"),
),
},
},
})
}

func TestAccServer_BlockExternalRootVolumeUpdate(t *testing.T) {
tt := acctest.NewTestTools(t)
defer tt.Cleanup()
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ProviderFactories: tt.ProviderFactories,
CheckDestroy: instancechecks.IsServerDestroyed(tt),
Steps: []resource.TestStep{
{
Config: `
resource "scaleway_instance_server" "main" {
name = "tf-tests-instance-block-external-root-volume-iops-update"
image = "ubuntu_jammy"
type = "PLAY2-PICO"
root_volume {
volume_type = "sbs_volume"
size_in_gb = 50
sbs_iops = 5000
}
}`,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("scaleway_instance_server.main", "type", "PLAY2-PICO"),
resource.TestCheckResourceAttr("scaleway_instance_server.main", "additional_volume_ids.#", "0"),
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.volume_type", string(instanceSDK.VolumeVolumeTypeSbsVolume)),
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.sbs_iops", "5000"),
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.size_in_gb", "50"),
),
},
{
Config: `
resource "scaleway_instance_server" "main" {
name = "tf-tests-instance-block-external-root-volume-iops-update"
image = "ubuntu_jammy"
type = "PLAY2-PICO"
root_volume {
volume_type = "sbs_volume"
size_in_gb = 50
sbs_iops = 15000
}
}`,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("scaleway_instance_server.main", "type", "PLAY2-PICO"),
resource.TestCheckResourceAttr("scaleway_instance_server.main", "additional_volume_ids.#", "0"),
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.volume_type", string(instanceSDK.VolumeVolumeTypeSbsVolume)),
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.sbs_iops", "15000"),
resource.TestCheckResourceAttr("scaleway_instance_server.main", "root_volume.0.size_in_gb", "50"),
),
},
},
})
}

func TestAccServer_PrivateNetworkMissingPNIC(t *testing.T) {
tt := acctest.NewTestTools(t)
defer tt.Cleanup()
Expand Down
1,850 changes: 1,023 additions & 827 deletions internal/services/instance/testdata/data-source-private-nic-basic.cassette.yaml

Large diffs are not rendered by default.

1,870 changes: 1,278 additions & 592 deletions internal/services/instance/testdata/data-source-server-basic.cassette.yaml

Large diffs are not rendered by default.

1,594 changes: 1,042 additions & 552 deletions internal/services/instance/testdata/data-source-servers-basic.cassette.yaml

Large diffs are not rendered by default.

Loading

0 comments on commit 35c751c

Please sign in to comment.