Skip to content

Commit

Permalink
dynamic host volumes: basic CLI CRUD operations (#24382)
Browse files Browse the repository at this point in the history
This changeset implements a first pass at the CLI for Dynamic Host Volumes.

Ref: https://hashicorp.atlassian.net/browse/NET-11549
  • Loading branch information
tgross committed Dec 2, 2024
1 parent 72be1fa commit 93e7b61
Show file tree
Hide file tree
Showing 15 changed files with 1,295 additions and 25 deletions.
236 changes: 236 additions & 0 deletions api/host_volumes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
// 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

// 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"
HostVolumeStateDeleted HostVolumeState = "deleted"
)

// 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"

HostVolumeAccessModeMultiNodeReader HostVolumeAccessMode = "multi-node-reader-only"
HostVolumeAccessModeMultiNodeSingleWriter HostVolumeAccessMode = "multi-node-single-writer"
HostVolumeAccessModeMultiNodeMultiWriter HostVolumeAccessMode = "multi-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 {
Volumes []*HostVolume
}

type HostVolumeRegisterRequest struct {
Volumes []*HostVolume
}

type HostVolumeListRequest struct {
NodeID string
NodePool string
}

type HostVolumeDeleteRequest struct {
VolumeIDs []string
}

// Create forwards to client agents so host volumes can be created on those
// hosts, and registers the volumes with Nomad servers.
func (hv *HostVolumes) Create(req *HostVolumeCreateRequest, opts *WriteOptions) ([]*HostVolume, *WriteMeta, error) {
var out struct {
Volumes []*HostVolume
}
wm, err := hv.client.put("/v1/volume/host/create", req, &out, opts)
if err != nil {
return nil, wm, err
}
return out.Volumes, wm, nil
}

// Register registers host volumes that were created out-of-band with the Nomad
// servers.
func (hv *HostVolumes) Register(req *HostVolumeRegisterRequest, opts *WriteOptions) ([]*HostVolume, *WriteMeta, error) {
var out struct {
Volumes []*HostVolume
}
wm, err := hv.client.put("/v1/volume/host/register", req, &out, opts)
if err != nil {
return nil, wm, err
}
return out.Volumes, 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(id string, opts *WriteOptions) (*WriteMeta, error) {
path, err := url.JoinPath("/v1/volume/host/", url.PathEscape(id))
if err != nil {
return nil, err
}
wm, err := hv.client.delete(path, nil, nil, opts)
return wm, err
}
10 changes: 6 additions & 4 deletions command/volume_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ Usage: nomad volume create [options] <input>
If the supplied path is "-" the volume file is read from stdin. Otherwise, it
is read from the file at the supplied path.
When ACLs are enabled, this command requires a token with the
'csi-write-volume' capability for the volume's namespace.
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:
Expand Down Expand Up @@ -99,8 +100,9 @@ func (c *VolumeCreateCommand) Run(args []string) int {

switch strings.ToLower(volType) {
case "csi":
code := c.csiCreate(client, ast)
return code
return c.csiCreate(client, ast)
case "host":
return c.hostVolumeCreate(client, ast)
default:
c.Ui.Error(fmt.Sprintf("Error unknown volume type: %s", volType))
return 1
Expand Down
Loading

0 comments on commit 93e7b61

Please sign in to comment.