Skip to content

Commit fc9456f

Browse files
authored
feat(instance): server create with additional volumes sbs (#4329)
1 parent 21fe18d commit fc9456f

File tree

30 files changed

+14695
-10059
lines changed

30 files changed

+14695
-10059
lines changed

internal/namespaces/instance/v1/custom_ip.go

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package instance
22

33
import (
44
"context"
5-
"errors"
65
"fmt"
76
"net"
87
"reflect"
@@ -191,51 +190,3 @@ func ipDetachCommand() *core.Command {
191190
},
192191
}
193192
}
194-
195-
func cleanIPs(api *instance.API, zone scw.Zone, ipIDs []string) []error {
196-
errs := []error(nil)
197-
for _, ipID := range ipIDs {
198-
err := api.DeleteIP(&instance.DeleteIPRequest{
199-
Zone: zone,
200-
IP: ipID,
201-
})
202-
if err != nil {
203-
errs = append(errs, err)
204-
}
205-
}
206-
207-
return errs
208-
}
209-
210-
func ipIDsFromResponses(resps []*instance.CreateIPResponse) []string {
211-
IDs := make([]string, 0, len(resps))
212-
for _, resp := range resps {
213-
IDs = append(IDs, resp.IP.ID)
214-
}
215-
216-
return IDs
217-
}
218-
219-
// createIPs will create multiple IPs, if one creation fails, all created IPs will be cleaned up.
220-
func createIPs(api *instance.API, reqs []*instance.CreateIPRequest, opts ...scw.RequestOption) ([]string, error) {
221-
resps := make([]*instance.CreateIPResponse, 0, len(reqs))
222-
for _, req := range reqs {
223-
resp, err := api.CreateIP(req, opts...)
224-
if err != nil {
225-
if len(resps) > 0 {
226-
errs := cleanIPs(api, resps[0].IP.Zone, ipIDsFromResponses(resps))
227-
if len(errs) > 0 {
228-
cleanErr := errors.Join(errs...)
229-
cleanErr = fmt.Errorf("failed to clean IPs after creation failure: %w", cleanErr)
230-
err = fmt.Errorf("%s: %w", cleanErr, err)
231-
}
232-
}
233-
234-
return nil, err
235-
}
236-
237-
resps = append(resps, resp)
238-
}
239-
240-
return ipIDsFromResponses(resps), nil
241-
}

internal/namespaces/instance/v1/custom_server_create.go

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -242,25 +242,32 @@ func instanceServerCreateRun(ctx context.Context, argsI interface{}) (i interfac
242242
return nil, err
243243
}
244244

245-
createReq, createIPReqs := serverBuilder.Build()
245+
apiInstance := instance.NewAPI(client)
246+
247+
preCreationSetup := serverBuilder.BuildPreCreationSetup()
246248
postCreationSetup := serverBuilder.BuildPostCreationSetup()
247-
needIPCreation := len(createIPReqs) > 0
248249

