Skip to content

Commit

Permalink
feat: add root_volume config and default to sbs volume type (#212)
Browse files Browse the repository at this point in the history
* feat: add root_volume config and default to sbs volume type

* update block cassettes

* lint

* wait for volumes before deletion

* update simple_test

* add root_volume test

* update cassettes

* add root_volume test

* update cassettes

* lint
  • Loading branch information
Codelax authored Feb 11, 2025
1 parent 23b2a4b commit 22b6fc8
Show file tree
Hide file tree
Showing 24 changed files with 3,576 additions and 642 deletions.
2 changes: 2 additions & 0 deletions .web-docs/components/builder/scaleway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ can also be supplied to override the typical auto-generated key:

- `remove_volume` (bool) - RemoveVolume remove the temporary volumes created before running the server

- `root_volume` (ConfigRootVolume) - RootVolumeType lets you configure the root volume

- `block_volume` ([]ConfigBlockVolume) - BlockVolumes define block volumes attached to the server alongside the default volume
See the [BlockVolumes](#block-volumes-configuration) documentation for fields.

Expand Down
11 changes: 11 additions & 0 deletions builder/scaleway/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package scaleway

import (
_ "unsafe" // Import required for link

"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)

//go:linkname createServer github.com/scaleway/scaleway-sdk-go/api/instance/v1.(*API).createServer
func createServer(*instance.API, *instance.CreateServerRequest, ...scw.RequestOption) (*instance.CreateServerResponse, error)
14 changes: 13 additions & 1 deletion builder/scaleway/config.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//go:generate packer-sdc struct-markdown
//go:generate packer-sdc mapstructure-to-hcl2 -type Config,ConfigBlockVolume
//go:generate packer-sdc mapstructure-to-hcl2 -type Config,ConfigBlockVolume,ConfigRootVolume

package scaleway

Expand Down Expand Up @@ -30,6 +30,15 @@ const (
defaultCleanupMachineRelatedDataStatus = "false"
)

type ConfigRootVolume struct {
// The type of the root volume
Type string `mapstructure:"type"`
// IOPS of the root volume if using SBS, will only affect runtime. Image's volumes cannot have a configured IOPS.
IOPS *uint32 `mapstructure:"iops"`
// Size of the root volume
SizeInGB uint64 `mapstructure:"size_in_gb"`
}

type ConfigBlockVolume struct {
// The name of the created volume
Name string `mapstructure:"name"`
Expand Down Expand Up @@ -100,6 +109,9 @@ type Config struct {
// RemoveVolume remove the temporary volumes created before running the server
RemoveVolume bool `mapstructure:"remove_volume"`

// RootVolumeType lets you configure the root volume
RootVolume ConfigRootVolume `mapstructure:"root_volume" required:"false"`

// BlockVolumes define block volumes attached to the server alongside the default volume
// See the [BlockVolumes](#block-volumes-configuration) documentation for fields.
BlockVolumes []ConfigBlockVolume `mapstructure:"block_volume"`
Expand Down
29 changes: 29 additions & 0 deletions builder/scaleway/config.hcl2spec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 49 additions & 0 deletions builder/scaleway/config_root_volume.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package scaleway

import (
"errors"

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/scw"
)

// IsConfigured returns true if root volume has been manually configured.
// If true, the volume template should be used when creating the server.
func (c *ConfigRootVolume) IsConfigured() bool {
return c.Type != "" || c.IOPS != nil || c.SizeInGB != 0
}

// VolumeServerTemplate returns the template to create the volume in a CreateServerRequest
func (c *ConfigRootVolume) VolumeServerTemplate() *instance.VolumeServerTemplate {
tmpl := &instance.VolumeServerTemplate{}

if c.Type != "" {
tmpl.VolumeType = instance.VolumeVolumeType(c.Type)
} else {
tmpl.VolumeType = instance.VolumeVolumeTypeSbsVolume
}

if c.SizeInGB > 0 {
tmpl.Size = scw.SizePtr(scw.Size(c.SizeInGB) * scw.GB)
}

return tmpl
}

func (c *ConfigRootVolume) PostServerCreationSetup(blockAPI *block.API, server *instance.Server) error {
if c.IOPS != nil {
rootVolume, exists := server.Volumes["0"]
if !exists {
return errors.New("root volume not found")
}
_, err := blockAPI.UpdateVolume(&block.UpdateVolumeRequest{
Zone: rootVolume.Zone,
VolumeID: rootVolume.ID,
PerfIops: c.IOPS,
})
return err
}

return nil
}
18 changes: 17 additions & 1 deletion builder/scaleway/step_create_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
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/scw"
)
Expand Down Expand Up @@ -58,6 +59,13 @@ func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) mu
}
}

if c.RootVolume.IsConfigured() {
if createServerReq.Volumes == nil {
createServerReq.Volumes = make(map[string]*instance.VolumeServerTemplate)
}
createServerReq.Volumes["0"] = c.RootVolume.VolumeServerTemplate()
}

if len(c.BlockVolumes) > 0 {
if createServerReq.Volumes == nil {
createServerReq.Volumes = make(map[string]*instance.VolumeServerTemplate)
Expand All @@ -69,14 +77,22 @@ func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) mu
}
}

