Skip to content

Commit

Permalink
feat(instance): add terminate command (#998)
Browse files Browse the repository at this point in the history
This command uses the server action API: with `action=terminate` https://developers.scaleway.com/en/products/instance/api/#post-049a5b to quickly delete an instance without backing up its attached volumes.

Options:

* `with-ip`: also delete the flexible IPs
* `with-block`: by default, `terminate` will delete the block storage volumes. When `false`, the command will detach the block volumes before calling `terminate`
* prompt user by default when terminating an instance with at least 1 block volume
* Check IP
  • Loading branch information
jawher authored May 6, 2020
1 parent 4da8ded commit 76fbcaf
Show file tree
Hide file tree
Showing 17 changed files with 7,983 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ GLOBAL FLAGS:
-p, --profile string The config profile to use

SEE ALSO:
# Terminate a running server
scw instance server terminate

# Stop a running server
scw instance server stop
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Terminates a server with the given ID and all of its volumes.

USAGE:
scw instance server terminate <server-id ...> [arg=value ...]

EXAMPLES:
Terminate a server in the default zone with a given id
scw instance server terminate 11111111-1111-1111-1111-111111111111

Terminate a server in fr-par-1 zone with a given id
scw instance server terminate 11111111-1111-1111-1111-111111111111 zone=fr-par-1

Terminate a server and also delete its flexible IPs
scw instance server terminate 11111111-1111-1111-1111-111111111111 with-ip=true

ARGS:
server-id
[with-ip] Delete the IP attached to the server
[with-block=prompt] Delete the Block Storage volumes attached to the server (prompt | true | false)
[zone] Zone to target. If none is passed will use default zone from the config

FLAGS:
-h, --help help for terminate

GLOBAL FLAGS:
-D, --debug Enable debug mode
-o, --output string Output format: json or human
-p, --profile string The config profile to use

SEE ALSO:
# delete a running server
scw instance server delete

# Stop a running server
scw instance server stop
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ AVAILABLE COMMANDS:
console Connect to the serial console of an instance
create Create server
delete Delete server
terminate Terminate server
detach-volume Detach a volume from its server
ssh SSH into a server
start Power on server
Expand Down
1 change: 1 addition & 0 deletions internal/namespaces/instance/v1/custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func GetCommands() *core.Commands {
serverConsoleCommand(),
serverCreateCommand(),
serverDeleteCommand(),
serverTerminateCommand(),
serverDetachVolumeCommand(),
serverSSHCommand(),
serverStartCommand(),
Expand Down
166 changes: 166 additions & 0 deletions internal/namespaces/instance/v1/custom_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,10 @@ func serverDeleteCommand() *core.Command {
},
},
SeeAlsos: []*core.SeeAlso{
{
Command: "scw instance server terminate",
Short: "Terminate a running server",
},
{
Command: "scw instance server stop",
Short: "Stop a running server",
Expand Down Expand Up @@ -822,3 +826,165 @@ func serverDeleteCommand() *core.Command {
},
}
}

type customTerminateServerRequest struct {
Zone scw.Zone
ServerID string
WithIP bool
WithBlock withBlock
}

type withBlock string

const (
withBlockPrompt = withBlock("prompt")
withBlockTrue = withBlock("true")
withBlockFalse = withBlock("false")
)

func serverTerminateCommand() *core.Command {
return &core.Command{
Short: `Terminate server`,
Long: `Terminates a server with the given ID and all of its volumes.`,
Namespace: "instance",
Verb: "terminate",
Resource: "server",
ArgsType: reflect.TypeOf(customTerminateServerRequest{}),
ArgSpecs: core.ArgSpecs{
{
Name: "server-id",
Required: true,
Positional: true,
},
{
Name: "with-ip",
Short: "Delete the IP attached to the server",
},
{
Name: "with-block",
Short: "Delete the Block Storage volumes attached to the server",
Default: core.DefaultValueSetter("prompt"),
EnumValues: []string{
string(withBlockPrompt),
string(withBlockTrue),
string(withBlockFalse),
},
},
core.ZoneArgSpec(),
},
Examples: []*core.Example{
{
Short: "Terminate a server in the default zone with a given id",
Request: `{"server_id": "11111111-1111-1111-1111-111111111111"}`,
},
{
Short: "Terminate a server in fr-par-1 zone with a given id",
Request: `{"zone":"fr-par-1", "server_id": "11111111-1111-1111-1111-111111111111"}`,
},
{
Short: "Terminate a server and also delete its flexible IPs",
Request: `{"with_ip":true, "server_id": "11111111-1111-1111-1111-111111111111"}`,
},
},
SeeAlsos: []*core.SeeAlso{
{
Command: "scw instance server delete",
Short: "delete a running server",
},
{
Command: "scw instance server stop",
Short: "Stop a running server",
},
},
Run: func(ctx context.Context, argsI interface{}) (interface{}, error) {
terminateServerArgs := argsI.(*customTerminateServerRequest)

client := core.ExtractClient(ctx)
api := instance.NewAPI(client)

server, err := api.GetServer(&instance.GetServerRequest{
Zone: terminateServerArgs.Zone,
ServerID: terminateServerArgs.ServerID,
})
if err != nil {
return nil, err
}

deleteBlockVolumes, err := shouldDeleteBlockVolumes(server, terminateServerArgs.WithBlock)
if err != nil {
return nil, err
}

if !deleteBlockVolumes {
// detach block storage volumes before terminating the instance to preserve them
var multiErr error
for _, volume := range server.Server.Volumes {
if volume.VolumeType != instance.VolumeTypeBSSD {
continue
}

if _, err := api.DetachVolume(&instance.DetachVolumeRequest{
Zone: terminateServerArgs.Zone,
VolumeID: volume.ID,
}); err != nil {
multiErr = multierror.Append(multiErr, err)
continue
}

_, _ = interactive.Printf("successfully detached volume %s\n", volume.Name)
}

if multiErr != nil {
return nil, multiErr
}
}

if _, err := api.ServerAction(&instance.ServerActionRequest{
Zone: terminateServerArgs.Zone,
ServerID: terminateServerArgs.ServerID,
Action: instance.ServerActionTerminate,
}); err != nil {
return nil, err
}

var multiErr error
if terminateServerArgs.WithIP && server.Server.PublicIP != nil && !server.Server.PublicIP.Dynamic {
err = api.DeleteIP(&instance.DeleteIPRequest{
Zone: terminateServerArgs.Zone,
IP: server.Server.PublicIP.ID,
})
if err != nil {
multiErr = multierror.Append(multiErr, err)
} else {
_, _ = interactive.Printf("successfully deleted ip %s\n", server.Server.PublicIP.Address.String())
}
}

return &core.SuccessResult{}, multiErr
},
}
}

func shouldDeleteBlockVolumes(server *instance.GetServerResponse, terminateWithBlock withBlock) (bool, error) {
switch terminateWithBlock {
case withBlockTrue:
return true, nil
case withBlockFalse:
return false, nil
case withBlockPrompt:
// Only prompt user if at least one block volume is attached to the instance
for _, volume := range server.Server.Volumes {
if volume.VolumeType != instance.VolumeTypeBSSD {
continue
}

return interactive.PromptBoolWithConfig(&interactive.PromptBoolConfig{
Prompt: "Do you also want to delete block volumes attached to this instance ?",
DefaultValue: false,
})
}
return false, nil
default:
return false, fmt.Errorf("unsupported with-block value %v", terminateWithBlock)
}
}
71 changes: 71 additions & 0 deletions internal/namespaces/instance/v1/custom_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,77 @@ func Test_ServerDelete(t *testing.T) {
interactive.IsInteractive = false
}

// These tests needs to be run in sequence
// since they are using the interactive print
func Test_ServerTerminate(t *testing.T) {
interactive.IsInteractive = true

t.Run("without IP", core.Test(&core.TestConfig{
Commands: GetCommands(),
BeforeFunc: core.ExecStoreBeforeCmd("Server", "scw instance server create image=ubuntu-bionic -w"),
Cmd: `scw instance server terminate {{ .Server.ID }}`,
Check: core.TestCheckCombine(
core.TestCheckGolden(),
core.TestCheckExitCode(0),
func(t *testing.T, ctx *core.CheckFuncCtx) {
api := instance.NewAPI(ctx.Client)
server := ctx.Meta["Server"].(*instance.Server)
_, err := api.GetIP(&instance.GetIPRequest{
IP: server.PublicIP.ID,
})
assert.NoError(t, err)
},
),
AfterFunc: core.ExecAfterCmd(`scw instance ip delete {{ index .Server.PublicIP.ID }}`),
DisableParallel: true,
}))

t.Run("with IP", core.Test(&core.TestConfig{
Commands: GetCommands(),
BeforeFunc: core.ExecStoreBeforeCmd("Server", "scw instance server create image=ubuntu-bionic -w"),
Cmd: `scw instance server terminate {{ .Server.ID }} with-ip=true`,
Check: core.TestCheckCombine(
core.TestCheckGolden(),
core.TestCheckExitCode(0),
func(t *testing.T, ctx *core.CheckFuncCtx) {
api := instance.NewAPI(ctx.Client)
server := ctx.Meta["Server"].(*instance.Server)
_, err := api.GetIP(&instance.GetIPRequest{
IP: server.PublicIP.ID,
})
require.IsType(t, &scw.ResponseError{}, err)
assert.Equal(t, 403, err.(*scw.ResponseError).StatusCode)
},
),
DisableParallel: true,
}))

t.Run("without block", core.Test(&core.TestConfig{
Commands: GetCommands(),
BeforeFunc: core.ExecStoreBeforeCmd("Server", "scw instance server create image=ubuntu-bionic additional-volumes.0=block:10G -w"),
Cmd: `scw instance server terminate {{ .Server.ID }} with-ip=true with-block=false`,
Check: core.TestCheckCombine(
core.TestCheckGolden(),
core.TestCheckExitCode(0),
),
AfterFunc: core.ExecAfterCmd(`scw instance volume delete {{ (index .Server.Volumes "1").ID }}`),
DisableParallel: true,
}))

t.Run("with block", core.Test(&core.TestConfig{
Commands: GetCommands(),
BeforeFunc: core.ExecStoreBeforeCmd("Server", "scw instance server create image=ubuntu-bionic additional-volumes.0=block:10G -w"),
Cmd: `scw instance server terminate {{ .Server.ID }} with-ip=true with-block=true`,
Check: core.TestCheckCombine(
core.TestCheckGolden(),
core.TestCheckExitCode(0),
),
DisableParallel: true,
}))

interactive.IsInteractive = false
}

// These tests needs to be run in sequence
// since they are using the interactive print
func Test_ServerBackup(t *testing.T) {
Expand Down
Loading

0 comments on commit 76fbcaf

Please sign in to comment.