github.com/hernad/nomad@v1.6.112/nomad/structs/node.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package structs
     5  
     6  import (
     7  	"fmt"
     8  	"reflect"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/hashicorp/hcl/v2/hclsyntax"
    13  	"golang.org/x/exp/maps"
    14  )
    15  
    16  // CSITopology is a map of topological domains to topological segments.
    17  // A topological domain is a sub-division of a cluster, like "region",
    18  // "zone", "rack", etc.
    19  //
    20  // According to CSI, there are a few requirements for the keys within this map:
    21  //   - Valid keys have two segments: an OPTIONAL prefix and name, separated
    22  //     by a slash (/), for example: "com.company.example/zone".
    23  //   - The key name segment is REQUIRED. The prefix is OPTIONAL.
    24  //   - The key name MUST be 63 characters or less, begin and end with an
    25  //     alphanumeric character ([a-z0-9A-Z]), and contain only dashes (-),
    26  //     underscores (_), dots (.), or alphanumerics in between, for example
    27  //     "zone".
    28  //   - The key prefix MUST be 63 characters or less, begin and end with a
    29  //     lower-case alphanumeric character ([a-z0-9]), contain only
    30  //     dashes (-), dots (.), or lower-case alphanumerics in between, and
    31  //     follow domain name notation format
    32  //     (https://tools.ietf.org/html/rfc1035#section-2.3.1).
    33  //   - The key prefix SHOULD include the plugin's host company name and/or
    34  //     the plugin name, to minimize the possibility of collisions with keys
    35  //     from other plugins.
    36  //   - If a key prefix is specified, it MUST be identical across all
    37  //     topology keys returned by the SP (across all RPCs).
    38  //   - Keys MUST be case-insensitive. Meaning the keys "Zone" and "zone"
    39  //     MUST not both exist.
    40  //   - Each value (topological segment) MUST contain 1 or more strings.
    41  //   - Each string MUST be 63 characters or less and begin and end with an
    42  //     alphanumeric character with '-', '_', '.', or alphanumerics in
    43  //     between.
    44  //
    45  // However, Nomad applies lighter restrictions to these, as they are already
    46  // only referenced by plugin within the scheduler and as such collisions and
    47  // related concerns are less of an issue. We may implement these restrictions
    48  // in the future.
    49  type CSITopology struct {
    50  	Segments map[string]string
    51  }
    52  
    53  func (t *CSITopology) Copy() *CSITopology {
    54  	if t == nil {
    55  		return nil
    56  	}
    57  
    58  	return &CSITopology{
    59  		Segments: maps.Clone(t.Segments),
    60  	}
    61  }
    62  
    63  func (t *CSITopology) Equal(o *CSITopology) bool {
    64  	if t == nil || o == nil {
    65  		return t == o
    66  	}
    67  	return maps.Equal(t.Segments, o.Segments)
    68  }
    69  
    70  func (t *CSITopology) MatchFound(o []*CSITopology) bool {
    71  	if t == nil || o == nil || len(o) == 0 {
    72  		return false
    73  	}
    74  
    75  	for _, other := range o {
    76  		if t.Equal(other) {
    77  			return true
    78  		}
    79  	}
    80  	return false
    81  }
    82  
    83  // CSITopologyRequest are the topologies submitted as options to the
    84  // storage provider at the time the volume was created. The storage
    85  // provider will return a single topology.
    86  type CSITopologyRequest struct {
    87  	Required  []*CSITopology
    88  	Preferred []*CSITopology
    89  }
    90  
    91  func (tr *CSITopologyRequest) Equal(o *CSITopologyRequest) bool {
    92  	if tr == nil && o == nil {
    93  		return true
    94  	}
    95  	if tr == nil && o != nil || tr != nil && o == nil {
    96  		return false
    97  	}
    98  	if len(tr.Required) != len(o.Required) || len(tr.Preferred) != len(o.Preferred) {
    99  		return false
   100  	}
   101  	for i, topo := range tr.Required {
   102  		if !topo.Equal(o.Required[i]) {
   103  			return false
   104  		}
   105  	}
   106  	for i, topo := range tr.Preferred {
   107  		if !topo.Equal(o.Preferred[i]) {
   108  			return false
   109  		}
   110  	}
   111  	return true
   112  }
   113  
   114  // CSINodeInfo is the fingerprinted data from a CSI Plugin that is specific to
   115  // the Node API.
   116  type CSINodeInfo struct {
   117  	// ID is the identity of a given nomad client as observed by the storage
   118  	// provider.
   119  	ID string
   120  
   121  	// MaxVolumes is the maximum number of volumes that can be attached to the
   122  	// current host via this provider.
   123  	// If 0 then unlimited volumes may be attached.
   124  	MaxVolumes int64
   125  
   126  	// AccessibleTopology specifies where (regions, zones, racks, etc.) the node is
   127  	// accessible from within the storage provider.
   128  	//
   129  	// A plugin that returns this field MUST also set the `RequiresTopologies`
   130  	// property.
   131  	//
   132  	// This field is OPTIONAL. If it is not specified, then we assume that the
   133  	// the node is not subject to any topological constraint, and MAY
   134  	// schedule workloads that reference any volume V, such that there are
   135  	// no topological constraints declared for V.
   136  	//
   137  	// Example 1:
   138  	//   accessible_topology =
   139  	//     {"region": "R1", "zone": "Z2"}
   140  	// Indicates the node exists within the "region" "R1" and the "zone"
   141  	// "Z2" within the storage provider.
   142  	AccessibleTopology *CSITopology
   143  
   144  	// RequiresNodeStageVolume indicates whether the client should Stage/Unstage
   145  	// volumes on this node.
   146  	RequiresNodeStageVolume bool
   147  
   148  	// SupportsStats indicates plugin support for GET_VOLUME_STATS
   149  	SupportsStats bool
   150  
   151  	// SupportsExpand indicates plugin support for EXPAND_VOLUME
   152  	SupportsExpand bool
   153  
   154  	// SupportsCondition indicates plugin support for VOLUME_CONDITION
   155  	SupportsCondition bool
   156  }
   157  
   158  func (n *CSINodeInfo) Copy() *CSINodeInfo {
   159  	if n == nil {
   160  		return nil
   161  	}
   162  
   163  	nc := new(CSINodeInfo)
   164  	*nc = *n
   165  	nc.AccessibleTopology = n.AccessibleTopology.Copy()
   166  
   167  	return nc
   168  }
   169  
   170  // CSIControllerInfo is the fingerprinted data from a CSI Plugin that is specific to
   171  // the Controller API.
   172  type CSIControllerInfo struct {
   173  
   174  	// SupportsCreateDelete indicates plugin support for CREATE_DELETE_VOLUME
   175  	SupportsCreateDelete bool
   176  
   177  	// SupportsPublishVolume is true when the controller implements the
   178  	// methods required to attach and detach volumes. If this is false Nomad
   179  	// should skip the controller attachment flow.
   180  	SupportsAttachDetach bool
   181  
   182  	// SupportsListVolumes is true when the controller implements the
   183  	// ListVolumes RPC. NOTE: This does not guarantee that attached nodes will
   184  	// be returned unless SupportsListVolumesAttachedNodes is also true.
   185  	SupportsListVolumes bool
   186  
   187  	// SupportsGetCapacity indicates plugin support for GET_CAPACITY
   188  	SupportsGetCapacity bool
   189  
   190  	// SupportsCreateDeleteSnapshot indicates plugin support for
   191  	// CREATE_DELETE_SNAPSHOT
   192  	SupportsCreateDeleteSnapshot bool
   193  
   194  	// SupportsListSnapshots indicates plugin support for LIST_SNAPSHOTS
   195  	SupportsListSnapshots bool
   196  
   197  	// SupportsClone indicates plugin support for CLONE_VOLUME
   198  	SupportsClone bool
   199  
   200  	// SupportsReadOnlyAttach is set to true when the controller returns the
   201  	// ATTACH_READONLY capability.
   202  	SupportsReadOnlyAttach bool
   203  
   204  	// SupportsExpand indicates plugin support for EXPAND_VOLUME
   205  	SupportsExpand bool
   206  
   207  	// SupportsListVolumesAttachedNodes indicates whether the plugin will
   208  	// return attached nodes data when making ListVolume RPCs (plugin support
   209  	// for LIST_VOLUMES_PUBLISHED_NODES)
   210  	SupportsListVolumesAttachedNodes bool
   211  
   212  	// SupportsCondition indicates plugin support for VOLUME_CONDITION
   213  	SupportsCondition bool
   214  
   215  	// SupportsGet indicates plugin support for GET_VOLUME
   216  	SupportsGet bool
   217  }
   218  
   219  func (c *CSIControllerInfo) Copy() *CSIControllerInfo {
   220  	if c == nil {
   221  		return nil
   222  	}
   223  
   224  	nc := new(CSIControllerInfo)
   225  	*nc = *c
   226  
   227  	return nc
   228  }
   229  
   230  // CSIInfo is the current state of a single CSI Plugin. This is updated regularly
   231  // as plugin health changes on the node.
   232  type CSIInfo struct {
   233  	PluginID          string
   234  	AllocID           string
   235  	Healthy           bool
   236  	HealthDescription string
   237  	UpdateTime        time.Time
   238  
   239  	Provider        string // vendor name from CSI GetPluginInfoResponse
   240  	ProviderVersion string // vendor version from CSI GetPluginInfoResponse
   241  
   242  	// RequiresControllerPlugin is set when the CSI Plugin returns the
   243  	// CONTROLLER_SERVICE capability. When this is true, the volumes should not be
   244  	// scheduled on this client until a matching controller plugin is available.
   245  	RequiresControllerPlugin bool
   246  
   247  	// RequiresTopologies is set when the CSI Plugin returns the
   248  	// VOLUME_ACCESSIBLE_CONSTRAINTS capability. When this is true, we must
   249  	// respect the Volume and Node Topology information.
   250  	RequiresTopologies bool
   251  
   252  	// CSI Specific metadata
   253  	ControllerInfo *CSIControllerInfo `json:",omitempty"`
   254  	NodeInfo       *CSINodeInfo       `json:",omitempty"`
   255  }
   256  
   257  func (c *CSIInfo) Copy() *CSIInfo {
   258  	if c == nil {
   259  		return nil
   260  	}
   261  
   262  	nc := new(CSIInfo)
   263  	*nc = *c
   264  	nc.ControllerInfo = c.ControllerInfo.Copy()
   265  	nc.NodeInfo = c.NodeInfo.Copy()
   266  
   267  	return nc
   268  }
   269  
   270  func (c *CSIInfo) SetHealthy(hs bool) {
   271  	c.Healthy = hs
   272  	if hs {
   273  		c.HealthDescription = "healthy"
   274  	} else {
   275  		c.HealthDescription = "unhealthy"
   276  	}
   277  }
   278  
   279  func (c *CSIInfo) Equal(o *CSIInfo) bool {
   280  	if c == nil && o == nil {
   281  		return c == o
   282  	}
   283  
   284  	nc := *c
   285  	nc.UpdateTime = time.Time{}
   286  	no := *o
   287  	no.UpdateTime = time.Time{}
   288  
   289  	return reflect.DeepEqual(nc, no)
   290  }
   291  
   292  func (c *CSIInfo) IsController() bool {
   293  	if c == nil || c.ControllerInfo == nil {
   294  		return false
   295  	}
   296  	return true
   297  }
   298  
   299  func (c *CSIInfo) IsNode() bool {
   300  	if c == nil || c.NodeInfo == nil {
   301  		return false
   302  	}
   303  	return true
   304  }
   305  
   306  // DriverInfo is the current state of a single driver. This is updated
   307  // regularly as driver health changes on the node.
   308  type DriverInfo struct {
   309  	Attributes        map[string]string
   310  	Detected          bool
   311  	Healthy           bool
   312  	HealthDescription string
   313  	UpdateTime        time.Time
   314  }
   315  
   316  func (di *DriverInfo) Copy() *DriverInfo {
   317  	if di == nil {
   318  		return nil
   319  	}
   320  
   321  	cdi := new(DriverInfo)
   322  	*cdi = *di
   323  	cdi.Attributes = maps.Clone(di.Attributes)
   324  	return cdi
   325  }
   326  
   327  // MergeHealthCheck merges information from a health check for a drier into a
   328  // node's driver info
   329  func (di *DriverInfo) MergeHealthCheck(other *DriverInfo) {
   330  	di.Healthy = other.Healthy
   331  	di.HealthDescription = other.HealthDescription
   332  	di.UpdateTime = other.UpdateTime
   333  }
   334  
   335  // MergeFingerprintInfo merges information from fingerprinting a node for a
   336  // driver into a node's driver info for that driver.
   337  func (di *DriverInfo) MergeFingerprintInfo(other *DriverInfo) {
   338  	di.Detected = other.Detected
   339  	di.Attributes = other.Attributes
   340  }
   341  
   342  // HealthCheckEquals determines if two driver info objects are equal. As this
   343  // is used in the process of health checking, we only check the fields that are
   344  // computed by the health checker. In the future, this will be merged.
   345  func (di *DriverInfo) HealthCheckEquals(other *DriverInfo) bool {
   346  	if di == nil && other == nil {
   347  		return true
   348  	}
   349  
   350  	if di.Healthy != other.Healthy {
   351  		return false
   352  	}
   353  
   354  	if di.HealthDescription != other.HealthDescription {
   355  		return false
   356  	}
   357  
   358  	return true
   359  }
   360  
   361  // NodeMetaApplyRequest is used to update Node metadata on Client agents.
   362  type NodeMetaApplyRequest struct {
   363  	QueryOptions // Client RPCs must use QueryOptions to set AllowStale=true
   364  
   365  	// NodeID is the node being targeted by this request (or the node
   366  	// receiving this request if NodeID is empty).
   367  	NodeID string
   368  
   369  	// Meta is the new Node metadata being applied and differs slightly
   370  	// from Node.Meta as nil values are used to unset Node.Meta keys.
   371  	Meta map[string]*string
   372  }
   373  
   374  func (n *NodeMetaApplyRequest) Validate() error {
   375  	if len(n.Meta) == 0 {
   376  		return fmt.Errorf("missing required Meta object")
   377  	}
   378  	for k := range n.Meta {
   379  		if k == "" {
   380  			return fmt.Errorf("metadata keys must not be empty")
   381  		}
   382  
   383  		// Validate keys are dotted identifiers since their primary use case is in
   384  		// constraints as interpolated hcl variables.
   385  		// https://github.com/hashicorp/hcl/blob/v2.16.0/hclsyntax/spec.md#identifiers
   386  		for _, part := range strings.Split(k, ".") {
   387  			if !hclsyntax.ValidIdentifier(part) {
   388  				return fmt.Errorf("%q is invalid; metadata keys must be valid dotted hcl identifiers", k)
   389  			}
   390  		}
   391  	}
   392  
   393  	return nil
   394  }
   395  
   396  // NodeMetaResponse is used to read Node metadata directly from Client agents.
   397  type NodeMetaResponse struct {
   398  	// Meta is the merged static + dynamic Node metadata
   399  	Meta map[string]string
   400  
   401  	// Dynamic is the dynamic Node metadata (set via API)
   402  	Dynamic map[string]*string
   403  
   404  	// Static is the static Node metadata (set via agent configuration)
   405  	Static map[string]string
   406  }