249250
//
250-
// IP creation
251-
//
252-
apiInstance := instance.NewAPI(client)
253-
254-
if needIPCreation {
255-
logger.Debugf("creating IP")
251+
// Post server creation setup
252+
/// - IPs creation
253+
err = preCreationSetup.Execute(ctx)
254+
if err != nil {
255+
logger.Debugf("failed to create required resources, deleting created resources")
256+
cleanErr := preCreationSetup.Clean(ctx)
257+
if cleanErr != nil {
258+
logger.Warningf("cannot clean created resources: %s.", cleanErr)
259+
}
260+
return nil, fmt.Errorf("cannot create resource required for server: %s", err)
261+
}
256262

257-
ipIDs, err := createIPs(apiInstance, createIPReqs)
258-
if err != nil {
259-
return nil, fmt.Errorf("error while creating your public IPs: %s", err)
263+
createReq, err := serverBuilder.Build()
264+
if err != nil {
265+
cleanErr := preCreationSetup.Clean(ctx)
266+
if cleanErr != nil {
267+
logger.Warningf("cannot clean created resources: %s.", cleanErr)
260268
}
261269

262-
createReq.PublicIPs = scw.StringsPtr(ipIDs)
263-
logger.Debugf("IPs created: %s", strings.Join(ipIDs, ", "))
270+
return nil, fmt.Errorf("cannot create the server: %s", err)
264271
}
265272

266273
//
@@ -269,14 +276,9 @@ func instanceServerCreateRun(ctx context.Context, argsI interface{}) (i interfac
269276
logger.Debugf("creating server")
270277
serverRes, err := apiInstance.CreateServer(createReq)
271278
if err != nil {
272-
if needIPCreation && createReq.PublicIPs != nil {
273-
// Delete the created IP
274-
formattedIPs := strings.Join(*createReq.PublicIPs, ", ")
275-
logger.Debugf("deleting created IPs: %s", formattedIPs)
276-
errs := cleanIPs(apiInstance, createReq.Zone, *createReq.PublicIPs)
277-
if len(errs) > 0 {
278-
logger.Warningf("cannot delete created IPs %s: %s.", formattedIPs, errors.Join(errs...))
279-
}
279+
cleanErr := preCreationSetup.Clean(ctx)
280+
if cleanErr != nil {
281+
logger.Warningf("cannot clean created resources: %s.", cleanErr)
280282
}
281283

282284
return nil, fmt.Errorf("cannot create the server: %s", err)

internal/namespaces/instance/v1/custom_server_create_builder.go

Lines changed: 152 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -118,22 +118,12 @@ func (sb *ServerBuilder) isWindows() bool {
118118
return commercialTypeIsWindowsServer(sb.createReq.CommercialType)
119119
}
120120

121-
func (sb *ServerBuilder) rootVolumeTemplate() *instance.VolumeServerTemplate {
122-
rootVolume, exists := sb.createReq.Volumes["0"]
123-
if !exists {
124-
return nil
125-
}
126-
127-
return rootVolume
128-
}
129-
130121
func (sb *ServerBuilder) rootVolumeIsSBS() bool {
131-
rootVolume := sb.rootVolumeTemplate()
132-
if rootVolume == nil {
122+
if sb.rootVolume == nil {
133123
return false
134124
}
135125

136-
return rootVolume.VolumeType == instance.VolumeVolumeTypeSbsVolume
126+
return sb.rootVolume.VolumeType == instance.VolumeVolumeTypeSbsVolume
137127
}
138128

139129
func (sb *ServerBuilder) marketplaceImageType() marketplace.LocalImageType {
@@ -237,14 +227,22 @@ func (sb *ServerBuilder) AddIP(ip string) (*ServerBuilder, error) {
237227
return sb, nil
238228
}
239229

230+
func (sb *ServerBuilder) addIPID(ipID string) *ServerBuilder {
231+
if sb.createReq.PublicIPs == nil {
232+
sb.createReq.PublicIPs = new([]string)
233+
}
234+
235+
*sb.createReq.PublicIPs = append(*sb.createReq.PublicIPs, ipID)
236+
237+
return sb
238+
}
239+
240240
// AddVolumes build volume templates from arguments.
241241
//
242242
// More format details in buildVolumeTemplate function.
243243
//
244244
// Also add default volumes to server, ex: scratch storage for GPU servers
245245
func (sb *ServerBuilder) AddVolumes(rootVolume string, additionalVolumes []string) (*ServerBuilder, error) {
246-
var err error
247-
248246
if len(additionalVolumes) > 0 || rootVolume != "" {
249247
if rootVolume != "" {
250248
rootVolumeBuilder, err := NewVolumeBuilder(sb.createReq.Zone, rootVolume)
@@ -260,29 +258,6 @@ func (sb *ServerBuilder) AddVolumes(rootVolume string, additionalVolumes []strin
260258
}
261259
sb.volumes = append(sb.volumes, additionalVolumeBuilder)
262260
}
263-
264-
volumes := make(map[string]*instance.VolumeServerTemplate, len(sb.volumes)+1)
265-
if sb.rootVolume != nil {
266-
volumes["0"], err = sb.rootVolume.BuildVolumeServerTemplate(sb.apiInstance, sb.apiBlock)
267-
if err != nil {
268-
return sb, fmt.Errorf("failed to build root volume: %w", err)
269-
}
270-
}
271-
for i, volume := range sb.volumes {
272-
volumeTemplate, err := volume.BuildVolumeServerTemplate(sb.apiInstance, sb.apiBlock)
273-
if err != nil {
274-
return sb, fmt.Errorf("failed to build volume template: %w", err)
275-
}
276-
index := strconv.Itoa(i + 1)
277-
volumeTemplate.Name = scw.StringPtr(sb.createReq.Name + "-" + index)
278-
volumes[index] = volumeTemplate
279-
}
280-
// Sanitize the volume map to respect API schemas
281-
sb.createReq.Volumes = volumes
282-
}
283-
284-
if sb.serverType != nil {
285-
sb.createReq.Volumes = addDefaultVolumes(sb.serverType, sb.createReq.Volumes)
286261
}
287262

288263
return sb, nil
@@ -360,11 +335,148 @@ func (sb *ServerBuilder) Validate() error {
360335
logger.Warningf("skipping image server-type compatibility validation")
361336
}
362337

363-
return sb.ValidateVolumes()
338+
return nil
364339
}
365340

366-
func (sb *ServerBuilder) Build() (*instance.CreateServerRequest, []*instance.CreateIPRequest) {
367-
return sb.createReq, sb.createIPReqs
341+
func (sb *ServerBuilder) BuildVolumes() error {
342+
var err error
343+
344+
volumes := make(map[string]*instance.VolumeServerTemplate, len(sb.volumes)+1)
345+
if sb.rootVolume != nil {
346+
volumes["0"], err = sb.rootVolume.BuildVolumeServerTemplate(sb.apiInstance, sb.apiBlock)
347+
if err != nil {
348+
return fmt.Errorf("failed to build root volume: %w", err)
349+
}
350+
}
351+
352+
for i, volume := range sb.volumes {
353+
volumeTemplate, err := volume.BuildVolumeServerTemplate(sb.apiInstance, sb.apiBlock)
354+
if err != nil {
355+
return fmt.Errorf("failed to build volume template: %w", err)
356+
}
357+
index := strconv.Itoa(i + 1)
358+
volumeTemplate.Name = scw.StringPtr(sb.createReq.Name + "-" + index)
359+
volumes[index] = volumeTemplate
360+
}
361+
// Sanitize the volume map to respect API schemas
362+
sb.createReq.Volumes = volumes
363+
364+
if sb.serverType != nil {
365+
sb.createReq.Volumes = addDefaultVolumes(sb.serverType, sb.createReq.Volumes)
366+
}
367+
368+
return nil
369+
}
370+
371+
func (sb *ServerBuilder) Build() (*instance.CreateServerRequest, error) {
372+
err := sb.BuildVolumes()
373+
if err != nil {
374+
return nil, err
375+
}
376+
377+
return sb.createReq, sb.ValidateVolumes()
378+
}
379+
380+
type PreServerCreationSetupFunc func(ctx context.Context) error
381+
382+
type PreServerCreationSetup struct {
383+
setupFunctions []PreServerCreationSetupFunc
384+
cleanFunctions []PreServerCreationSetupFunc
385+
}
386+
387+
func (sb *ServerBuilder) BuildPreCreationSetup() *PreServerCreationSetup {
388+
setup := &PreServerCreationSetup{}
389+
390+
for _, ipCreationRequest := range sb.createIPReqs {
391+
setup.setupFunctions = append(setup.setupFunctions, func(ctx context.Context) error {
392+
resp, err := sb.apiInstance.CreateIP(ipCreationRequest, scw.WithContext(ctx))
393+
if err != nil {
394+
return err
395+
}
396+
397+
sb.addIPID(resp.IP.ID)
398+
399+
setup.cleanFunctions = append(setup.cleanFunctions, func(ctx context.Context) error {
400+
return sb.apiInstance.DeleteIP(&instance.DeleteIPRequest{
401+
IP: resp.IP.ID,
402+
Zone: resp.IP.Zone,
403+
}, scw.WithContext(ctx))
404+
})
405+
406+
return nil
407+
})
408+
}
409+
410+
sb.BuildPreCreationVolumesSetup(setup)
411+
412+
return setup
413+
}
414+
415+
// BuildPreCreationVolumesSetup configure PreServerCreationSetup to create required SBS volumes.
416+
// Instance API does not support SBS volumes creation alongside the server, they must be created before then imported.
417+
func (sb *ServerBuilder) BuildPreCreationVolumesSetup(setup *PreServerCreationSetup) {
418+
for _, volume := range sb.volumes {
419+
if volume.VolumeType != instance.VolumeVolumeTypeSbsVolume || volume.VolumeID != nil || volume.Size == nil {
420+
continue
421+
}
422+
423+
projectID := "" // If let empty, ProjectID will be set by scaleway client to default Project ID.
424+
if sb.createReq.Project != nil {
425+
projectID = *sb.createReq.Project
426+
}
427+
428+
setup.setupFunctions = append(setup.setupFunctions, func(ctx context.Context) error {
429+
vol, err := sb.apiBlock.CreateVolume(&block.CreateVolumeRequest{
430+
Zone: volume.Zone,
431+
Name: core.GetRandomName("vol"),
432+
PerfIops: volume.IOPS,
433+
ProjectID: projectID,
434+
FromEmpty: &block.CreateVolumeRequestFromEmpty{
435+
Size: *volume.Size,
436+
},
437+
}, scw.WithContext(ctx))
438+
if err != nil {
439+
return err
440+
}
441+
442+
volume.VolumeID = &vol.ID
443+
444+
setup.cleanFunctions = append(setup.cleanFunctions, func(ctx context.Context) error {
445+
return sb.apiBlock.DeleteVolume(&block.DeleteVolumeRequest{
446+
Zone: vol.Zone,
447+
VolumeID: vol.ID,
448+
}, scw.WithContext(ctx))
449+
})
450+
451+
return nil
452+
})
453+
}
454+
}
455+
456+
func (s *PreServerCreationSetup) Execute(ctx context.Context) error {
457+
for _, setupFunc := range s.setupFunctions {
458+
if err := setupFunc(ctx); err != nil {
459+
return err
460+
}
461+
}
462+
463+
return nil
464+
}
465+
466+
func (s *PreServerCreationSetup) Clean(ctx context.Context) error {
467+
errs := []error(nil)
468+
469+
for _, cleanFunc := range s.cleanFunctions {
470+
if err := cleanFunc(ctx); err != nil {
471+
errs = append(errs, err)
472+
}
473+
}
474+
475+
if len(errs) > 0 {
476+
return errors.Join(errs...)
477+
}
478+
479+
return nil
368480
}
369481

370482
type PostServerCreationSetupFunc func(ctx context.Context, server *instance.Server) error

internal/namespaces/instance/v1/custom_server_create_test.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ func Test_CreateServer(t *testing.T) {
238238
AfterFunc: deleteServerAfterFunc(),
239239
}))
240240

241-
t.Run("sbs additional volumes", core.Test(&core.TestConfig{
241+
t.Run("sbs additional volumes from id", core.Test(&core.TestConfig{
242242
Commands: core.NewCommandsMerge(
243243
instance.GetCommands(),
244244
block.GetCommands(),
@@ -260,6 +260,25 @@ func Test_CreateServer(t *testing.T) {
260260
),
261261
}))
262262

263+
t.Run("sbs additional volumes", core.Test(&core.TestConfig{
264+
Commands: core.NewCommandsMerge(
265+
instance.GetCommands(),
266+
block.GetCommands(),
267+
),
268+
Cmd: testServerCommand("image=ubuntu_jammy additional-volumes.0=sbs:20G stopped=true"),
269+
Check: core.TestCheckCombine(
270+
core.TestCheckExitCode(0),
271+
func(t *testing.T, ctx *core.CheckFuncCtx) {
272+
t.Helper()
273+
assert.NotNil(t, ctx.Result)
274+
assert.Equal(t, instanceSDK.VolumeServerVolumeTypeSbsVolume, ctx.Result.(*instanceSDK.Server).Volumes["1"].VolumeType)
275+
},
276+
),
277+
AfterFunc: core.AfterFuncCombine(
278+
deleteServerAfterFunc(),
279+
),
280+
}))
281+
263282
t.Run("use sbs root volume", core.Test(&core.TestConfig{
264283
Commands: core.NewCommandsMerge(
265284
instance.GetCommands(),

0 commit comments

Comments
 (0)