open-nomad/command/volume_snapshot_list.go

221 lines
5.6 KiB
Go

package command
import (
"errors"
"fmt"
"io"
"os"
"sort"
"strings"
"github.com/dustin/go-humanize"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/api/contexts"
flaghelper "github.com/hashicorp/nomad/helper/flags"
"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 for a plugin 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:
-page-token
Where to start pagination.
-per-page
How many results to show per page. Defaults to 30.
-plugin: Display only snapshots managed by a particular plugin. This
parameter is required.
-secret
Secrets to pass to the plugin to list snapshots. Accepts multiple
flags in the form -secret key=value
-verbose
Display full information for snapshots.
`
return strings.TrimSpace(helpText)
}
func (c *VolumeSnapshotListCommand) Synopsis() string {
return "Display a list of volume snapshots for plugin"
}
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
var secretsArgs flaghelper.StringFlag
var perPage int
var pageToken string
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.StringVar(&pluginID, "plugin", "", "")
flags.BoolVar(&verbose, "verbose", false, "")
flags.Var(&secretsArgs, "secret", "secrets for snapshot, ex. -secret key=value")
flags.IntVar(&perPage, "per-page", 30, "")
flags.StringVar(&pageToken, "page-token", "", "")
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
}
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) == 0 {
c.Ui.Error(fmt.Sprintf("No plugins(s) with prefix or ID %q found", pluginID))
return 1
}
if len(plugs) > 1 {
if pluginID != plugs[0].ID {
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
}
}
pluginID = plugs[0].ID
secrets := api.CSISecrets{}
for _, kv := range secretsArgs {
if key, value, found := strings.Cut(kv, "="); found {
secrets[key] = value
} else {
c.Ui.Error("Secret must be in the format: -secret key=value")
return 1
}
}
req := &api.CSISnapshotListRequest{
PluginID: pluginID,
Secrets: secrets,
QueryOptions: api.QueryOptions{
PerPage: int32(perPage),
NextToken: pageToken,
Params: map[string]string{},
},
}
resp, _, err := client.CSIVolumes().ListSnapshotsOpts(req)
if err != nil && !errors.Is(err, io.EOF) {
c.Ui.Error(fmt.Sprintf(
"Error querying CSI external snapshots for plugin %q: %s", pluginID, err))
return 1
}
if resp == nil || len(resp.Snapshots) == 0 {
// several plugins return EOF once you hit the end of the page,
// rather than an empty list
return 0
}
c.Ui.Output(csiFormatSnapshots(resp.Snapshots, verbose))
if resp.NextToken != "" {
c.Ui.Output(fmt.Sprintf(`
Results have been paginated. To get the next page run:
%s -page-token %s`, argsWithoutPageToken(os.Args), resp.NextToken))
}
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
}
for _, v := range snapshots {
rows = append(rows, fmt.Sprintf("%s|%s|%s|%s|%v",
v.ID,
limit(v.ExternalSourceVolumeID, length),
humanize.IBytes(uint64(v.SizeBytes)),
formatUnixNanoTime(v.CreateTime*1e9), // seconds to nanoseconds
v.IsReady,
))
}
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
}