Files
nomad/command/volume_status_host.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

201 lines
5.5 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"errors"
"fmt"
"sort"
"strings"
humanize "github.com/dustin/go-humanize"
"github.com/hashicorp/nomad/api"
)
// hostVolumeListError is a non-fatal error for the 'volume status' command when
// used with the -type option unset, because we want to continue on to list CSI
// volumes
var hostVolumeListError = errors.New("Error listing host volumes")
func (c *VolumeStatusCommand) hostVolumeStatus(client *api.Client, id, nodeID, nodePool string, opts formatOpts) error {
if id == "" {
return c.hostVolumeList(client, nodeID, nodePool, opts)
}
if nodeID != "" || nodePool != "" {
return errors.New("-node or -node-pool options can only be used when no ID is provided")
}
// get a host volume that matches the given prefix or a list of all matches
// if an exact match is not found. note we can't use the shared getByPrefix
// helper here because the List API doesn't match the required signature
volStub, possible, err := getHostVolumeByPrefix(client, id, c.namespace)
if err != nil {
return fmt.Errorf("%w: %w", hostVolumeListError, err)
}
if len(possible) > 0 {
out, err := formatHostVolumes(possible, opts)
if err != nil {
return fmt.Errorf("Error formatting: %w", err)
}
return fmt.Errorf("Prefix matched multiple host volumes\n\n%s", out)
}
vol, _, err := client.HostVolumes().Get(volStub.ID, nil)
if err != nil {
return fmt.Errorf("Error querying host volume: %w", err)
}
str, err := formatHostVolume(vol, opts)
if err != nil {
return fmt.Errorf("Error formatting host volume: %w", err)
}
c.Ui.Output(c.Colorize().Color(str))
return nil
}
func (c *VolumeStatusCommand) hostVolumeList(client *api.Client, nodeID, nodePool string, opts formatOpts) error {
if !(opts.json || len(opts.template) > 0) {
c.Ui.Output(c.Colorize().Color("[bold]Dynamic Host Volumes[reset]"))
}
vols, _, err := client.HostVolumes().List(&api.HostVolumeListRequest{
NodeID: nodeID,
NodePool: nodePool,
}, nil)
if err != nil {
return fmt.Errorf("Error querying host volumes: %w", err)
}
if len(vols) == 0 {
c.Ui.Error("No dynamic host volumes")
return nil // empty is not an error
}
str, err := formatHostVolumes(vols, opts)
if err != nil {
return fmt.Errorf("Error formatting host volumes: %w", err)
}
c.Ui.Output(c.Colorize().Color(str))
return nil
}
func getHostVolumeByPrefix(client *api.Client, prefix, ns string) (*api.HostVolumeStub, []*api.HostVolumeStub, error) {
vols, _, err := client.HostVolumes().List(nil, &api.QueryOptions{
Prefix: prefix,
Namespace: ns,
})
if err != nil {
return nil, nil, fmt.Errorf("error querying volumes: %w", err)
}
switch len(vols) {
case 0:
return nil, nil, fmt.Errorf("no volumes with prefix or ID %q found", prefix)
case 1:
return vols[0], nil, nil
default:
// search for exact matches to account for multiple exact ID or name
// matches across namespaces
var match *api.HostVolumeStub
exactMatchesCount := 0
for _, vol := range vols {
if vol.ID == prefix || vol.Name == prefix {
exactMatchesCount++
match = vol
}
}
if exactMatchesCount == 1 {
return match, nil, nil
}
return nil, vols, nil
}
}
func formatHostVolume(vol *api.HostVolume, opts formatOpts) (string, error) {
if opts.json || len(opts.template) > 0 {
out, err := Format(opts.json, opts.template, vol)
if err != nil {
return "", fmt.Errorf("format error: %w", err)
}
return out, nil
}
output := []string{
fmt.Sprintf("ID|%s", vol.ID),
fmt.Sprintf("Name|%s", vol.Name),
fmt.Sprintf("Namespace|%s", vol.Namespace),
fmt.Sprintf("Plugin ID|%s", vol.PluginID),
fmt.Sprintf("Node ID|%s", vol.NodeID),
fmt.Sprintf("Node Pool|%s", vol.NodePool),
fmt.Sprintf("Capacity|%s", humanize.IBytes(uint64(vol.CapacityBytes))),
fmt.Sprintf("State|%s", vol.State),
fmt.Sprintf("Host Path|%s", vol.HostPath),
}
// Exit early
if opts.short {
return formatKV(output), nil
}
full := []string{formatKV(output)}
banner := "\n[bold]Capabilities[reset]"
caps := formatHostVolumeCapabilities(vol.RequestedCapabilities)
full = append(full, banner)
full = append(full, caps)
// Format the allocs
banner = "\n[bold]Allocations[reset]"
allocs := formatAllocListStubs(vol.Allocations, opts.verbose, opts.length)
full = append(full, banner)
full = append(full, allocs)
return strings.Join(full, "\n"), nil
}
// TODO: we could make a bunch more formatters into shared functions using this
type formatOpts struct {
verbose bool
short bool
length int
json bool
template string
}
func formatHostVolumes(vols []*api.HostVolumeStub, opts formatOpts) (string, error) {
// Sort the output by volume ID
sort.Slice(vols, func(i, j int) bool { return vols[i].ID < vols[j].ID })
if opts.json || len(opts.template) > 0 {
out, err := Format(opts.json, opts.template, vols)
if err != nil {
return "", fmt.Errorf("format error: %w", err)
}
return out, nil
}
rows := make([]string, len(vols)+1)
rows[0] = "ID|Name|Namespace|Plugin ID|Node ID|Node Pool|State"
for i, v := range vols {
rows[i+1] = fmt.Sprintf("%s|%s|%s|%s|%s|%s|%s",
limit(v.ID, opts.length),
v.Name,
v.Namespace,
v.PluginID,
limit(v.NodeID, opts.length),
v.NodePool,
v.State,
)
}
return formatList(rows), nil
}
func formatHostVolumeCapabilities(caps []*api.HostVolumeCapability) string {
lines := make([]string, len(caps)+1)
lines[0] = "Access Mode|Attachment Mode"
for i, cap := range caps {
lines[i+1] = fmt.Sprintf("%s|%s", cap.AccessMode, cap.AttachmentMode)
}
return formatList(lines)
}