createServerResp, err := instanceAPI.CreateServer(createServerReq, scw.WithContext(ctx))
createServerResp, err := createServer(instanceAPI, createServerReq, scw.WithContext(ctx))
if err != nil {
err := fmt.Errorf("error creating server: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}

err = c.RootVolume.PostServerCreationSetup(block.NewAPI(state.Get("client").(*scw.Client)), createServerResp.Server)
if err != nil {
err := fmt.Errorf("error during post server creation setup server: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}

if len(c.UserData) > 0 {
m := map[string]io.Reader{}
for k, v := range c.UserData {
Expand Down
53 changes: 44 additions & 9 deletions builder/scaleway/step_create_volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,7 @@ func (s *stepCreateVolume) Cleanup(state multistep.StateBag) {

volumes := state.Get("volumes").([]*instance.VolumeServer)
for _, volume := range volumes {
err := instanceAPI.DeleteVolume(&instance.DeleteVolumeRequest{
VolumeID: volume.ID,
Zone: scw.Zone(c.Zone),
})
err := waitAndDeleteInstanceVolume(instanceAPI, scw.Zone(c.Zone), volume.ID)
if err != nil && !httperrors.Is404(err) {
err := fmt.Errorf("error removing block volume %s: %s", volume.ID, err)
state.Put("error", err)
Expand All @@ -105,15 +102,53 @@ func (s *stepCreateVolume) Cleanup(state multistep.StateBag) {
if err == nil {
continue
}

err = blockAPI.DeleteVolume(&block.DeleteVolumeRequest{
Zone: scw.Zone(c.Zone),
VolumeID: volume.ID,
})
err = waitAndDeleteBlockVolume(blockAPI, scw.Zone(c.Zone), volume.ID)
if err != nil {
err := fmt.Errorf("error removing block volume %s: %s", volume.ID, err)
state.Put("error", err)
ui.Error(fmt.Sprintf("Error removing block volume %s: %s. (Ignored)", volume.ID, err))
}
}
}

func waitAndDeleteInstanceVolume(instanceAPI *instance.API, zone scw.Zone, volumeID string) error {
_, err := instanceAPI.WaitForVolume(&instance.WaitForVolumeRequest{
VolumeID: volumeID,
Zone: zone,
})
if err != nil && !httperrors.Is404(err) {
return err
}

err = instanceAPI.DeleteVolume(&instance.DeleteVolumeRequest{
VolumeID: volumeID,
Zone: zone,
})
if err != nil {
return err
}

return nil
}

func waitAndDeleteBlockVolume(blockAPI *block.API, zone scw.Zone, volumeID string) error {
volumeTerminalStatus := block.VolumeStatusAvailable
_, err := blockAPI.WaitForVolumeAndReferences(&block.WaitForVolumeAndReferencesRequest{
VolumeID: volumeID,
Zone: zone,
VolumeTerminalStatus: &volumeTerminalStatus,
})
if err != nil {
return err
}

err = blockAPI.DeleteVolume(&block.DeleteVolumeRequest{
Zone: zone,
VolumeID: volumeID,
})
if err != nil {
return err
}

return nil
}
2 changes: 2 additions & 0 deletions docs-partials/builder/scaleway/Config-not-required.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

- `remove_volume` (bool) - RemoveVolume remove the temporary volumes created before running the server

- `root_volume` (ConfigRootVolume) - RootVolumeType lets you configure the root volume

- `block_volume` ([]ConfigBlockVolume) - BlockVolumes define block volumes attached to the server alongside the default volume
See the [BlockVolumes](#block-volumes-configuration) documentation for fields.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- Code generated from the comments of the ConfigRootVolume struct in builder/scaleway/config.go; DO NOT EDIT MANUALLY -->

- `type` (string) - The type of the root volume

- `iops` (\*uint32) - IOPS of the root volume if using SBS, will only affect runtime. Image's volumes cannot have a configured IOPS.

- `size_in_gb` (uint64) - Size of the root volume

<!-- End of code generated from the comments of the ConfigRootVolume struct in builder/scaleway/config.go; -->
52 changes: 52 additions & 0 deletions internal/checks/block_snapshot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package checks

import (
"context"
"errors"
"fmt"

"github.com/scaleway/packer-plugin-scaleway/internal/tester"
block "github.com/scaleway/scaleway-sdk-go/api/block/v1alpha1"
"github.com/scaleway/scaleway-sdk-go/scw"
)

type BlockSnapshotCheck struct {
zone scw.Zone
snapshotID *string

size *scw.Size
}

func (c *BlockSnapshotCheck) SizeInGB(size uint64) *BlockSnapshotCheck {
c.size = scw.SizePtr(scw.Size(size) * scw.GB)

return c
}

func (c *BlockSnapshotCheck) Check(ctx context.Context) error {
testCtx := tester.ExtractCtx(ctx)
api := block.NewAPI(testCtx.ScwClient)

if c.snapshotID == nil {
return errors.New("snapshot ID is required")
}

snapshot, err := api.GetSnapshot(&block.GetSnapshotRequest{
Zone: c.zone,
SnapshotID: *c.snapshotID,
})
if err != nil {
return fmt.Errorf("error getting snapshot %s: %w", *c.snapshotID, err)
}

if c.size != nil && snapshot.Size != *c.size {
return fmt.Errorf("snapshot size %d does not match expected size %d", snapshot.Size, *c.size)
}

return nil
}

// BlockSnapshot returns an empty check, to be passed to another check to fill ID and zone
func BlockSnapshot() *BlockSnapshotCheck {
return &BlockSnapshotCheck{}
}
26 changes: 20 additions & 6 deletions internal/checks/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ type ImageCheck struct {
zone scw.Zone
imageName string

rootVolumeType *string
size *scw.Size
extraVolumesType map[string]string
rootVolumeType *string
rootVolumeSnapshot *BlockSnapshotCheck
size *scw.Size
extraVolumesType map[string]string
}

func (c *ImageCheck) RootVolumeType(rootVolumeType string) *ImageCheck {
Expand All @@ -33,6 +34,12 @@ func (c *ImageCheck) RootVolumeType(rootVolumeType string) *ImageCheck {
return c
}

func (c *ImageCheck) RootVolumeBlockSnapshot(snapshotCheck *BlockSnapshotCheck) *ImageCheck {
c.rootVolumeSnapshot = snapshotCheck

return c
}

func (c *ImageCheck) ExtraVolumeType(key string, volumeType string) *ImageCheck {
if c.extraVolumesType == nil {
c.extraVolumesType = map[string]string{}
Expand All @@ -51,6 +58,7 @@ func (c *ImageCheck) SizeInGb(size uint64) *ImageCheck {
func (c *ImageCheck) Check(ctx context.Context) error {
testCtx := tester.ExtractCtx(ctx)
api := instance.NewAPI(testCtx.ScwClient)
images := []*instance.Image(nil)

resp, err := api.ListImages(&instance.ListImagesRequest{
Name: &c.imageName,
Expand All @@ -61,15 +69,21 @@ func (c *ImageCheck) Check(ctx context.Context) error {
return fmt.Errorf("failed to list images: %w", err)
}

if len(resp.Images) == 0 {
for _, img := range resp.Images {
if img.Name == c.imageName {
images = append(images, img)
}
}

if len(images) == 0 {
return fmt.Errorf("image %s not found, no images found", c.imageName)
}

if len(resp.Images) > 1 {
if len(images) > 1 {
return fmt.Errorf("multiple images found with name %s", c.imageName)
}

image := resp.Images[0]
image := images[0]

if image.Name != c.imageName {
return fmt.Errorf("image name %s does not match expected %s", image.Name, c.imageName)
Expand Down
Loading

0 comments on commit 22b6fc8

Please sign in to comment.