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(block): add snapshot wait command #4455

Merged
merged 7 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -15,6 +15,7 @@ ARGS:

FLAGS:
-h, --help help for create
-w, --wait wait until the snapshot is ready

GLOBAL FLAGS:
-c, --config string The path to the config file
Expand Down
3 changes: 3 additions & 0 deletions cmd/scw/testdata/test-all-usage-block-snapshot-usage.golden
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ AVAILABLE COMMANDS:
list List all snapshots
update Update a snapshot

WORKFLOW COMMANDS:
wait Wait for snapshot to reach a stable state

FLAGS:
-h, --help help for snapshot

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
🟥🟥🟥 STDERR️️ 🟥🟥🟥️
Wait for snapshot to reach a stable state. This is similar to using --wait flag on other action commands, but without requiring a new action on the snapshot.

USAGE:
scw block snapshot wait <snapshot-id ...> [arg=value ...]

EXAMPLES:
Wait for a snapshot to be available
scw block snapshot wait 11111111-1111-1111-1111-111111111111 terminal-status=available

ARGS:
[timeout=5m0s] Timeout of the wait
snapshot-id ID of the snapshot affected by the action.
[terminal-status] Expected terminal status, will wait until this status is reached. (unknown_status | creating | available | error | deleting | deleted | in_use | locked | exporting)
[zone=fr-par-1] Zone to target. If none is passed will use default zone from the config (fr-par-1 | fr-par-2 | fr-par-3 | nl-ams-1 | nl-ams-2 | nl-ams-3 | pl-waw-1 | pl-waw-2 | pl-waw-3)

FLAGS:
-h, --help help for wait

GLOBAL FLAGS:
-c, --config string The path to the config file
-D, --debug Enable debug mode
-o, --output string Output format: json or human, see 'scw help output' for more info (default "human")
-p, --profile string The config profile to use
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ ARGS:

FLAGS:
-h, --help help for create
-w, --wait wait until the volume is ready

