Files
nomad/command/volume_status.go
Tim Gross 4cdfa19b1e volume status: default type to show both DHV and CSI volumes (#25185)
The `-type` option for `volume status` is a UX papercut because for many
clusters there will be only one sort of volume in use. Update the CLI so that
the default behavior is to query CSI and/or DHV.

This behavior is subtly different when the user provides an ID or not. If the
user doesn't provide an ID, we query both CSI and DHV and show both tables. If
the user provides an ID, we query DHV first and then CSI, and show only the
appropriate volume. Because DHV IDs are UUIDs, we're sure we won't have
collisions between the two. We only show errors if both queries return an error.

Fixes: https://hashicorp.atlassian.net/browse/NET-12214
2025-02-24 11:38:07 -05:00

214 lines
5.2 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"errors"
"fmt"
"strings"
"github.com/hashicorp/nomad/api/contexts"
"github.com/posener/complete"
)
type VolumeStatusCommand struct {
Meta
length int
short bool
verbose bool
json bool
template string
}
func (c *VolumeStatusCommand) Help() string {
helpText := `
Usage: nomad volume status [options] <id>
Display status information about a CSI volume. If no volume id is given, a
list of all volumes will be displayed.
When ACLs are enabled, this command requires a token with the
'csi-read-volume' and 'csi-list-volumes' capability for the volume's
namespace.
General Options:
` + generalOptionsUsage(usageOptsDefault) + `
Status Options:
-type <type>
List only volumes of type <type> (one of "host" or "csi"). If omitted, the
command will query for both dynamic host volumes and CSI volumes.
-short
Display short output. Used only when a single volume is being
queried, and drops verbose information about allocations.
-verbose
Display full volumes information.
-json
Output the volumes in JSON format.
-t
Format and display volumes using a Go template.
-node-pool <pool>
Filter results by node pool, when no volume ID is provided and -type=host.
-node <node ID>
Filter results by node ID, when no volume ID is provided and -type=host.
`
return strings.TrimSpace(helpText)
}
func (c *VolumeStatusCommand) Synopsis() string {
return "Display status information about a volume"
}
func (c *VolumeStatusCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-type": complete.PredictSet("csi", "host"),
"-short": complete.PredictNothing,
"-verbose": complete.PredictNothing,
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
"-node": nodePredictor(c.Client, nil),
"-node-pool": nodePoolPredictor(c.Client, nil),
})
}
func (c *VolumeStatusCommand) 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.Volumes, nil)
if err != nil {
return []string{}
}
matches := resp.Matches[contexts.Volumes]
resp, _, err = client.Search().PrefixSearch(a.Last, contexts.HostVolumes, nil)
if err != nil {
return []string{}
}
matches = append(matches, resp.Matches[contexts.HostVolumes]...)
return matches
})
}
func (c *VolumeStatusCommand) Name() string { return "volume status" }
func (c *VolumeStatusCommand) Run(args []string) int {
var typeArg, nodeID, nodePool string
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.StringVar(&typeArg, "type", "", "")
flags.BoolVar(&c.short, "short", false, "")
flags.BoolVar(&c.verbose, "verbose", false, "")
flags.BoolVar(&c.json, "json", false, "")
flags.StringVar(&c.template, "t", "", "")
flags.StringVar(&nodeID, "node", "", "")
flags.StringVar(&nodePool, "node-pool", "", "")
if err := flags.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing arguments %s", err))
return 1
}
// Check that we either got no arguments or exactly one
args = flags.Args()
if len(args) > 1 {
c.Ui.Error("This command takes either no arguments or one: <id>")
c.Ui.Error(commandErrorText(c))
return 1
}
// Truncate alloc and node IDs unless full length is requested
c.length = shortId
if c.verbose {
c.length = fullId
}
// Get the HTTP client
client, err := c.Meta.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}
id := ""
if len(args) == 1 {
id = args[0]
}
opts := formatOpts{
verbose: c.verbose,
short: c.short,
length: c.length,
json: c.json,
template: c.template,
}
switch typeArg {
case "csi":
if nodeID != "" || nodePool != "" {
c.Ui.Error("-node and -node-pool can only be used with -type host")
return 1
}
if err := c.csiVolumeStatus(client, id, opts); err != nil {
c.Ui.Error(err.Error())
return 1
}
case "host":
if err := c.hostVolumeStatus(client, id, nodeID, nodePool, opts); err != nil {
c.Ui.Error(err.Error())
return 1
}
case "":
if id == "" {
// for list, we want to show both
dhvErr := c.hostVolumeList(client, nodeID, nodePool, opts)
if dhvErr != nil {
c.Ui.Error(dhvErr.Error())
}
c.Ui.Output("")
csiErr := c.csiVolumesList(client, opts)
if csiErr != nil {
c.Ui.Error(csiErr.Error())
}
if dhvErr == nil && csiErr == nil {
return 0
}
return 1
} else {
// for read, we only want to show whichever has results
hostErr := c.hostVolumeStatus(client, id, nodeID, nodePool, opts)
if hostErr != nil {
if !errors.Is(hostErr, hostVolumeListError) {
c.Ui.Error(hostErr.Error())
return 1 // we found a host volume but had some other error
}
csiErr := c.csiVolumeStatus(client, id, opts)
if csiErr != nil {
c.Ui.Error(hostErr.Error())
c.Ui.Error(csiErr.Error())
return 1
}
}
}
default:
c.Ui.Error(fmt.Sprintf("No such volume type %q", typeArg))
return 1
}
return 0
}