Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(instance): add terminate command #998

Merged
merged 6 commits into from
May 6, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Connect to the serial console of an instance

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

ARGS:
server-id Server ID to connect to
Expand Down
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
168 changes: 168 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,167 @@ 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 {
fmt.Printf("delete IP...\n")
err = api.DeleteIP(&instance.DeleteIPRequest{
Zone: terminateServerArgs.Zone,
IP: server.Server.PublicIP.ID,
})
if err != nil {
fmt.Printf("failed to delete iop: %+v\n", err)
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