2021-04-01 15:16:52 +00:00
|
|
|
package command
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2021-04-08 15:13:36 +00:00
|
|
|
"io"
|
2021-04-01 15:16:52 +00:00
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
humanize "github.com/dustin/go-humanize"
|
|
|
|
"github.com/hashicorp/nomad/api"
|
|
|
|
"github.com/hashicorp/nomad/api/contexts"
|
2021-04-08 15:13:36 +00:00
|
|
|
"github.com/pkg/errors"
|
2021-04-01 15:16:52 +00:00
|
|
|
"github.com/posener/complete"
|
|
|
|
)
|
|
|
|
|
|
|
|
type VolumeSnapshotListCommand struct {
|
|
|
|
Meta
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *VolumeSnapshotListCommand) Help() string {
|
|
|
|
helpText := `
|
|
|
|
Usage: nomad volume snapshot list [-plugin plugin_id]
|
|
|
|
|
|
|
|
Display a list of CSI volume snapshots along with their
|
|
|
|
source volume ID as known to the external storage provider.
|
|
|
|
|
|
|
|
When ACLs are enabled, this command requires a token with the
|
|
|
|
'csi-list-volumes' capability for the plugin's namespace.
|
|
|
|
|
|
|
|
General Options:
|
|
|
|
|
|
|
|
` + generalOptionsUsage(usageOptsDefault) + `
|
|
|
|
|
|
|
|
List Options:
|
|
|
|
|
|
|
|
-plugin: Display only snapshots managed by a particular plugin. By default
|
|
|
|
this command will query all plugins for their snapshots.
|
|
|
|
`
|
|
|
|
return strings.TrimSpace(helpText)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *VolumeSnapshotListCommand) Synopsis() string {
|
|
|
|
return "Display a list of volume snapshots"
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *VolumeSnapshotListCommand) AutocompleteFlags() complete.Flags {
|
|
|
|
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
|
|
|
|
complete.Flags{})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *VolumeSnapshotListCommand) AutocompleteArgs() complete.Predictor {
|
|
|
|
return complete.PredictFunc(func(a complete.Args) []string {
|
|
|
|
client, err := c.Meta.Client()
|
|
|
|
if err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Plugins, nil)
|
|
|
|
if err != nil {
|
|
|
|
return []string{}
|
|
|
|
}
|
|
|
|
return resp.Matches[contexts.Plugins]
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *VolumeSnapshotListCommand) Name() string { return "volume snapshot list" }
|
|
|
|
|
|
|
|
func (c *VolumeSnapshotListCommand) Run(args []string) int {
|
|
|
|
var pluginID string
|
|
|
|
var verbose bool
|
|
|
|
|
|
|
|
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
|
|
|
|
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
|
|
|
flags.StringVar(&pluginID, "plugin", "", "")
|
|
|
|
flags.BoolVar(&verbose, "verbose", false, "")
|
|
|
|
|
|
|
|
if err := flags.Parse(args); err != nil {
|
|
|
|
c.Ui.Error(fmt.Sprintf("Error parsing arguments %s", err))
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
|
|
|
|
args = flags.Args()
|
|
|
|
if len(args) > 0 {
|
|
|
|
c.Ui.Error("This command takes no arguments")
|
|
|
|
c.Ui.Error(commandErrorText(c))
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the HTTP client
|
|
|
|
client, err := c.Meta.Client()
|
|
|
|
if err != nil {
|
|
|
|
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
|
|
|
|
// filter by plugin if a plugin ID was passed
|
|
|
|
if pluginID != "" {
|
|
|
|
plugs, _, err := client.CSIPlugins().List(&api.QueryOptions{Prefix: pluginID})
|
|
|
|
if err != nil {
|
|
|
|
c.Ui.Error(fmt.Sprintf("Error querying CSI plugins: %s", err))
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(plugs) > 1 {
|
|
|
|
out, err := c.csiFormatPlugins(plugs)
|
|
|
|
if err != nil {
|
|
|
|
c.Ui.Error(fmt.Sprintf("Error formatting: %s", err))
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
c.Ui.Error(fmt.Sprintf("Prefix matched multiple plugins\n\n%s", out))
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
if len(plugs) == 0 {
|
|
|
|
c.Ui.Error(fmt.Sprintf("No plugins(s) with prefix or ID %q found", pluginID))
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
|
|
|
|
pluginID = plugs[0].ID
|
|
|
|
}
|
|
|
|
|
|
|
|
q := &api.QueryOptions{PerPage: 30} // TODO: tune page size
|
|
|
|
|
|
|
|
for {
|
|
|
|
resp, _, err := client.CSIVolumes().ListSnapshots(pluginID, q)
|
2021-04-08 15:13:36 +00:00
|
|
|
if err != nil && !errors.Is(err, io.EOF) {
|
2021-04-01 15:16:52 +00:00
|
|
|
c.Ui.Error(fmt.Sprintf(
|
2021-04-07 13:28:08 +00:00
|
|
|
"Error querying CSI external snapshots for plugin %q: %s", pluginID, err))
|
2021-04-01 15:16:52 +00:00
|
|
|
return 1
|
|
|
|
}
|
2021-04-08 15:13:36 +00:00
|
|
|
if resp == nil || len(resp.Snapshots) == 0 {
|
|
|
|
// several plugins return EOF once you hit the end of the page,
|
|
|
|
// rather than an empty list
|
|
|
|
break
|
2021-04-01 15:16:52 +00:00
|
|
|
}
|
|
|
|
|
2021-04-08 15:13:36 +00:00
|
|
|
c.Ui.Output(csiFormatSnapshots(resp.Snapshots, verbose))
|
2021-04-01 15:16:52 +00:00
|
|
|
q.NextToken = resp.NextToken
|
|
|
|
if q.NextToken == "" {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
// we can't know the shape of arbitrarily-sized lists of snapshots,
|
|
|
|
// so break after each page
|
|
|
|
c.Ui.Output("...")
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
func csiFormatSnapshots(snapshots []*api.CSISnapshot, verbose bool) string {
|
|
|
|
rows := []string{"Snapshot ID|Volume ID|Size|Create Time|Ready?"}
|
|
|
|
length := 12
|
|
|
|
if verbose {
|
|
|
|
length = 30
|
|
|
|
}
|
2021-04-07 12:58:18 +00:00
|
|
|
for _, v := range snapshots {
|
|
|
|
rows = append(rows, fmt.Sprintf("%s|%s|%s|%s|%v",
|
2021-05-27 06:06:56 +00:00
|
|
|
v.ID,
|
2021-04-01 15:16:52 +00:00
|
|
|
limit(v.ExternalSourceVolumeID, length),
|
|
|
|
humanize.IBytes(uint64(v.SizeBytes)),
|
|
|
|
formatUnixNanoTime(v.CreateTime),
|
|
|
|
v.IsReady,
|
2021-04-07 12:58:18 +00:00
|
|
|
))
|
2021-04-01 15:16:52 +00:00
|
|
|
}
|
|
|
|
return formatList(rows)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *VolumeSnapshotListCommand) csiFormatPlugins(plugs []*api.CSIPluginListStub) (string, error) {
|
|
|
|
// TODO: this has a lot of overlap with 'nomad plugin status', so we
|
|
|
|
// should factor out some shared formatting helpers.
|
|
|
|
sort.Slice(plugs, func(i, j int) bool { return plugs[i].ID < plugs[j].ID })
|
|
|
|
length := 30
|
|
|
|
rows := make([]string, len(plugs)+1)
|
|
|
|
rows[0] = "ID|Provider|Controllers Healthy/Expected|Nodes Healthy/Expected"
|
|
|
|
for i, p := range plugs {
|
|
|
|
rows[i+1] = fmt.Sprintf("%s|%s|%d/%d|%d/%d",
|
|
|
|
limit(p.ID, length),
|
|
|
|
p.Provider,
|
|
|
|
p.ControllersHealthy,
|
|
|
|
p.ControllersExpected,
|
|
|
|
p.NodesHealthy,
|
|
|
|
p.NodesExpected,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return formatList(rows), nil
|
|
|
|
}
|