GLOBAL FLAGS:
-c, --config string The path to the config file
Expand Down
33 changes: 33 additions & 0 deletions docs/commands/block.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This API allows you to manage your Block Storage volumes.
- [Import a snapshot from a Scaleway Object Storage bucket](#import-a-snapshot-from-a-scaleway-object-storage-bucket)
- [List all snapshots](#list-all-snapshots)
- [Update a snapshot](#update-a-snapshot)
- [Wait for snapshot to reach a stable state](#wait-for-snapshot-to-reach-a-stable-state)
- [A Block Storage volume is a logical storage drive on a network-connected storage system. It is exposed to Instances as if it were a physical disk, and can be attached and detached like a hard drive. Several Block volumes can be attached to one Instance at a time](#a-block-storage-volume-is-a-logical-storage-drive-on-a-network-connected-storage-system.-it-is-exposed-to-instances-as-if-it-were-a-physical-disk,-and-can-be-attached-and-detached-like-a-hard-drive.-several-block-volumes-can-be-attached-to-one-instance-at-a-time)
- [Create a volume](#create-a-volume)
- [Delete a detached volume](#delete-a-detached-volume)
Expand Down Expand Up @@ -185,6 +186,38 @@ scw block snapshot update <snapshot-id ...> [arg=value ...]



### Wait for snapshot to reach a stable state

Wait for snapshot to reach a stable state. This is similar to using --wait flag on other action commands, but without requiring a new action on the snapshot.

**Usage:**

```
scw block snapshot wait <snapshot-id ...> [arg=value ...]
```


**Args:**

| Name | | Description |
|------|---|-------------|
| timeout | Default: `5m0s` | Timeout of the wait |
| snapshot-id | Required | ID of the snapshot affected by the action. |
| terminal-status | One of: `unknown_status`, `creating`, `available`, `error`, `deleting`, `deleted`, `in_use`, `locked`, `exporting` | Expected terminal status, will wait until this status is reached. |
| zone | Default: `fr-par-1`<br />One of: `fr-par-1`, `fr-par-2`, `fr-par-3`, `nl-ams-1`, `nl-ams-2`, `nl-ams-3`, `pl-waw-1`, `pl-waw-2`, `pl-waw-3` | Zone to target. If none is passed will use default zone from the config |


**Examples:**


Wait for a snapshot to be available
```
scw block snapshot wait 11111111-1111-1111-1111-111111111111 terminal-status=available
```




## A Block Storage volume is a logical storage drive on a network-connected storage system. It is exposed to Instances as if it were a physical disk, and can be attached and detached like a hard drive. Several Block volumes can be attached to one Instance at a time

Block volumes can be snapshotted, mounted or unmounted.
Expand Down
4 changes: 4 additions & 0 deletions internal/namespaces/block/v1alpha1/custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ func GetCommands() *core.Commands {
cmds := GetGeneratedCommands()

cmds.Add(volumeWaitCommand())
cmds.Add(snapshotWaitCommand())

cmds.MustFind("block", "snapshot", "create").Override(blockSnapshotCreateBuilder)
cmds.MustFind("block", "volume", "create").Override(blockVolumeCreateBuilder)

human.RegisterMarshalerFunc(block.VolumeStatus(""), human.EnumMarshalFunc(volumeStatusMarshalSpecs))
human.RegisterMarshalerFunc(block.SnapshotStatus(""), human.EnumMarshalFunc(snapshotStatusMarshalSpecs))
Expand Down
88 changes: 88 additions & 0 deletions internal/namespaces/block/v1alpha1/custom_snapshot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package block

import (
"context"
"reflect"
"time"

"github.com/scaleway/scaleway-cli/v2/core"
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"
)

const (
snapshotActionTimeout = 5 * time.Minute
)

type snapshotWaitRequest struct {
Zone scw.Zone
SnapshotID string
Timeout time.Duration

TerminalStatus *block.SnapshotStatus
}

func snapshotWaitCommand() *core.Command {
snapshotsStatuses := block.SnapshotStatus("").Values()
snapshotsStatusStrings := make([]string, len(snapshotsStatuses))
for k, v := range snapshotsStatuses {
snapshotsStatusStrings[k] = v.String()
}

return &core.Command{
Short: `Wait for snapshot to reach a stable state`,
Long: `Wait for snapshot to reach a stable state. This is similar to using --wait flag on other action commands, but without requiring a new action on the snapshot.`,
Namespace: "block",
Resource: "snapshot",
Verb: "wait",
Groups: []string{"workflow"},
ArgsType: reflect.TypeOf(snapshotWaitRequest{}),
Run: func(ctx context.Context, argsI interface{}) (i interface{}, err error) {
args := argsI.(*snapshotWaitRequest)

return block.NewAPI(core.ExtractClient(ctx)).WaitForSnapshot(&block.WaitForSnapshotRequest{
Zone: args.Zone,
SnapshotID: args.SnapshotID,
Timeout: scw.TimeDurationPtr(args.Timeout),
RetryInterval: core.DefaultRetryInterval,

TerminalStatus: args.TerminalStatus,
})
},
ArgSpecs: core.ArgSpecs{
core.WaitTimeoutArgSpec(snapshotActionTimeout),
{
Name: "snapshot-id",
Short: `ID of the snapshot affected by the action.`,
Required: true,
Positional: true,
},
{
Name: "terminal-status",
Short: `Expected terminal status, will wait until this status is reached.`,
EnumValues: snapshotsStatusStrings,
},
core.ZoneArgSpec((*instance.API)(nil).Zones()...),
},
Examples: []*core.Example{
{
Short: "Wait for a snapshot to be available",
ArgsJSON: `{"snapshot_id": "11111111-1111-1111-1111-111111111111", "terminal_status": "available"}`,
},
},
}
}

func blockSnapshotCreateBuilder(c *core.Command) *core.Command {
c.WaitFunc = func(ctx context.Context, _, respI interface{}) (interface{}, error) {
resp := respI.(*block.Snapshot)

return block.NewAPI(core.ExtractClient(ctx)).WaitForSnapshot(&block.WaitForSnapshotRequest{
SnapshotID: resp.ID,
Zone: resp.Zone,
})
}

return c
}
49 changes: 49 additions & 0 deletions internal/namespaces/block/v1alpha1/custom_snapshot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package block_test

import (
"testing"

"github.com/scaleway/scaleway-cli/v2/core"
block "github.com/scaleway/scaleway-cli/v2/internal/namespaces/block/v1alpha1"
"github.com/scaleway/scaleway-cli/v2/internal/testhelpers"
blockSDK "github.com/scaleway/scaleway-sdk-go/api/block/v1alpha1"
"github.com/stretchr/testify/require"
)

func Test_SnapshotWait(t *testing.T) {
t.Run("Wait command", core.Test(&core.TestConfig{
Commands: block.GetCommands(),
BeforeFunc: core.BeforeFuncCombine(
core.ExecStoreBeforeCmd("Volume", "scw block volume create perf-iops=5000 from-empty.size=20GB -w"),
core.ExecStoreBeforeCmd("Snapshot", "scw block snapshot create volume-id={{ .Volume.ID }}"),
),
Cmd: "scw block snapshot wait {{ .Snapshot.ID }}",
Check: core.TestCheckCombine(
core.TestCheckGolden(),
core.TestCheckExitCode(0),
),
AfterFunc: core.AfterFuncCombine(
deleteSnapshot("Snapshot"),
deleteVolume("Volume"),
),
}))

t.Run("Wait flag", core.Test(&core.TestConfig{
Commands: block.GetCommands(),
BeforeFunc: core.ExecStoreBeforeCmd("Volume", "scw block volume create perf-iops=5000 from-empty.size=20GB -w"),
Cmd: "scw block snapshot create volume-id={{ .Volume.ID }} -w",
Check: core.TestCheckCombine(
func(t *testing.T, ctx *core.CheckFuncCtx) {
t.Helper()
snap := testhelpers.Value[*blockSDK.Snapshot](t, ctx.Result)
require.Equal(t, blockSDK.SnapshotStatusAvailable, snap.Status)
},
core.TestCheckGolden(),
core.TestCheckExitCode(0),
),
AfterFunc: core.AfterFuncCombine(
deleteSnapshot("CmdResult"),
deleteVolume("Volume"),
),
}))
}
23 changes: 18 additions & 5 deletions internal/namespaces/block/v1alpha1/custom_volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ type volumeWaitRequest struct {
}

func volumeWaitCommand() *core.Command {
terminalStatus := block.VolumeStatus("").Values()
terminalStatusStrings := make([]string, len(terminalStatus))
for k, v := range terminalStatus {
terminalStatusStrings[k] = v.String()
volumeStatuses := block.VolumeStatus("").Values()
volumeStatusStrings := make([]string, len(volumeStatuses))
for k, v := range volumeStatuses {
volumeStatusStrings[k] = v.String()
}

return &core.Command{
Expand Down Expand Up @@ -61,7 +61,7 @@ func volumeWaitCommand() *core.Command {
{
Name: "terminal-status",
Short: `Expected terminal status, will wait until this status is reached.`,
EnumValues: terminalStatusStrings,
EnumValues: volumeStatusStrings,
},
core.ZoneArgSpec((*instance.API)(nil).Zones()...),
},
Expand All @@ -73,3 +73,16 @@ func volumeWaitCommand() *core.Command {
},
}
}

func blockVolumeCreateBuilder(c *core.Command) *core.Command {
c.WaitFunc = func(ctx context.Context, _, respI interface{}) (interface{}, error) {
resp := respI.(*block.Volume)

return block.NewAPI(core.ExtractClient(ctx)).WaitForVolume(&block.WaitForVolumeRequest{
VolumeID: resp.ID,
Zone: resp.Zone,
})
}

return c
}
41 changes: 41 additions & 0 deletions internal/namespaces/block/v1alpha1/custom_volume_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package block_test

import (
"testing"

"github.com/scaleway/scaleway-cli/v2/core"
block "github.com/scaleway/scaleway-cli/v2/internal/namespaces/block/v1alpha1"
"github.com/scaleway/scaleway-cli/v2/internal/testhelpers"
blockSDK "github.com/scaleway/scaleway-sdk-go/api/block/v1alpha1"
"github.com/stretchr/testify/require"
)

func Test_VolumeWait(t *testing.T) {
t.Run("Wait command", core.Test(&core.TestConfig{
Commands: block.GetCommands(),
BeforeFunc: core.BeforeFuncCombine(
core.ExecStoreBeforeCmd("Volume", "scw block volume create perf-iops=5000 from-empty.size=20GB"),
),
Cmd: "scw block volume wait {{ .Volume.ID }}",
Check: core.TestCheckCombine(
core.TestCheckGolden(),
core.TestCheckExitCode(0),
),
AfterFunc: deleteVolume("Volume"),
}))

t.Run("Wait flag", core.Test(&core.TestConfig{
Commands: block.GetCommands(),
Cmd: "scw block volume create perf-iops=5000 from-empty.size=20GB -w",
Check: core.TestCheckCombine(
func(t *testing.T, ctx *core.CheckFuncCtx) {
t.Helper()
vol := testhelpers.Value[*blockSDK.Volume](t, ctx.Result)
require.Equal(t, blockSDK.VolumeStatusAvailable, vol.Status)
},
core.TestCheckGolden(),
core.TestCheckExitCode(0),
),
AfterFunc: deleteVolume("CmdResult"),
}))
}
15 changes: 15 additions & 0 deletions internal/namespaces/block/v1alpha1/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package block_test

import "github.com/scaleway/scaleway-cli/v2/core"

// deleteVolume deletes a volume.
// metaKey must be the key in meta where the volume state is stored.
func deleteVolume(metaKey string) core.AfterFunc {
return core.ExecAfterCmd("scw block volume delete {{." + metaKey + ".ID}}")
}

// deleteSnapshot deletes a snapshot.
// metaKey must be the key in meta where the volume state is stored.
func deleteSnapshot(metaKey string) core.AfterFunc {
return core.ExecAfterCmd("scw block snapshot delete {{." + metaKey + ".ID}}")
}
Loading
Loading