// 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/hashicorp/nomad/helper" flaghelper "github.com/hashicorp/nomad/helper/flags" "github.com/posener/complete" ) type VolumeDeleteCommand struct { Meta Secrets string } func (c *VolumeDeleteCommand) Help() string { helpText := ` Usage: nomad volume delete [options] Delete a volume from an external storage provider. The volume must still be registered with Nomad in order to be deleted. Deleting will fail if the volume is still in use by an allocation or in the process of being unpublished. When ACLs are enabled, this command requires a token with the appropriate capability in the volume's namespace: the 'csi-write-volume' capability for CSI volumes or 'host-volume-create' for dynamic host volumes. General Options: ` + generalOptionsUsage(usageOptsDefault) + ` Delete Options: -force Delete the volume from the Nomad state store if the node has been garbage collected. You should only use -force if the node will never rejoin the cluster. Only available for dynamic host volumes. -secret Secrets to pass to the plugin to delete the snapshot. Accepts multiple flags in the form -secret key=value. Only available for CSI volumes. -type Type of volume to delete. Must be one of "csi" or "host". Defaults to "csi". ` return strings.TrimSpace(helpText) } func (c *VolumeDeleteCommand) AutocompleteFlags() complete.Flags { return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), complete.Flags{ "-type": complete.PredictSet("csi", "host"), "-secret": complete.PredictNothing, }) } func (c *VolumeDeleteCommand) 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 *VolumeDeleteCommand) Synopsis() string { return "Delete a volume" } func (c *VolumeDeleteCommand) Name() string { return "volume delete" } func (c *VolumeDeleteCommand) Run(args []string) int { var secretsArgs flaghelper.StringFlag var typeArg string var force bool flags := c.Meta.FlagSet(c.Name(), FlagSetClient) flags.Usage = func() { c.Ui.Output(c.Help()) } flags.Var(&secretsArgs, "secret", "secrets for snapshot, ex. -secret key=value") flags.StringVar(&typeArg, "type", "csi", "type of volume (csi or host)") flags.BoolVar(&force, "force", false, "force delete from garbage collected node") 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 < 1 { c.Ui.Error("This command takes at least one argument: ") c.Ui.Error(commandErrorText(c)) return 1 } volID := args[0] // Get the HTTP client client, err := c.Meta.Client() if err != nil { c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) return 1 } switch typeArg { case "csi": return c.deleteCSIVolume(client, volID, secretsArgs) case "host": return c.deleteHostVolume(client, volID, force) default: c.Ui.Error(fmt.Sprintf("No such volume type %q", typeArg)) return 1 } } func (c *VolumeDeleteCommand) deleteCSIVolume(client *api.Client, volID string, secretsArgs flaghelper.StringFlag) int { secrets := api.CSISecrets{} for _, kv := range secretsArgs { if key, value, found := strings.Cut(kv, "="); found { secrets[key] = value } else { c.Ui.Error("Secret must be in the format: -secret key=value") return 1 } } // get a CSI volume that matches the given prefix or a list of all matches // if an exact match is not found. stub, 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(`Could not find existing CSI volume to delete: %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 = stub.ID c.namespace = stub.Namespace err = client.CSIVolumes().DeleteOpts(&api.CSIVolumeDeleteRequest{ ExternalVolumeID: volID, Secrets: secrets, }, nil) if err != nil { c.Ui.Error(fmt.Sprintf("Error deleting volume: %s", err)) return 1 } c.Ui.Output(fmt.Sprintf("Successfully deleted volume %q!", volID)) return 0 } func (c *VolumeDeleteCommand) deleteHostVolume(client *api.Client, volID string, force bool) int { if !helper.IsUUID(volID) { stub, possible, err := getHostVolumeByPrefix(client, volID, c.namespace) if err != nil { c.Ui.Error(fmt.Sprintf(`Could not find existing host volume to delete: %s`, err)) return 1 } if len(possible) > 0 { out, err := formatHostVolumes(possible, formatOpts{short: true}) 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 = stub.ID c.namespace = stub.Namespace } _, _, err := client.HostVolumes().Delete(&api.HostVolumeDeleteRequest{ ID: volID, Force: force}, nil) if err != nil { c.Ui.Error(fmt.Sprintf("Error deleting volume: %s", err)) return 1 } c.Ui.Output(fmt.Sprintf("Successfully deleted volume %q!", volID)) return 0 }