Files
nomad/api/host_volumes.go
Tim Gross 41cf1b03b4 host volumes: -force flag for delete (#25902)
When a node is garbage collected, we leave behind the dynamic host volume in the
state store. We don't want to automatically garbage collect the volumes and risk
data loss, but we should allow these to be removed via the API.

Fixes: https://github.com/hashicorp/nomad/issues/25762
Fixes: https://hashicorp.atlassian.net/browse/NMD-705
2025-05-21 08:55:52 -04:00

254 lines
8.2 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package api
import "net/url"
// HostVolume represents a Dynamic Host Volume: a volume associated with a
// specific Nomad client agent but created via API.
type HostVolume struct {
// Namespace is the Nomad namespace for the host volume, which constrains
// which jobs can mount it.
Namespace string `mapstructure:"namespace" hcl:"namespace"`
// ID is a UUID-like string generated by the server.
ID string `mapstructure:"id" hcl:"id"`
// Name is the name that group.volume will use to identify the volume
// source. Not expected to be unique.
Name string `mapstructure:"name" hcl:"name"`
// PluginID is the name of the host volume plugin on the client that will be
// used for creating the volume. If omitted, the client will use its default
// built-in plugin.
PluginID string `mapstructure:"plugin_id" hcl:"plugin_id"`
// NodePool is the node pool of the node where the volume is placed. If the
// user doesn't provide a node ID, a node will be selected using the
// NodePool and Constraints. If the user provides both NodePool and NodeID,
// NodePool will be used to validate the request. If omitted, the server
// will populate this value in before writing the volume to Raft.
NodePool string `mapstructure:"node_pool" hcl:"node_pool"`
// NodeID is the node where the volume is placed. If the user doesn't
// provide a NodeID, one will be selected using the NodePool and
// Constraints. If omitted, this field will then be populated by the server
// before writing the volume to Raft.
NodeID string `mapstructure:"node_id" hcl:"node_id"`
// Constraints are optional. If the NodeID is not provided, the NodePool and
// Constraints are used to select a node. If the NodeID is provided,
// Constraints are used to validate that the node meets those constraints at
// the time of volume creation.
Constraints []*Constraint `json:",omitempty" hcl:"constraint"`
// Because storage may allow only specific intervals of size, we accept a
// min and max and return the actual capacity when the volume is created or
// updated on the client
RequestedCapacityMinBytes int64 `mapstructure:"capacity_min" hcl:"capacity_min"`
RequestedCapacityMaxBytes int64 `mapstructure:"capacity_max" hcl:"capacity_max"`
CapacityBytes int64 `mapstructure:"capacity" hcl:"capacity"`
// RequestedCapabilities defines the options available to group.volume
// blocks. The scheduler checks against the listed capability blocks and
// selects a node for placement if *any* capability block works.
RequestedCapabilities []*HostVolumeCapability `hcl:"capability"`
// Parameters are an opaque map of parameters for the host volume plugin.
Parameters map[string]string `json:",omitempty"`
// HostPath is the path on disk where the volume's mount point was
// created. We record this to make debugging easier.
HostPath string `mapstructure:"host_path" hcl:"host_path"`
// State represents the overall state of the volume. One of pending, ready,
// deleted.
State HostVolumeState
CreateIndex uint64
CreateTime int64
ModifyIndex uint64
ModifyTime int64
// Allocations is the list of non-client-terminal allocations with claims on
// this host volume. They are denormalized on read and this field will be
// never written to Raft
Allocations []*AllocationListStub `json:",omitempty" mapstructure:"-" hcl:"-"`
}
// HostVolume state reports the current status of the host volume
type HostVolumeState string
const (
HostVolumeStatePending HostVolumeState = "pending"
HostVolumeStateReady HostVolumeState = "ready"
HostVolumeStateUnavailable HostVolumeState = "unavailable"
)
// HostVolumeCapability is the requested attachment and access mode for a volume
type HostVolumeCapability struct {
AttachmentMode HostVolumeAttachmentMode `mapstructure:"attachment_mode" hcl:"attachment_mode"`
AccessMode HostVolumeAccessMode `mapstructure:"access_mode" hcl:"access_mode"`
}
// HostVolumeAttachmentMode chooses the type of storage API that will be used to
// interact with the device.
type HostVolumeAttachmentMode string
const (
HostVolumeAttachmentModeUnknown HostVolumeAttachmentMode = ""
HostVolumeAttachmentModeBlockDevice HostVolumeAttachmentMode = "block-device"
HostVolumeAttachmentModeFilesystem HostVolumeAttachmentMode = "file-system"
)
// HostVolumeAccessMode indicates how Nomad should make the volume available to
// concurrent allocations.
type HostVolumeAccessMode string
const (
HostVolumeAccessModeUnknown HostVolumeAccessMode = ""
HostVolumeAccessModeSingleNodeReader HostVolumeAccessMode = "single-node-reader-only"
HostVolumeAccessModeSingleNodeWriter HostVolumeAccessMode = "single-node-writer"
HostVolumeAccessModeSingleNodeSingleWriter HostVolumeAccessMode = "single-node-single-writer"
HostVolumeAccessModeSingleNodeMultiWriter HostVolumeAccessMode = "single-node-multi-writer"
)
// HostVolumeStub is used for responses for the List Volumes endpoint
type HostVolumeStub struct {
Namespace string
ID string
Name string
PluginID string
NodePool string
NodeID string
CapacityBytes int64
State HostVolumeState
CreateIndex uint64
CreateTime int64
ModifyIndex uint64
ModifyTime int64
}
// HostVolumes is used to access the host volumes API.
type HostVolumes struct {
client *Client
}
// HostVolumes returns a new handle on the host volumes API.
func (c *Client) HostVolumes() *HostVolumes {
return &HostVolumes{client: c}
}
type HostVolumeCreateRequest struct {
Volume *HostVolume
// PolicyOverride overrides Sentinel soft-mandatory policy enforcement
PolicyOverride bool
}
type HostVolumeRegisterRequest struct {
Volume *HostVolume
// PolicyOverride overrides Sentinel soft-mandatory policy enforcement
PolicyOverride bool
}
type HostVolumeCreateResponse struct {
Volume *HostVolume
Warnings string
}
type HostVolumeRegisterResponse struct {
Volume *HostVolume
Warnings string
}
type HostVolumeListRequest struct {
NodeID string
NodePool string
}
type HostVolumeDeleteRequest struct {
ID string
Force bool
}
type HostVolumeDeleteResponse struct{}
// Create forwards to client agents so a host volume can be created on those
// hosts, and registers the volume with Nomad servers.
func (hv *HostVolumes) Create(req *HostVolumeCreateRequest, opts *WriteOptions) (*HostVolumeCreateResponse, *WriteMeta, error) {
var out *HostVolumeCreateResponse
wm, err := hv.client.put("/v1/volume/host/create", req, &out, opts)
if err != nil {
return nil, wm, err
}
return out, wm, nil
}
// Register registers a host volume that was created out-of-band with the Nomad
// servers.
func (hv *HostVolumes) Register(req *HostVolumeRegisterRequest, opts *WriteOptions) (*HostVolumeRegisterResponse, *WriteMeta, error) {
var out *HostVolumeRegisterResponse
wm, err := hv.client.put("/v1/volume/host/register", req, &out, opts)
if err != nil {
return nil, wm, err
}
return out, wm, nil
}
// Get queries for a single host volume, by ID
func (hv *HostVolumes) Get(id string, opts *QueryOptions) (*HostVolume, *QueryMeta, error) {
var out *HostVolume
path, err := url.JoinPath("/v1/volume/host/", url.PathEscape(id))
if err != nil {
return nil, nil, err
}
qm, err := hv.client.query(path, &out, opts)
if err != nil {
return nil, qm, err
}
return out, qm, nil
}
// List queries for a set of host volumes, by namespace, node, node pool, or
// name prefix.
func (hv *HostVolumes) List(req *HostVolumeListRequest, opts *QueryOptions) ([]*HostVolumeStub, *QueryMeta, error) {
var out []*HostVolumeStub
qv := url.Values{}
qv.Set("type", "host")
if req != nil {
if req.NodeID != "" {
qv.Set("node_id", req.NodeID)
}
if req.NodePool != "" {
qv.Set("node_pool", req.NodePool)
}
}
qm, err := hv.client.query("/v1/volumes?"+qv.Encode(), &out, opts)
if err != nil {
return nil, qm, err
}
return out, qm, nil
}
// Delete deletes a host volume
func (hv *HostVolumes) Delete(req *HostVolumeDeleteRequest, opts *WriteOptions) (*HostVolumeDeleteResponse, *WriteMeta, error) {
var resp *HostVolumeDeleteResponse
path, err := url.JoinPath("/v1/volume/host/", url.PathEscape(req.ID))
if err != nil {
return nil, nil, err
}
if req.Force {
path = path + "?force=true"
}
wm, err := hv.client.delete(path, nil, resp, opts)
return resp, wm, err
}