mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
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
201 lines
5.5 KiB
Go
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)
|
|
}
|