Files
nomad/command/volume_status_host.go
Tim Gross cc99e8f0a2 dynamic host volumes: add -id arg for updates of existing volumes (#24996)
If you create a volume via `volume create/register` and want to update it later,
you need to change the volume spec to add the ID that was returned. This isn't a
very nice UX, so let's add an `-id` argument that allows you to update existing
volumes that have that ID.

Ref: https://hashicorp.atlassian.net/browse/NET-12083
2025-02-03 10:26:30 -05:00

199 lines
4.9 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"fmt"
"sort"
"strings"
humanize "github.com/dustin/go-humanize"
"github.com/hashicorp/nomad/api"
)
func (c *VolumeStatusCommand) hostVolumeStatus(client *api.Client, id, nodeID, nodePool string) int {
if id == "" {
return c.listHostVolumes(client, nodeID, nodePool)
}
if nodeID != "" || nodePool != "" {
c.Ui.Error("-node or -node-pool options can only be used when no ID is provided")
return 1
}
opts := formatOpts{
verbose: c.verbose,
short: c.short,
length: c.length,
json: c.json,
template: c.template,
}
// 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 {
c.Ui.Error(fmt.Sprintf("Error listing volumes: %s", err))
return 1
}
if len(possible) > 0 {
out, err := formatHostVolumes(possible, opts)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error formatting: %s", err))
return 1
}
c.Ui.Error(fmt.Sprintf("Prefix matched multiple volumes\n\n%s", out))
return 1
}
vol, _, err := client.HostVolumes().Get(volStub.ID, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying volume: %s", err))
return 1
}
str, err := formatHostVolume(vol, opts)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error formatting volume: %s", err))
return 1
}
c.Ui.Output(c.Colorize().Color(str))
return 0
}
func (c *VolumeStatusCommand) listHostVolumes(client *api.Client, nodeID, nodePool string) int {
vols, _, err := client.HostVolumes().List(&api.HostVolumeListRequest{
NodeID: nodeID,
NodePool: nodePool,
}, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying volumes: %s", err))
return 1
}
opts := formatOpts{
verbose: c.verbose,
short: c.short,
length: c.length,
json: c.json,
template: c.template,
}
str, err := formatHostVolumes(vols, opts)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error formatting volumes: %s", err))
return 1
}
c.Ui.Output(c.Colorize().Color(str))
return 0
}
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: %s", 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: %v", 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)}
// 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: %v", 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
}