// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package command import ( "fmt" "strings" "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/api/contexts" "github.com/posener/complete" ) type VolumeDetachCommand struct { Meta } func (c *VolumeDetachCommand) Help() string { helpText := ` Usage: nomad volume detach [options] Detach a volume from a Nomad client. When ACLs are enabled, this command requires a token with the 'csi-write-volume' and 'csi-read-volume' capabilities for the volume's namespace. General Options: ` + generalOptionsUsage(usageOptsDefault) + ` ` return strings.TrimSpace(helpText) } func (c *VolumeDetachCommand) AutocompleteFlags() complete.Flags { return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), complete.Flags{}) } func (c *VolumeDetachCommand) 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.Nodes, nil) if err != nil { return []string{} } matches = append(matches, resp.Matches[contexts.Nodes]...) return matches }) } func (c *VolumeDetachCommand) Synopsis() string { return "Detach a volume" } func (c *VolumeDetachCommand) Name() string { return "volume detach" } func (c *VolumeDetachCommand) Run(args []string) int { flags := c.Meta.FlagSet(c.Name(), FlagSetClient) flags.Usage = func() { c.Ui.Output(c.Help()) } if err := flags.Parse(args); err != nil { c.Ui.Error(fmt.Sprintf("Error parsing arguments %s", err)) return 1 } // Check that we get exactly two arguments args = flags.Args() if l := len(args); l != 2 { c.Ui.Error("This command takes two arguments: ") c.Ui.Error(commandErrorText(c)) return 1 } volID := args[0] nodeID := args[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 } nodeID = sanitizeUUIDPrefix(nodeID) nodes, _, err := client.Nodes().PrefixList(nodeID) if err != nil { c.Ui.Error(fmt.Sprintf("Error detaching volume: %s", err)) return 1 } if len(nodes) > 1 { c.Ui.Error(fmt.Sprintf("Prefix matched multiple nodes\n\n%s", formatNodeStubList(nodes, true))) return 1 } if len(nodes) == 1 { nodeID = nodes[0].ID } // If the Nodes.PrefixList doesn't return a node, the node may have been // GC'd. The unpublish workflow gracefully handles this case so that we // can free the claim. Make a best effort to find a node ID among the // volume's claimed allocations, otherwise just use the node ID we've been // given. if len(nodes) == 0 { // get a CSI volume that matches the given prefix or a list of all // matches if an exact match is not found. volStub, possible, err := getByPrefix[api.CSIVolumeListStub]("volumes", client.CSIVolumes().List, func(vol *api.CSIVolumeListStub, prefix string) bool { return vol.ID == prefix }, &api.QueryOptions{ Prefix: volID, Namespace: c.namespace, }) if err != nil { c.Ui.Error(fmt.Sprintf("Error listing volumes: %s", err)) return 1 } if len(possible) > 0 { out, err := csiFormatVolumes(possible, false, "") 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 } volID = volStub.ID vol, _, err := client.CSIVolumes().Info(volID, nil) if err != nil { c.Ui.Error(fmt.Sprintf("Error querying volume: %s", err)) return 1 } nodeIDs := []string{} for _, alloc := range vol.Allocations { if strings.HasPrefix(alloc.NodeID, nodeID) { nodeIDs = append(nodeIDs, alloc.NodeID) } } if len(nodeIDs) > 1 { c.Ui.Error(fmt.Sprintf("Prefix matched multiple node IDs\n\n%s", formatList(nodeIDs))) } if len(nodeIDs) == 1 { nodeID = nodeIDs[0] } } err = client.CSIVolumes().Detach(volID, nodeID, nil) if err != nil { c.Ui.Error(fmt.Sprintf("Error detaching volume: %s", err)) return 1 } return 0 }