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 }