Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dynamic host volumes: basic CLI CRUD #24382

Merged
merged 1 commit into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"`
Comment on lines +61 to +63
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

users can't (shouldn't be able to) override this, right? so why hcl tag?

...on second thought, I guess it must be for Register, where the volume will have been created out-of-band.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, only for Register.


// 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"
Comment on lines +85 to +87
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if a node becomes unavailable, temporary or otherwise? would it go back to pending, or might we want unknown or similar?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We didn't really cover that feature in the RFC. I think we'll need to figure out whether there's any notion of liveness when we start on the client-side work.

)

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