From 0cc2b0382cfd62bdfab3398f447486961df6f058 Mon Sep 17 00:00:00 2001 From: Kazuyoshi Kato Date: Fri, 1 Dec 2023 13:34:27 -0800 Subject: [PATCH] Add `fly volume snapshot create` This new command is used to snapshot volumes on-demand. --- api/volume_types.go | 1 + flaps/actions.go | 9 ++- flaps/flaps_volumes.go | 13 ++++- internal/command/volumes/create.go | 1 - internal/command/volumes/snapshots/create.go | 57 +++++++++++++++++++ internal/command/volumes/snapshots/list.go | 20 ++++++- .../command/volumes/snapshots/snapshots.go | 1 + 7 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 internal/command/volumes/snapshots/create.go diff --git a/api/volume_types.go b/api/volume_types.go index a3703ca5a8..695d440d06 100644 --- a/api/volume_types.go +++ b/api/volume_types.go @@ -47,4 +47,5 @@ type VolumeSnapshot struct { Size int `json:"size"` Digest string `json:"digest"` CreatedAt time.Time `json:"created_at"` + Status string `json:"status"` } diff --git a/flaps/actions.go b/flaps/actions.go index 0b01b7a277..b832dcf7f0 100644 --- a/flaps/actions.go +++ b/flaps/actions.go @@ -26,7 +26,8 @@ const ( volumeCreate volumetUpdate volumeGet - volumeSnapshot + volumeSnapshotCreate + volumeSnapshotList volumeExtend volumeDelete ) @@ -79,8 +80,10 @@ func (a *flapsAction) String() string { return "volume_list" case volumetUpdate: return "volume_update" - case volumeSnapshot: - return "volume_snapshot" + case volumeSnapshotCreate: + return "volume_snapshot_create" + case volumeSnapshotList: + return "volume_snapshot_list" case volumeExtend: return "volume_extend" case volumeDelete: diff --git a/flaps/flaps_volumes.go b/flaps/flaps_volumes.go index c05ef5e64e..40d3537dd9 100644 --- a/flaps/flaps_volumes.go +++ b/flaps/flaps_volumes.go @@ -80,13 +80,24 @@ func (f *Client) GetVolumeSnapshots(ctx context.Context, volumeId string) ([]api out := make([]api.VolumeSnapshot, 0) - err := f.sendRequestVolumes(ctx, volumeSnapshot, http.MethodGet, getVolumeSnapshotsEndpoint, nil, &out, nil) + err := f.sendRequestVolumes(ctx, volumeSnapshotList, http.MethodGet, getVolumeSnapshotsEndpoint, nil, &out, nil) if err != nil { return nil, fmt.Errorf("failed to get volume %s snapshots: %w", volumeId, err) } return out, nil } +func (f *Client) CreateVolumeSnapshot(ctx context.Context, volumeId string) error { + err := f.sendRequestVolumes( + ctx, volumeSnapshotCreate, http.MethodPost, fmt.Sprintf("/%s/snapshots", volumeId), + nil, nil, nil, + ) + if err != nil { + return fmt.Errorf("failed to snapshot %s: %w", volumeId, err) + } + return nil +} + type ExtendVolumeRequest struct { SizeGB int `json:"size_gb"` } diff --git a/internal/command/volumes/create.go b/internal/command/volumes/create.go index c5f0fb43c6..98fb66fbe2 100644 --- a/internal/command/volumes/create.go +++ b/internal/command/volumes/create.go @@ -35,7 +35,6 @@ func newCreate() *cobra.Command { command.RequireSession, command.RequireAppName, ) - cmd.Args = cobra.ExactArgs(1) flag.Add(cmd, diff --git a/internal/command/volumes/snapshots/create.go b/internal/command/volumes/snapshots/create.go new file mode 100644 index 0000000000..1bb63c9654 --- /dev/null +++ b/internal/command/volumes/snapshots/create.go @@ -0,0 +1,57 @@ +package snapshots + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/superfly/flyctl/client" + "github.com/superfly/flyctl/flaps" + "github.com/superfly/flyctl/internal/appconfig" + "github.com/superfly/flyctl/internal/command" + "github.com/superfly/flyctl/internal/flag" +) + +func newCreate() *cobra.Command { + const ( + short = "Snapshot a volume" + long = "Snapshot a volume\n" + usage = "create " + ) + + cmd := command.New(usage, short, long, create, command.RequireSession) + cmd.Args = cobra.ExactArgs(1) + + flag.Add(cmd, flag.JSONOutput()) + return cmd +} + +func create(ctx context.Context) error { + var client = client.FromContext(ctx).API() + + volumeId := flag.FirstArg(ctx) + + appName := appconfig.NameFromContext(ctx) + if appName == "" { + n, err := client.GetAppNameFromVolume(ctx, volumeId) + if err != nil { + return err + } + appName = *n + } + + flapsClient, err := flaps.NewFromAppName(ctx, appName) + if err != nil { + return err + } + + err = flapsClient.CreateVolumeSnapshot(ctx, volumeId) + if err != nil { + return err + } + + fmt.Printf("Scheduled to snapshot volume %s\n", volumeId) + + return nil +} diff --git a/internal/command/volumes/snapshots/list.go b/internal/command/volumes/snapshots/list.go index c441937b55..67898ae568 100644 --- a/internal/command/volumes/snapshots/list.go +++ b/internal/command/volumes/snapshots/list.go @@ -5,6 +5,7 @@ import ( "fmt" "sort" "strconv" + "time" "github.com/dustin/go-humanize" "github.com/spf13/cobra" @@ -40,6 +41,13 @@ func newList() *cobra.Command { return cmd } +func timeToString(t time.Time) string { + if t.IsZero() { + return "" + } + return humanize.Time(t) +} + func runList(ctx context.Context) error { var ( io = iostreams.FromContext(ctx) @@ -84,12 +92,18 @@ func runList(ctx context.Context) error { rows := make([][]string, 0, len(snapshots)) for _, snapshot := range snapshots { + id := snapshot.ID + if id == "" { + id = "(pending)" + } + rows = append(rows, []string{ - snapshot.ID, + id, + snapshot.Status, strconv.Itoa(snapshot.Size), - humanize.Time(snapshot.CreatedAt), + timeToString(snapshot.CreatedAt), }) } - return render.Table(io.Out, "Snapshots", rows, "ID", "Size", "Created At") + return render.Table(io.Out, "Snapshots", rows, "ID", "Status", "Size", "Created At") } diff --git a/internal/command/volumes/snapshots/snapshots.go b/internal/command/volumes/snapshots/snapshots.go index 6b078e5fc9..b3729c41db 100644 --- a/internal/command/volumes/snapshots/snapshots.go +++ b/internal/command/volumes/snapshots/snapshots.go @@ -22,6 +22,7 @@ func New() *cobra.Command { snapshots.AddCommand( newList(), + newCreate(), ) return snapshots