open-nomad/nomad/structs/node.go
Michael Schurter 35d65c7c7e
Dynamic Node Metadata (#15844)
Fixes #14617
Dynamic Node Metadata allows Nomad users, and their jobs, to update Node metadata through an API. Currently Node metadata is only reloaded when a Client agent is restarted.

Includes new UI for editing metadata as well.

---------

Co-authored-by: Phil Renaud <phil.renaud@hashicorp.com>
2023-02-07 14:42:25 -08:00

404 lines
11 KiB
Go

package structs
import (
"fmt"
"reflect"
"strings"
"time"
"github.com/hashicorp/hcl/v2/hclsyntax"
"golang.org/x/exp/maps"
)
// CSITopology is a map of topological domains to topological segments.
// A topological domain is a sub-division of a cluster, like "region",
// "zone", "rack", etc.
//
// According to CSI, there are a few requirements for the keys within this map:
// - Valid keys have two segments: an OPTIONAL prefix and name, separated
// by a slash (/), for example: "com.company.example/zone".
// - The key name segment is REQUIRED. The prefix is OPTIONAL.
// - The key name MUST be 63 characters or less, begin and end with an
// alphanumeric character ([a-z0-9A-Z]), and contain only dashes (-),
// underscores (_), dots (.), or alphanumerics in between, for example
// "zone".
// - The key prefix MUST be 63 characters or less, begin and end with a
// lower-case alphanumeric character ([a-z0-9]), contain only
// dashes (-), dots (.), or lower-case alphanumerics in between, and
// follow domain name notation format
// (https://tools.ietf.org/html/rfc1035#section-2.3.1).
// - The key prefix SHOULD include the plugin's host company name and/or
// the plugin name, to minimize the possibility of collisions with keys
// from other plugins.
// - If a key prefix is specified, it MUST be identical across all
// topology keys returned by the SP (across all RPCs).
// - Keys MUST be case-insensitive. Meaning the keys "Zone" and "zone"
// MUST not both exist.
// - Each value (topological segment) MUST contain 1 or more strings.
// - Each string MUST be 63 characters or less and begin and end with an
// alphanumeric character with '-', '_', '.', or alphanumerics in
// between.
//
// However, Nomad applies lighter restrictions to these, as they are already
// only referenced by plugin within the scheduler and as such collisions and
// related concerns are less of an issue. We may implement these restrictions
// in the future.
type CSITopology struct {
Segments map[string]string
}
func (t *CSITopology) Copy() *CSITopology {
if t == nil {
return nil
}
return &CSITopology{
Segments: maps.Clone(t.Segments),
}
}
func (t *CSITopology) Equal(o *CSITopology) bool {
if t == nil || o == nil {
return t == o
}
return maps.Equal(t.Segments, o.Segments)
}
func (t *CSITopology) MatchFound(o []*CSITopology) bool {
if t == nil || o == nil || len(o) == 0 {
return false
}
for _, other := range o {
if t.Equal(other) {
return true
}
}
return false
}
// CSITopologyRequest are the topologies submitted as options to the
// storage provider at the time the volume was created. The storage
// provider will return a single topology.
type CSITopologyRequest struct {
Required []*CSITopology
Preferred []*CSITopology
}
func (tr *CSITopologyRequest) Equal(o *CSITopologyRequest) bool {
if tr == nil && o == nil {
return true
}
if tr == nil && o != nil || tr != nil && o == nil {
return false
}
if len(tr.Required) != len(o.Required) || len(tr.Preferred) != len(o.Preferred) {
return false
}
for i, topo := range tr.Required {
if !topo.Equal(o.Required[i]) {
return false
}
}
for i, topo := range tr.Preferred {
if !topo.Equal(o.Preferred[i]) {
return false
}
}
return true
}
// CSINodeInfo is the fingerprinted data from a CSI Plugin that is specific to
// the Node API.
type CSINodeInfo struct {
// ID is the identity of a given nomad client as observed by the storage
// provider.
ID string
// MaxVolumes is the maximum number of volumes that can be attached to the
// current host via this provider.
// If 0 then unlimited volumes may be attached.
MaxVolumes int64
// AccessibleTopology specifies where (regions, zones, racks, etc.) the node is
// accessible from within the storage provider.
//
// A plugin that returns this field MUST also set the `RequiresTopologies`
// property.
//
// This field is OPTIONAL. If it is not specified, then we assume that the
// the node is not subject to any topological constraint, and MAY
// schedule workloads that reference any volume V, such that there are
// no topological constraints declared for V.
//
// Example 1:
// accessible_topology =
// {"region": "R1", "zone": "Z2"}
// Indicates the node exists within the "region" "R1" and the "zone"
// "Z2" within the storage provider.
AccessibleTopology *CSITopology
// RequiresNodeStageVolume indicates whether the client should Stage/Unstage
// volumes on this node.
RequiresNodeStageVolume bool
// SupportsStats indicates plugin support for GET_VOLUME_STATS
SupportsStats bool
// SupportsExpand indicates plugin support for EXPAND_VOLUME
SupportsExpand bool
// SupportsCondition indicates plugin support for VOLUME_CONDITION
SupportsCondition bool
}
func (n *CSINodeInfo) Copy() *CSINodeInfo {
if n == nil {
return nil
}
nc := new(CSINodeInfo)
*nc = *n
nc.AccessibleTopology = n.AccessibleTopology.Copy()
return nc
}
// CSIControllerInfo is the fingerprinted data from a CSI Plugin that is specific to
// the Controller API.
type CSIControllerInfo struct {
// SupportsCreateDelete indicates plugin support for CREATE_DELETE_VOLUME
SupportsCreateDelete bool
// SupportsPublishVolume is true when the controller implements the
// methods required to attach and detach volumes. If this is false Nomad
// should skip the controller attachment flow.
SupportsAttachDetach bool
// SupportsListVolumes is true when the controller implements the
// ListVolumes RPC. NOTE: This does not guarantee that attached nodes will
// be returned unless SupportsListVolumesAttachedNodes is also true.
SupportsListVolumes bool
// SupportsGetCapacity indicates plugin support for GET_CAPACITY
SupportsGetCapacity bool
// SupportsCreateDeleteSnapshot indicates plugin support for
// CREATE_DELETE_SNAPSHOT
SupportsCreateDeleteSnapshot bool
// SupportsListSnapshots indicates plugin support for LIST_SNAPSHOTS
SupportsListSnapshots bool
// SupportsClone indicates plugin support for CLONE_VOLUME
SupportsClone bool
// SupportsReadOnlyAttach is set to true when the controller returns the
// ATTACH_READONLY capability.
SupportsReadOnlyAttach bool
// SupportsExpand indicates plugin support for EXPAND_VOLUME
SupportsExpand bool
// SupportsListVolumesAttachedNodes indicates whether the plugin will
// return attached nodes data when making ListVolume RPCs (plugin support
// for LIST_VOLUMES_PUBLISHED_NODES)
SupportsListVolumesAttachedNodes bool
// SupportsCondition indicates plugin support for VOLUME_CONDITION
SupportsCondition bool
// SupportsGet indicates plugin support for GET_VOLUME
SupportsGet bool
}
func (c *CSIControllerInfo) Copy() *CSIControllerInfo {
if c == nil {
return nil
}
nc := new(CSIControllerInfo)
*nc = *c
return nc
}
// CSIInfo is the current state of a single CSI Plugin. This is updated regularly
// as plugin health changes on the node.
type CSIInfo struct {
PluginID string
AllocID string
Healthy bool
HealthDescription string
UpdateTime time.Time
Provider string // vendor name from CSI GetPluginInfoResponse
ProviderVersion string // vendor version from CSI GetPluginInfoResponse
// RequiresControllerPlugin is set when the CSI Plugin returns the
// CONTROLLER_SERVICE capability. When this is true, the volumes should not be
// scheduled on this client until a matching controller plugin is available.
RequiresControllerPlugin bool
// RequiresTopologies is set when the CSI Plugin returns the
// VOLUME_ACCESSIBLE_CONSTRAINTS capability. When this is true, we must
// respect the Volume and Node Topology information.
RequiresTopologies bool
// CSI Specific metadata
ControllerInfo *CSIControllerInfo `json:",omitempty"`
NodeInfo *CSINodeInfo `json:",omitempty"`
}
func (c *CSIInfo) Copy() *CSIInfo {
if c == nil {
return nil
}
nc := new(CSIInfo)
*nc = *c
nc.ControllerInfo = c.ControllerInfo.Copy()
nc.NodeInfo = c.NodeInfo.Copy()
return nc
}
func (c *CSIInfo) SetHealthy(hs bool) {
c.Healthy = hs
if hs {
c.HealthDescription = "healthy"
} else {
c.HealthDescription = "unhealthy"
}
}
func (c *CSIInfo) Equal(o *CSIInfo) bool {
if c == nil && o == nil {
return c == o
}
nc := *c
nc.UpdateTime = time.Time{}
no := *o
no.UpdateTime = time.Time{}
return reflect.DeepEqual(nc, no)
}
func (c *CSIInfo) IsController() bool {
if c == nil || c.ControllerInfo == nil {
return false
}
return true
}
func (c *CSIInfo) IsNode() bool {
if c == nil || c.NodeInfo == nil {
return false
}
return true
}
// DriverInfo is the current state of a single driver. This is updated
// regularly as driver health changes on the node.
type DriverInfo struct {
Attributes map[string]string
Detected bool
Healthy bool
HealthDescription string
UpdateTime time.Time
}
func (di *DriverInfo) Copy() *DriverInfo {
if di == nil {
return nil
}
cdi := new(DriverInfo)
*cdi = *di
cdi.Attributes = maps.Clone(di.Attributes)
return cdi
}
// MergeHealthCheck merges information from a health check for a drier into a
// node's driver info
func (di *DriverInfo) MergeHealthCheck(other *DriverInfo) {
di.Healthy = other.Healthy
di.HealthDescription = other.HealthDescription
di.UpdateTime = other.UpdateTime
}
// MergeFingerprintInfo merges information from fingerprinting a node for a
// driver into a node's driver info for that driver.
func (di *DriverInfo) MergeFingerprintInfo(other *DriverInfo) {
di.Detected = other.Detected
di.Attributes = other.Attributes
}
// HealthCheckEquals determines if two driver info objects are equal. As this
// is used in the process of health checking, we only check the fields that are
// computed by the health checker. In the future, this will be merged.
func (di *DriverInfo) HealthCheckEquals(other *DriverInfo) bool {
if di == nil && other == nil {
return true
}
if di.Healthy != other.Healthy {
return false
}
if di.HealthDescription != other.HealthDescription {
return false
}
return true
}
// NodeMetaApplyRequest is used to update Node metadata on Client agents.
type NodeMetaApplyRequest struct {
WriteRequest
// NodeID is the node being targeted by this request (or the node
// receiving this request if NodeID is empty).
NodeID string
// Meta is the new Node metadata being applied and differs slightly
// from Node.Meta as nil values are used to unset Node.Meta keys.
Meta map[string]*string
}
func (n *NodeMetaApplyRequest) Validate() error {
if len(n.Meta) == 0 {
return fmt.Errorf("missing required Meta object")
}
for k := range n.Meta {
if k == "" {
return fmt.Errorf("metadata keys must not be empty")
}
// Validate keys are dotted identifiers since their primary use case is in
// constraints as interpolated hcl variables.
// https://github.com/hashicorp/hcl/blob/v2.16.0/hclsyntax/spec.md#identifiers
for _, part := range strings.Split(k, ".") {
if !hclsyntax.ValidIdentifier(part) {
return fmt.Errorf("%q is invalid; metadata keys must be valid dotted hcl identifiers", k)
}
}
}
return nil
}
// NodeMetaResponse is used to read Node metadata directly from Client agents.
type NodeMetaResponse struct {
// Meta is the merged static + dynamic Node metadata
Meta map[string]string
// Dynamic is the dynamic Node metadata (set via API)
Dynamic map[string]*string
// Static is the static Node metadata (set via agent configuration)
Static map[string]string
}