github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/nomad/structs/csi.go (about) 1 package structs 2 3 import ( 4 "errors" 5 "fmt" 6 "strings" 7 "time" 8 9 multierror "github.com/hashicorp/go-multierror" 10 "github.com/hashicorp/nomad/helper" 11 "golang.org/x/exp/maps" 12 "golang.org/x/exp/slices" 13 ) 14 15 // CSISocketName is the filename that Nomad expects plugins to create inside the 16 // PluginMountDir. 17 const CSISocketName = "csi.sock" 18 19 // CSIIntermediaryDirname is the name of the directory inside the PluginMountDir 20 // where Nomad will expect plugins to create intermediary mounts for volumes. 21 const CSIIntermediaryDirname = "volumes" 22 23 // VolumeTypeCSI is the type in the volume stanza of a TaskGroup 24 const VolumeTypeCSI = "csi" 25 26 // CSIPluginType is an enum string that encapsulates the valid options for a 27 // CSIPlugin stanza's Type. These modes will allow the plugin to be used in 28 // different ways by the client. 29 type CSIPluginType string 30 31 const ( 32 // CSIPluginTypeNode indicates that Nomad should only use the plugin for 33 // performing Node RPCs against the provided plugin. 34 CSIPluginTypeNode CSIPluginType = "node" 35 36 // CSIPluginTypeController indicates that Nomad should only use the plugin for 37 // performing Controller RPCs against the provided plugin. 38 CSIPluginTypeController CSIPluginType = "controller" 39 40 // CSIPluginTypeMonolith indicates that Nomad can use the provided plugin for 41 // both controller and node rpcs. 42 CSIPluginTypeMonolith CSIPluginType = "monolith" 43 ) 44 45 // CSIPluginTypeIsValid validates the given CSIPluginType string and returns 46 // true only when a correct plugin type is specified. 47 func CSIPluginTypeIsValid(pt CSIPluginType) bool { 48 switch pt { 49 case CSIPluginTypeNode, CSIPluginTypeController, CSIPluginTypeMonolith: 50 return true 51 default: 52 return false 53 } 54 } 55 56 // TaskCSIPluginConfig contains the data that is required to setup a task as a 57 // CSI plugin. This will be used by the csi_plugin_supervisor_hook to configure 58 // mounts for the plugin and initiate the connection to the plugin catalog. 59 type TaskCSIPluginConfig struct { 60 // ID is the identifier of the plugin. 61 // Ideally this should be the FQDN of the plugin. 62 ID string 63 64 // Type instructs Nomad on how to handle processing a plugin 65 Type CSIPluginType 66 67 // MountDir is the directory (within its container) in which the plugin creates a 68 // socket (called CSISocketName) for communication with Nomad. Default is /csi. 69 MountDir string 70 71 // StagePublishBaseDir is the base directory (within its container) in which the plugin 72 // mounts volumes being staged and bind mount volumes being published. 73 // e.g. staging_target_path = {StagePublishBaseDir}/staging/{volume-id}/{usage-mode} 74 // e.g. target_path = {StagePublishBaseDir}/per-alloc/{alloc-id}/{volume-id}/{usage-mode} 75 // Default is /local/csi. 76 StagePublishBaseDir string 77 78 // HealthTimeout is the time after which the CSI plugin tasks will be killed 79 // if the CSI Plugin is not healthy. 80 HealthTimeout time.Duration `mapstructure:"health_timeout" hcl:"health_timeout,optional"` 81 } 82 83 func (t *TaskCSIPluginConfig) Copy() *TaskCSIPluginConfig { 84 if t == nil { 85 return nil 86 } 87 88 nt := new(TaskCSIPluginConfig) 89 *nt = *t 90 91 return nt 92 } 93 94 // CSIVolumeCapability is the requested attachment and access mode for a 95 // volume 96 type CSIVolumeCapability struct { 97 AttachmentMode CSIVolumeAttachmentMode 98 AccessMode CSIVolumeAccessMode 99 } 100 101 // CSIVolumeAttachmentMode chooses the type of storage api that will be used to 102 // interact with the device. 103 type CSIVolumeAttachmentMode string 104 105 const ( 106 CSIVolumeAttachmentModeUnknown CSIVolumeAttachmentMode = "" 107 CSIVolumeAttachmentModeBlockDevice CSIVolumeAttachmentMode = "block-device" 108 CSIVolumeAttachmentModeFilesystem CSIVolumeAttachmentMode = "file-system" 109 ) 110 111 func ValidCSIVolumeAttachmentMode(attachmentMode CSIVolumeAttachmentMode) bool { 112 switch attachmentMode { 113 case CSIVolumeAttachmentModeBlockDevice, CSIVolumeAttachmentModeFilesystem: 114 return true 115 default: 116 return false 117 } 118 } 119 120 // CSIVolumeAccessMode indicates how a volume should be used in a storage topology 121 // e.g whether the provider should make the volume available concurrently. 122 type CSIVolumeAccessMode string 123 124 const ( 125 CSIVolumeAccessModeUnknown CSIVolumeAccessMode = "" 126 127 CSIVolumeAccessModeSingleNodeReader CSIVolumeAccessMode = "single-node-reader-only" 128 CSIVolumeAccessModeSingleNodeWriter CSIVolumeAccessMode = "single-node-writer" 129 130 CSIVolumeAccessModeMultiNodeReader CSIVolumeAccessMode = "multi-node-reader-only" 131 CSIVolumeAccessModeMultiNodeSingleWriter CSIVolumeAccessMode = "multi-node-single-writer" 132 CSIVolumeAccessModeMultiNodeMultiWriter CSIVolumeAccessMode = "multi-node-multi-writer" 133 ) 134 135 // ValidCSIVolumeAccessMode checks to see that the provided access mode is a valid, 136 // non-empty access mode. 137 func ValidCSIVolumeAccessMode(accessMode CSIVolumeAccessMode) bool { 138 switch accessMode { 139 case CSIVolumeAccessModeSingleNodeReader, CSIVolumeAccessModeSingleNodeWriter, 140 CSIVolumeAccessModeMultiNodeReader, CSIVolumeAccessModeMultiNodeSingleWriter, 141 CSIVolumeAccessModeMultiNodeMultiWriter: 142 return true 143 default: 144 return false 145 } 146 } 147 148 // ValidCSIVolumeWriteAccessMode checks for a writable access mode. 149 func ValidCSIVolumeWriteAccessMode(accessMode CSIVolumeAccessMode) bool { 150 switch accessMode { 151 case CSIVolumeAccessModeSingleNodeWriter, 152 CSIVolumeAccessModeMultiNodeSingleWriter, 153 CSIVolumeAccessModeMultiNodeMultiWriter: 154 return true 155 default: 156 return false 157 } 158 } 159 160 // CSIMountOptions contain optional additional configuration that can be used 161 // when specifying that a Volume should be used with VolumeAccessTypeMount. 162 type CSIMountOptions struct { 163 // FSType is an optional field that allows an operator to specify the type 164 // of the filesystem. 165 FSType string 166 167 // MountFlags contains additional options that may be used when mounting the 168 // volume by the plugin. This may contain sensitive data and should not be 169 // leaked. 170 MountFlags []string 171 } 172 173 func (o *CSIMountOptions) Copy() *CSIMountOptions { 174 if o == nil { 175 return nil 176 } 177 178 no := *o 179 no.MountFlags = slices.Clone(o.MountFlags) 180 return &no 181 } 182 183 func (o *CSIMountOptions) Merge(p *CSIMountOptions) { 184 if p == nil { 185 return 186 } 187 if p.FSType != "" { 188 o.FSType = p.FSType 189 } 190 if p.MountFlags != nil { 191 o.MountFlags = p.MountFlags 192 } 193 } 194 195 func (o *CSIMountOptions) Equal(p *CSIMountOptions) bool { 196 if o == nil && p == nil { 197 return true 198 } 199 if o == nil || p == nil { 200 return false 201 } 202 if o.FSType != p.FSType { 203 return false 204 } 205 return helper.SliceSetEq(o.MountFlags, p.MountFlags) 206 } 207 208 // CSIMountOptions implements the Stringer and GoStringer interfaces to prevent 209 // accidental leakage of sensitive mount flags via logs. 210 var _ fmt.Stringer = &CSIMountOptions{} 211 var _ fmt.GoStringer = &CSIMountOptions{} 212 213 func (o *CSIMountOptions) String() string { 214 mountFlagsString := "nil" 215 if len(o.MountFlags) != 0 { 216 mountFlagsString = "[REDACTED]" 217 } 218 219 return fmt.Sprintf("csi.CSIOptions(FSType: %s, MountFlags: %s)", o.FSType, mountFlagsString) 220 } 221 222 func (o *CSIMountOptions) GoString() string { 223 return o.String() 224 } 225 226 // CSISecrets contain optional additional configuration that can be used 227 // when specifying that a Volume should be used with VolumeAccessTypeMount. 228 type CSISecrets map[string]string 229 230 // CSISecrets implements the Stringer and GoStringer interfaces to prevent 231 // accidental leakage of secrets via logs. 232 var _ fmt.Stringer = &CSISecrets{} 233 var _ fmt.GoStringer = &CSISecrets{} 234 235 func (s *CSISecrets) String() string { 236 redacted := map[string]string{} 237 for k := range *s { 238 redacted[k] = "[REDACTED]" 239 } 240 return fmt.Sprintf("csi.CSISecrets(%v)", redacted) 241 } 242 243 func (s *CSISecrets) GoString() string { 244 return s.String() 245 } 246 247 type CSIVolumeClaim struct { 248 AllocationID string 249 NodeID string 250 ExternalNodeID string 251 Mode CSIVolumeClaimMode 252 AccessMode CSIVolumeAccessMode 253 AttachmentMode CSIVolumeAttachmentMode 254 State CSIVolumeClaimState 255 } 256 257 type CSIVolumeClaimState int 258 259 const ( 260 CSIVolumeClaimStateTaken CSIVolumeClaimState = iota 261 CSIVolumeClaimStateNodeDetached 262 CSIVolumeClaimStateControllerDetached 263 CSIVolumeClaimStateReadyToFree 264 CSIVolumeClaimStateUnpublishing 265 ) 266 267 // CSIVolume is the full representation of a CSI Volume 268 type CSIVolume struct { 269 // ID is a namespace unique URL safe identifier for the volume 270 ID string 271 // Name is a display name for the volume, not required to be unique 272 Name string 273 // ExternalID identifies the volume for the CSI interface, may be URL unsafe 274 ExternalID string 275 Namespace string 276 277 // RequestedTopologies are the topologies submitted as options to 278 // the storage provider at the time the volume was created. After 279 // volumes are created, this field is ignored. 280 RequestedTopologies *CSITopologyRequest 281 282 // Topologies are the topologies returned by the storage provider, 283 // based on the RequestedTopologies and what the storage provider 284 // could support. This value cannot be set by the user. 285 Topologies []*CSITopology 286 287 AccessMode CSIVolumeAccessMode // *current* access mode 288 AttachmentMode CSIVolumeAttachmentMode // *current* attachment mode 289 MountOptions *CSIMountOptions 290 291 Secrets CSISecrets 292 Parameters map[string]string 293 Context map[string]string 294 Capacity int64 // bytes 295 296 // These values are used only on volume creation but we record them 297 // so that we can diff the volume later 298 RequestedCapacityMin int64 // bytes 299 RequestedCapacityMax int64 // bytes 300 RequestedCapabilities []*CSIVolumeCapability 301 CloneID string 302 SnapshotID string 303 304 // Allocations, tracking claim status 305 ReadAllocs map[string]*Allocation // AllocID -> Allocation 306 WriteAllocs map[string]*Allocation // AllocID -> Allocation 307 308 ReadClaims map[string]*CSIVolumeClaim `json:"-"` // AllocID -> claim 309 WriteClaims map[string]*CSIVolumeClaim `json:"-"` // AllocID -> claim 310 PastClaims map[string]*CSIVolumeClaim `json:"-"` // AllocID -> claim 311 312 // Schedulable is true if all the denormalized plugin health fields are true, and the 313 // volume has not been marked for garbage collection 314 Schedulable bool 315 PluginID string 316 Provider string 317 ProviderVersion string 318 ControllerRequired bool 319 ControllersHealthy int 320 ControllersExpected int 321 NodesHealthy int 322 NodesExpected int 323 ResourceExhausted time.Time 324 325 CreateIndex uint64 326 ModifyIndex uint64 327 } 328 329 // GetID implements the IDGetter interface, required for pagination. 330 func (v *CSIVolume) GetID() string { 331 if v == nil { 332 return "" 333 } 334 return v.ID 335 } 336 337 // GetNamespace implements the NamespaceGetter interface, required for 338 // pagination. 339 func (v *CSIVolume) GetNamespace() string { 340 if v == nil { 341 return "" 342 } 343 return v.Namespace 344 } 345 346 // GetCreateIndex implements the CreateIndexGetter interface, required for 347 // pagination. 348 func (v *CSIVolume) GetCreateIndex() uint64 { 349 if v == nil { 350 return 0 351 } 352 return v.CreateIndex 353 } 354 355 // CSIVolListStub is partial representation of a CSI Volume for inclusion in lists 356 type CSIVolListStub struct { 357 ID string 358 Namespace string 359 Name string 360 ExternalID string 361 Topologies []*CSITopology 362 AccessMode CSIVolumeAccessMode 363 AttachmentMode CSIVolumeAttachmentMode 364 CurrentReaders int 365 CurrentWriters int 366 Schedulable bool 367 PluginID string 368 Provider string 369 ControllerRequired bool 370 ControllersHealthy int 371 ControllersExpected int 372 NodesHealthy int 373 NodesExpected int 374 ResourceExhausted time.Time 375 376 CreateIndex uint64 377 ModifyIndex uint64 378 } 379 380 // NewCSIVolume creates the volume struct. No side-effects 381 func NewCSIVolume(volumeID string, index uint64) *CSIVolume { 382 out := &CSIVolume{ 383 ID: volumeID, 384 CreateIndex: index, 385 ModifyIndex: index, 386 } 387 388 out.newStructs() 389 return out 390 } 391 392 func (v *CSIVolume) newStructs() { 393 v.Topologies = []*CSITopology{} 394 v.MountOptions = new(CSIMountOptions) 395 v.Secrets = CSISecrets{} 396 v.Parameters = map[string]string{} 397 v.Context = map[string]string{} 398 399 v.ReadAllocs = map[string]*Allocation{} 400 v.WriteAllocs = map[string]*Allocation{} 401 v.ReadClaims = map[string]*CSIVolumeClaim{} 402 v.WriteClaims = map[string]*CSIVolumeClaim{} 403 v.PastClaims = map[string]*CSIVolumeClaim{} 404 } 405 406 func (v *CSIVolume) RemoteID() string { 407 if v.ExternalID != "" { 408 return v.ExternalID 409 } 410 return v.ID 411 } 412 413 func (v *CSIVolume) Stub() *CSIVolListStub { 414 return &CSIVolListStub{ 415 ID: v.ID, 416 Namespace: v.Namespace, 417 Name: v.Name, 418 ExternalID: v.ExternalID, 419 Topologies: v.Topologies, 420 AccessMode: v.AccessMode, 421 AttachmentMode: v.AttachmentMode, 422 CurrentReaders: len(v.ReadAllocs), 423 CurrentWriters: len(v.WriteAllocs), 424 Schedulable: v.Schedulable, 425 PluginID: v.PluginID, 426 Provider: v.Provider, 427 ControllerRequired: v.ControllerRequired, 428 ControllersHealthy: v.ControllersHealthy, 429 ControllersExpected: v.ControllersExpected, 430 NodesHealthy: v.NodesHealthy, 431 NodesExpected: v.NodesExpected, 432 ResourceExhausted: v.ResourceExhausted, 433 CreateIndex: v.CreateIndex, 434 ModifyIndex: v.ModifyIndex, 435 } 436 } 437 438 // ReadSchedulable determines if the volume is potentially schedulable 439 // for reads, considering only the volume capabilities and plugin 440 // health 441 func (v *CSIVolume) ReadSchedulable() bool { 442 if !v.Schedulable { 443 return false 444 } 445 446 return v.ResourceExhausted == time.Time{} 447 } 448 449 // WriteSchedulable determines if the volume is potentially 450 // schedulable for writes, considering only volume capabilities and 451 // plugin health 452 func (v *CSIVolume) WriteSchedulable() bool { 453 if !v.Schedulable { 454 return false 455 } 456 457 switch v.AccessMode { 458 case CSIVolumeAccessModeSingleNodeWriter, 459 CSIVolumeAccessModeMultiNodeSingleWriter, 460 CSIVolumeAccessModeMultiNodeMultiWriter: 461 return v.ResourceExhausted == time.Time{} 462 463 case CSIVolumeAccessModeUnknown: 464 // this volume was created but not currently claimed, so we check what 465 // it's capable of, not what it's been previously assigned 466 for _, cap := range v.RequestedCapabilities { 467 switch cap.AccessMode { 468 case CSIVolumeAccessModeSingleNodeWriter, 469 CSIVolumeAccessModeMultiNodeSingleWriter, 470 CSIVolumeAccessModeMultiNodeMultiWriter: 471 return v.ResourceExhausted == time.Time{} 472 } 473 } 474 } 475 return false 476 } 477 478 // HasFreeReadClaims determines if there are any free read claims available 479 func (v *CSIVolume) HasFreeReadClaims() bool { 480 switch v.AccessMode { 481 case CSIVolumeAccessModeSingleNodeReader: 482 return len(v.ReadClaims) == 0 483 case CSIVolumeAccessModeSingleNodeWriter: 484 return len(v.ReadClaims) == 0 && len(v.WriteClaims) == 0 485 case CSIVolumeAccessModeUnknown: 486 // This volume was created but not yet claimed, so its 487 // capabilities have been checked in ReadSchedulable 488 return true 489 default: 490 // For multi-node AccessModes, the CSI spec doesn't allow for 491 // setting a max number of readers we track node resource 492 // exhaustion through v.ResourceExhausted which is checked in 493 // ReadSchedulable 494 return true 495 } 496 } 497 498 // HasFreeWriteClaims determines if there are any free write claims available 499 func (v *CSIVolume) HasFreeWriteClaims() bool { 500 switch v.AccessMode { 501 case CSIVolumeAccessModeSingleNodeWriter, CSIVolumeAccessModeMultiNodeSingleWriter: 502 return len(v.WriteClaims) == 0 503 case CSIVolumeAccessModeMultiNodeMultiWriter: 504 // the CSI spec doesn't allow for setting a max number of writers. 505 // we track node resource exhaustion through v.ResourceExhausted 506 // which is checked in WriteSchedulable 507 return true 508 case CSIVolumeAccessModeUnknown: 509 // This volume was created but not yet claimed, so its 510 // capabilities have been checked in WriteSchedulable 511 return true 512 default: 513 // Reader modes never have free write claims 514 return false 515 } 516 } 517 518 // InUse tests whether any allocations are actively using the volume 519 func (v *CSIVolume) InUse() bool { 520 return len(v.ReadAllocs) != 0 || 521 len(v.WriteAllocs) != 0 522 } 523 524 // Copy returns a copy of the volume, which shares only the Topologies slice 525 func (v *CSIVolume) Copy() *CSIVolume { 526 out := new(CSIVolume) 527 *out = *v 528 out.newStructs() // zero-out the non-primitive structs 529 530 for _, t := range v.Topologies { 531 out.Topologies = append(out.Topologies, t.Copy()) 532 } 533 if v.MountOptions != nil { 534 *out.MountOptions = *v.MountOptions 535 } 536 for k, v := range v.Secrets { 537 out.Secrets[k] = v 538 } 539 for k, v := range v.Parameters { 540 out.Parameters[k] = v 541 } 542 for k, v := range v.Context { 543 out.Context[k] = v 544 } 545 546 for k, alloc := range v.ReadAllocs { 547 out.ReadAllocs[k] = alloc.Copy() 548 } 549 for k, alloc := range v.WriteAllocs { 550 out.WriteAllocs[k] = alloc.Copy() 551 } 552 553 for k, v := range v.ReadClaims { 554 claim := *v 555 out.ReadClaims[k] = &claim 556 } 557 for k, v := range v.WriteClaims { 558 claim := *v 559 out.WriteClaims[k] = &claim 560 } 561 for k, v := range v.PastClaims { 562 claim := *v 563 out.PastClaims[k] = &claim 564 } 565 566 return out 567 } 568 569 // Claim updates the allocations and changes the volume state 570 func (v *CSIVolume) Claim(claim *CSIVolumeClaim, alloc *Allocation) error { 571 // COMPAT: volumes registered prior to 1.1.0 will be missing caps for the 572 // volume on any claim. Correct this when we make the first change to a 573 // claim by setting its currently claimed capability as the only requested 574 // capability 575 if len(v.RequestedCapabilities) == 0 && v.AccessMode != "" && v.AttachmentMode != "" { 576 v.RequestedCapabilities = []*CSIVolumeCapability{ 577 { 578 AccessMode: v.AccessMode, 579 AttachmentMode: v.AttachmentMode, 580 }, 581 } 582 } 583 if v.AttachmentMode != CSIVolumeAttachmentModeUnknown && 584 claim.AttachmentMode != CSIVolumeAttachmentModeUnknown && 585 v.AttachmentMode != claim.AttachmentMode { 586 return fmt.Errorf("cannot change attachment mode of claimed volume") 587 } 588 589 if claim.State == CSIVolumeClaimStateTaken { 590 switch claim.Mode { 591 case CSIVolumeClaimRead: 592 return v.claimRead(claim, alloc) 593 case CSIVolumeClaimWrite: 594 return v.claimWrite(claim, alloc) 595 } 596 } 597 // either GC or a Unpublish checkpoint 598 return v.claimRelease(claim) 599 } 600 601 // claimRead marks an allocation as using a volume read-only 602 func (v *CSIVolume) claimRead(claim *CSIVolumeClaim, alloc *Allocation) error { 603 if _, ok := v.ReadAllocs[claim.AllocationID]; ok { 604 return nil 605 } 606 if alloc == nil { 607 return fmt.Errorf("allocation missing: %s", claim.AllocationID) 608 } 609 610 if !v.ReadSchedulable() { 611 return ErrCSIVolumeUnschedulable 612 } 613 614 if !v.HasFreeReadClaims() { 615 return ErrCSIVolumeMaxClaims 616 } 617 618 // Allocations are copy on write, so we want to keep the id but don't need the 619 // pointer. We'll get it from the db in denormalize. 620 v.ReadAllocs[claim.AllocationID] = nil 621 delete(v.WriteAllocs, claim.AllocationID) 622 623 v.ReadClaims[claim.AllocationID] = claim 624 delete(v.WriteClaims, claim.AllocationID) 625 delete(v.PastClaims, claim.AllocationID) 626 627 v.setModesFromClaim(claim) 628 return nil 629 } 630 631 // claimWrite marks an allocation as using a volume as a writer 632 func (v *CSIVolume) claimWrite(claim *CSIVolumeClaim, alloc *Allocation) error { 633 if _, ok := v.WriteAllocs[claim.AllocationID]; ok { 634 return nil 635 } 636 if alloc == nil { 637 return fmt.Errorf("allocation missing: %s", claim.AllocationID) 638 } 639 640 if !v.WriteSchedulable() { 641 return ErrCSIVolumeUnschedulable 642 } 643 644 if !v.HasFreeWriteClaims() { 645 return ErrCSIVolumeMaxClaims 646 } 647 648 // Allocations are copy on write, so we want to keep the id but don't need the 649 // pointer. We'll get it from the db in denormalize. 650 v.WriteAllocs[alloc.ID] = nil 651 delete(v.ReadAllocs, alloc.ID) 652 653 v.WriteClaims[alloc.ID] = claim 654 delete(v.ReadClaims, alloc.ID) 655 delete(v.PastClaims, alloc.ID) 656 657 v.setModesFromClaim(claim) 658 return nil 659 } 660 661 // setModesFromClaim sets the volume AttachmentMode and AccessMode based on 662 // the first claim we make. Originally the volume AccessMode and 663 // AttachmentMode were set during registration, but this is incorrect once we 664 // started creating volumes ourselves. But we still want these values for CLI 665 // and UI status. 666 func (v *CSIVolume) setModesFromClaim(claim *CSIVolumeClaim) { 667 if v.AttachmentMode == CSIVolumeAttachmentModeUnknown { 668 v.AttachmentMode = claim.AttachmentMode 669 } 670 if v.AccessMode == CSIVolumeAccessModeUnknown { 671 v.AccessMode = claim.AccessMode 672 } 673 } 674 675 // claimRelease is called when the allocation has terminated and 676 // already stopped using the volume 677 func (v *CSIVolume) claimRelease(claim *CSIVolumeClaim) error { 678 if claim.State == CSIVolumeClaimStateReadyToFree { 679 delete(v.ReadAllocs, claim.AllocationID) 680 delete(v.WriteAllocs, claim.AllocationID) 681 delete(v.ReadClaims, claim.AllocationID) 682 delete(v.WriteClaims, claim.AllocationID) 683 delete(v.PastClaims, claim.AllocationID) 684 685 // remove AccessMode/AttachmentMode if this is the last claim 686 if len(v.ReadClaims) == 0 && len(v.WriteClaims) == 0 && len(v.PastClaims) == 0 { 687 v.AccessMode = CSIVolumeAccessModeUnknown 688 v.AttachmentMode = CSIVolumeAttachmentModeUnknown 689 } 690 } else { 691 v.PastClaims[claim.AllocationID] = claim 692 } 693 return nil 694 } 695 696 // Equal checks equality by value. 697 func (v *CSIVolume) Equal(o *CSIVolume) bool { 698 if v == nil || o == nil { 699 return v == o 700 } 701 702 // Omit the plugin health fields, their values are controlled by plugin jobs 703 if v.ID == o.ID && 704 v.Namespace == o.Namespace && 705 v.AccessMode == o.AccessMode && 706 v.AttachmentMode == o.AttachmentMode && 707 v.PluginID == o.PluginID { 708 // Setwise equality of topologies 709 var ok bool 710 for _, t := range v.Topologies { 711 ok = false 712 for _, u := range o.Topologies { 713 if t.Equal(u) { 714 ok = true 715 break 716 } 717 } 718 if !ok { 719 return false 720 } 721 } 722 return true 723 } 724 return false 725 } 726 727 // Validate validates the volume struct, returning all validation errors at once 728 func (v *CSIVolume) Validate() error { 729 errs := []string{} 730 731 if v.ID == "" { 732 errs = append(errs, "missing volume id") 733 } 734 if v.PluginID == "" { 735 errs = append(errs, "missing plugin id") 736 } 737 if v.Namespace == "" { 738 errs = append(errs, "missing namespace") 739 } 740 if v.SnapshotID != "" && v.CloneID != "" { 741 errs = append(errs, "only one of snapshot_id and clone_id is allowed") 742 } 743 if len(v.RequestedCapabilities) == 0 { 744 errs = append(errs, "must include at least one capability block") 745 } 746 if v.RequestedTopologies != nil { 747 for _, t := range v.RequestedTopologies.Required { 748 if t != nil && len(t.Segments) == 0 { 749 errs = append(errs, "required topology is missing segments field") 750 } 751 } 752 for _, t := range v.RequestedTopologies.Preferred { 753 if t != nil && len(t.Segments) == 0 { 754 errs = append(errs, "preferred topology is missing segments field") 755 } 756 } 757 } 758 if len(errs) > 0 { 759 return fmt.Errorf("validation: %s", strings.Join(errs, ", ")) 760 } 761 return nil 762 } 763 764 // Merge updates the mutable fields of a volume with those from 765 // another volume. CSIVolume has many user-defined fields which are 766 // immutable once set, and many fields that are not 767 // user-settable. Merge will return an error if we try to mutate the 768 // user-defined immutable fields after they're set, but silently 769 // ignore fields that are controlled by Nomad. 770 func (v *CSIVolume) Merge(other *CSIVolume) error { 771 if other == nil { 772 return nil 773 } 774 775 var errs *multierror.Error 776 777 if v.Name != other.Name && other.Name != "" { 778 errs = multierror.Append(errs, errors.New("volume name cannot be updated")) 779 } 780 if v.ExternalID != other.ExternalID && other.ExternalID != "" { 781 errs = multierror.Append(errs, errors.New( 782 "volume external ID cannot be updated")) 783 } 784 if v.PluginID != other.PluginID { 785 errs = multierror.Append(errs, errors.New( 786 "volume plugin ID cannot be updated")) 787 } 788 if v.CloneID != other.CloneID && other.CloneID != "" { 789 errs = multierror.Append(errs, errors.New( 790 "volume clone ID cannot be updated")) 791 } 792 if v.SnapshotID != other.SnapshotID && other.SnapshotID != "" { 793 errs = multierror.Append(errs, errors.New( 794 "volume snapshot ID cannot be updated")) 795 } 796 797 // must be compatible with capacity range 798 // TODO: when ExpandVolume is implemented we'll need to update 799 // this logic https://github.com/hashicorp/nomad/issues/10324 800 if v.Capacity != 0 { 801 if other.RequestedCapacityMax < v.Capacity || 802 other.RequestedCapacityMin > v.Capacity { 803 errs = multierror.Append(errs, errors.New( 804 "volume requested capacity update was not compatible with existing capacity")) 805 } else { 806 v.RequestedCapacityMin = other.RequestedCapacityMin 807 v.RequestedCapacityMax = other.RequestedCapacityMax 808 } 809 } 810 811 // must be compatible with volume_capabilities 812 if v.AccessMode != CSIVolumeAccessModeUnknown || 813 v.AttachmentMode != CSIVolumeAttachmentModeUnknown { 814 var ok bool 815 for _, cap := range other.RequestedCapabilities { 816 if cap.AccessMode == v.AccessMode && 817 cap.AttachmentMode == v.AttachmentMode { 818 ok = true 819 break 820 } 821 } 822 if ok { 823 v.RequestedCapabilities = other.RequestedCapabilities 824 } else { 825 errs = multierror.Append(errs, errors.New( 826 "volume requested capabilities update was not compatible with existing capability in use")) 827 } 828 } else { 829 v.RequestedCapabilities = other.RequestedCapabilities 830 } 831 832 // topologies are immutable, so topology request changes must be 833 // compatible with the existing topology, if any 834 if len(v.Topologies) > 0 { 835 if !v.RequestedTopologies.Equal(other.RequestedTopologies) { 836 errs = multierror.Append(errs, errors.New( 837 "volume topology request update was not compatible with existing topology")) 838 } 839 } 840 841 // MountOptions can be updated so long as the volume isn't in use, 842 // but the caller will reject updating an in-use volume 843 v.MountOptions = other.MountOptions 844 845 // Secrets can be updated freely 846 v.Secrets = other.Secrets 847 848 // must be compatible with parameters set by from CreateVolumeResponse 849 850 if len(other.Parameters) != 0 && !maps.Equal(v.Parameters, other.Parameters) { 851 errs = multierror.Append(errs, errors.New( 852 "volume parameters cannot be updated")) 853 } 854 855 // Context is mutable and will be used during controller 856 // validation 857 v.Context = other.Context 858 return errs.ErrorOrNil() 859 } 860 861 // Request and response wrappers 862 type CSIVolumeRegisterRequest struct { 863 Volumes []*CSIVolume 864 WriteRequest 865 } 866 867 type CSIVolumeRegisterResponse struct { 868 QueryMeta 869 } 870 871 type CSIVolumeDeregisterRequest struct { 872 VolumeIDs []string 873 Force bool 874 WriteRequest 875 } 876 877 type CSIVolumeDeregisterResponse struct { 878 QueryMeta 879 } 880 881 type CSIVolumeCreateRequest struct { 882 Volumes []*CSIVolume 883 WriteRequest 884 } 885 886 type CSIVolumeCreateResponse struct { 887 Volumes []*CSIVolume 888 QueryMeta 889 } 890 891 type CSIVolumeDeleteRequest struct { 892 VolumeIDs []string 893 Secrets CSISecrets 894 WriteRequest 895 } 896 897 type CSIVolumeDeleteResponse struct { 898 QueryMeta 899 } 900 901 type CSIVolumeClaimMode int 902 903 const ( 904 CSIVolumeClaimRead CSIVolumeClaimMode = iota 905 CSIVolumeClaimWrite 906 907 // for GC we don't have a specific claim to set the state on, so instead we 908 // create a new claim for GC in order to bump the ModifyIndex and trigger 909 // volumewatcher 910 CSIVolumeClaimGC 911 ) 912 913 type CSIVolumeClaimBatchRequest struct { 914 Claims []CSIVolumeClaimRequest 915 } 916 917 type CSIVolumeClaimRequest struct { 918 VolumeID string 919 AllocationID string 920 NodeID string 921 ExternalNodeID string 922 Claim CSIVolumeClaimMode 923 AccessMode CSIVolumeAccessMode 924 AttachmentMode CSIVolumeAttachmentMode 925 State CSIVolumeClaimState 926 WriteRequest 927 } 928 929 func (req *CSIVolumeClaimRequest) ToClaim() *CSIVolumeClaim { 930 return &CSIVolumeClaim{ 931 AllocationID: req.AllocationID, 932 NodeID: req.NodeID, 933 ExternalNodeID: req.ExternalNodeID, 934 Mode: req.Claim, 935 AccessMode: req.AccessMode, 936 AttachmentMode: req.AttachmentMode, 937 State: req.State, 938 } 939 } 940 941 type CSIVolumeClaimResponse struct { 942 // Opaque static publish properties of the volume. SP MAY use this 943 // field to ensure subsequent `NodeStageVolume` or `NodePublishVolume` 944 // calls calls have contextual information. 945 // The contents of this field SHALL be opaque to nomad. 946 // The contents of this field SHALL NOT be mutable. 947 // The contents of this field SHALL be safe for the nomad to cache. 948 // The contents of this field SHOULD NOT contain sensitive 949 // information. 950 // The contents of this field SHOULD NOT be used for uniquely 951 // identifying a volume. The `volume_id` alone SHOULD be sufficient to 952 // identify the volume. 953 // This field is OPTIONAL and when present MUST be passed to 954 // `NodeStageVolume` or `NodePublishVolume` calls on the client 955 PublishContext map[string]string 956 957 // Volume contains the expanded CSIVolume for use on the client after a Claim 958 // has completed. 959 Volume *CSIVolume 960 961 QueryMeta 962 } 963 964 type CSIVolumeListRequest struct { 965 PluginID string 966 NodeID string 967 QueryOptions 968 } 969 970 type CSIVolumeListResponse struct { 971 Volumes []*CSIVolListStub 972 QueryMeta 973 } 974 975 // CSIVolumeExternalListRequest is a request to a controller plugin to list 976 // all the volumes known to the the storage provider. This request is 977 // paginated by the plugin and accepts the QueryOptions.PerPage and 978 // QueryOptions.NextToken fields 979 type CSIVolumeExternalListRequest struct { 980 PluginID string 981 QueryOptions 982 } 983 984 type CSIVolumeExternalListResponse struct { 985 Volumes []*CSIVolumeExternalStub 986 NextToken string 987 QueryMeta 988 } 989 990 // CSIVolumeExternalStub is the storage provider's view of a volume, as 991 // returned from the controller plugin; all IDs are for external resources 992 type CSIVolumeExternalStub struct { 993 ExternalID string 994 CapacityBytes int64 995 VolumeContext map[string]string 996 CloneID string 997 SnapshotID string 998 999 PublishedExternalNodeIDs []string 1000 IsAbnormal bool 1001 Status string 1002 } 1003 1004 type CSIVolumeGetRequest struct { 1005 ID string 1006 QueryOptions 1007 } 1008 1009 type CSIVolumeGetResponse struct { 1010 Volume *CSIVolume 1011 QueryMeta 1012 } 1013 1014 type CSIVolumeUnpublishRequest struct { 1015 VolumeID string 1016 Claim *CSIVolumeClaim 1017 WriteRequest 1018 } 1019 1020 type CSIVolumeUnpublishResponse struct { 1021 QueryMeta 1022 } 1023 1024 // CSISnapshot is the storage provider's view of a volume snapshot 1025 type CSISnapshot struct { 1026 // These fields map to those returned by the storage provider plugin 1027 ID string // storage provider's ID 1028 ExternalSourceVolumeID string // storage provider's ID for volume 1029 SizeBytes int64 1030 CreateTime int64 1031 IsReady bool 1032 1033 // These fields are controlled by Nomad 1034 SourceVolumeID string 1035 PluginID string 1036 1037 // These field are only used during snapshot creation and will not be 1038 // populated when the snapshot is returned 1039 Name string 1040 Secrets CSISecrets 1041 Parameters map[string]string 1042 } 1043 1044 type CSISnapshotCreateRequest struct { 1045 Snapshots []*CSISnapshot 1046 WriteRequest 1047 } 1048 1049 type CSISnapshotCreateResponse struct { 1050 Snapshots []*CSISnapshot 1051 QueryMeta 1052 } 1053 1054 type CSISnapshotDeleteRequest struct { 1055 Snapshots []*CSISnapshot 1056 WriteRequest 1057 } 1058 1059 type CSISnapshotDeleteResponse struct { 1060 QueryMeta 1061 } 1062 1063 // CSISnapshotListRequest is a request to a controller plugin to list all the 1064 // snapshot known to the the storage provider. This request is paginated by 1065 // the plugin and accepts the QueryOptions.PerPage and QueryOptions.NextToken 1066 // fields 1067 type CSISnapshotListRequest struct { 1068 PluginID string 1069 Secrets CSISecrets 1070 QueryOptions 1071 } 1072 1073 type CSISnapshotListResponse struct { 1074 Snapshots []*CSISnapshot 1075 NextToken string 1076 QueryMeta 1077 } 1078 1079 // CSIPlugin collects fingerprint info context for the plugin for clients 1080 type CSIPlugin struct { 1081 ID string 1082 Provider string // the vendor name from CSI GetPluginInfoResponse 1083 Version string // the vendor verson from CSI GetPluginInfoResponse 1084 ControllerRequired bool 1085 1086 // Map Node.IDs to fingerprint results, split by type. Monolith type plugins have 1087 // both sets of fingerprinting results. 1088 Controllers map[string]*CSIInfo 1089 Nodes map[string]*CSIInfo 1090 1091 // Allocations are populated by denormalize to show running allocations 1092 Allocations []*AllocListStub 1093 1094 // Jobs are populated to by job update to support expected counts and the UI 1095 ControllerJobs JobDescriptions 1096 NodeJobs JobDescriptions 1097 1098 // Cache the count of healthy plugins 1099 ControllersHealthy int 1100 ControllersExpected int 1101 NodesHealthy int 1102 NodesExpected int 1103 1104 CreateIndex uint64 1105 ModifyIndex uint64 1106 } 1107 1108 // NewCSIPlugin creates the plugin struct. No side-effects 1109 func NewCSIPlugin(id string, index uint64) *CSIPlugin { 1110 out := &CSIPlugin{ 1111 ID: id, 1112 CreateIndex: index, 1113 ModifyIndex: index, 1114 } 1115 1116 out.newStructs() 1117 return out 1118 } 1119 1120 func (p *CSIPlugin) newStructs() { 1121 p.Controllers = map[string]*CSIInfo{} 1122 p.Nodes = map[string]*CSIInfo{} 1123 p.ControllerJobs = make(JobDescriptions) 1124 p.NodeJobs = make(JobDescriptions) 1125 } 1126 1127 func (p *CSIPlugin) Copy() *CSIPlugin { 1128 copy := *p 1129 out := © 1130 out.newStructs() 1131 1132 for k, v := range p.Controllers { 1133 out.Controllers[k] = v.Copy() 1134 } 1135 1136 for k, v := range p.Nodes { 1137 out.Nodes[k] = v.Copy() 1138 } 1139 1140 for k, v := range p.ControllerJobs { 1141 out.ControllerJobs[k] = v.Copy() 1142 } 1143 1144 for k, v := range p.NodeJobs { 1145 out.NodeJobs[k] = v.Copy() 1146 } 1147 1148 return out 1149 } 1150 1151 type CSIControllerCapability byte 1152 1153 const ( 1154 // CSIControllerSupportsCreateDelete indicates plugin support for 1155 // CREATE_DELETE_VOLUME 1156 CSIControllerSupportsCreateDelete CSIControllerCapability = 0 1157 1158 // CSIControllerSupportsAttachDetach is true when the controller 1159 // implements the methods required to attach and detach volumes. If this 1160 // is false Nomad should skip the controller attachment flow. 1161 CSIControllerSupportsAttachDetach CSIControllerCapability = 1 1162 1163 // CSIControllerSupportsListVolumes is true when the controller implements 1164 // the ListVolumes RPC. NOTE: This does not guarantee that attached nodes 1165 // will be returned unless SupportsListVolumesAttachedNodes is also true. 1166 CSIControllerSupportsListVolumes CSIControllerCapability = 2 1167 1168 // CSIControllerSupportsGetCapacity indicates plugin support for 1169 // GET_CAPACITY 1170 CSIControllerSupportsGetCapacity CSIControllerCapability = 3 1171 1172 // CSIControllerSupportsCreateDeleteSnapshot indicates plugin support for 1173 // CREATE_DELETE_SNAPSHOT 1174 CSIControllerSupportsCreateDeleteSnapshot CSIControllerCapability = 4 1175 1176 // CSIControllerSupportsListSnapshots indicates plugin support for 1177 // LIST_SNAPSHOTS 1178 CSIControllerSupportsListSnapshots CSIControllerCapability = 5 1179 1180 // CSIControllerSupportsClone indicates plugin support for CLONE_VOLUME 1181 CSIControllerSupportsClone CSIControllerCapability = 6 1182 1183 // CSIControllerSupportsReadOnlyAttach is set to true when the controller 1184 // returns the ATTACH_READONLY capability. 1185 CSIControllerSupportsReadOnlyAttach CSIControllerCapability = 7 1186 1187 // CSIControllerSupportsExpand indicates plugin support for EXPAND_VOLUME 1188 CSIControllerSupportsExpand CSIControllerCapability = 8 1189 1190 // CSIControllerSupportsListVolumesAttachedNodes indicates whether the 1191 // plugin will return attached nodes data when making ListVolume RPCs 1192 // (plugin support for LIST_VOLUMES_PUBLISHED_NODES) 1193 CSIControllerSupportsListVolumesAttachedNodes CSIControllerCapability = 9 1194 1195 // CSIControllerSupportsCondition indicates plugin support for 1196 // VOLUME_CONDITION 1197 CSIControllerSupportsCondition CSIControllerCapability = 10 1198 1199 // CSIControllerSupportsGet indicates plugin support for GET_VOLUME 1200 CSIControllerSupportsGet CSIControllerCapability = 11 1201 ) 1202 1203 type CSINodeCapability byte 1204 1205 const ( 1206 1207 // CSINodeSupportsStageVolume indicates whether the client should 1208 // Stage/Unstage volumes on this node. 1209 CSINodeSupportsStageVolume CSINodeCapability = 0 1210 1211 // CSINodeSupportsStats indicates plugin support for GET_VOLUME_STATS 1212 CSINodeSupportsStats CSINodeCapability = 1 1213 1214 // CSINodeSupportsExpand indicates plugin support for EXPAND_VOLUME 1215 CSINodeSupportsExpand CSINodeCapability = 2 1216 1217 // CSINodeSupportsCondition indicates plugin support for VOLUME_CONDITION 1218 CSINodeSupportsCondition CSINodeCapability = 3 1219 ) 1220 1221 func (p *CSIPlugin) HasControllerCapability(cap CSIControllerCapability) bool { 1222 if len(p.Controllers) < 1 { 1223 return false 1224 } 1225 // we're picking the first controller because they should be uniform 1226 // across the same version of the plugin 1227 for _, c := range p.Controllers { 1228 switch cap { 1229 case CSIControllerSupportsCreateDelete: 1230 return c.ControllerInfo.SupportsCreateDelete 1231 case CSIControllerSupportsAttachDetach: 1232 return c.ControllerInfo.SupportsAttachDetach 1233 case CSIControllerSupportsListVolumes: 1234 return c.ControllerInfo.SupportsListVolumes 1235 case CSIControllerSupportsGetCapacity: 1236 return c.ControllerInfo.SupportsGetCapacity 1237 case CSIControllerSupportsCreateDeleteSnapshot: 1238 return c.ControllerInfo.SupportsCreateDeleteSnapshot 1239 case CSIControllerSupportsListSnapshots: 1240 return c.ControllerInfo.SupportsListSnapshots 1241 case CSIControllerSupportsClone: 1242 return c.ControllerInfo.SupportsClone 1243 case CSIControllerSupportsReadOnlyAttach: 1244 return c.ControllerInfo.SupportsReadOnlyAttach 1245 case CSIControllerSupportsExpand: 1246 return c.ControllerInfo.SupportsExpand 1247 case CSIControllerSupportsListVolumesAttachedNodes: 1248 return c.ControllerInfo.SupportsListVolumesAttachedNodes 1249 case CSIControllerSupportsCondition: 1250 return c.ControllerInfo.SupportsCondition 1251 case CSIControllerSupportsGet: 1252 return c.ControllerInfo.SupportsGet 1253 default: 1254 return false 1255 } 1256 } 1257 return false 1258 } 1259 1260 func (p *CSIPlugin) HasNodeCapability(cap CSINodeCapability) bool { 1261 if len(p.Nodes) < 1 { 1262 return false 1263 } 1264 // we're picking the first node because they should be uniform 1265 // across the same version of the plugin 1266 for _, c := range p.Nodes { 1267 switch cap { 1268 case CSINodeSupportsStageVolume: 1269 return c.NodeInfo.RequiresNodeStageVolume 1270 case CSINodeSupportsStats: 1271 return c.NodeInfo.SupportsStats 1272 case CSINodeSupportsExpand: 1273 return c.NodeInfo.SupportsExpand 1274 case CSINodeSupportsCondition: 1275 return c.NodeInfo.SupportsCondition 1276 default: 1277 return false 1278 } 1279 } 1280 return false 1281 } 1282 1283 // AddPlugin adds a single plugin running on the node. Called from state.NodeUpdate in a 1284 // transaction 1285 func (p *CSIPlugin) AddPlugin(nodeID string, info *CSIInfo) error { 1286 if info.ControllerInfo != nil { 1287 p.ControllerRequired = info.RequiresControllerPlugin 1288 prev, ok := p.Controllers[nodeID] 1289 if ok { 1290 if prev == nil { 1291 return fmt.Errorf("plugin missing controller: %s", nodeID) 1292 } 1293 if prev.Healthy { 1294 p.ControllersHealthy -= 1 1295 } 1296 } 1297 1298 // note: for this to work as expected, only a single 1299 // controller for a given plugin can be on a given Nomad 1300 // client, they also conflict on the client so this should be 1301 // ok 1302 if prev != nil || info.Healthy { 1303 p.Controllers[nodeID] = info 1304 } 1305 if info.Healthy { 1306 p.ControllersHealthy += 1 1307 } 1308 } 1309 1310 if info.NodeInfo != nil { 1311 prev, ok := p.Nodes[nodeID] 1312 if ok { 1313 if prev == nil { 1314 return fmt.Errorf("plugin missing node: %s", nodeID) 1315 } 1316 if prev.Healthy { 1317 p.NodesHealthy -= 1 1318 } 1319 } 1320 if prev != nil || info.Healthy { 1321 p.Nodes[nodeID] = info 1322 } 1323 if info.Healthy { 1324 p.NodesHealthy += 1 1325 } 1326 } 1327 1328 return nil 1329 } 1330 1331 // DeleteNode removes all plugins from the node. Called from state.DeleteNode in a 1332 // transaction 1333 func (p *CSIPlugin) DeleteNode(nodeID string) error { 1334 return p.DeleteNodeForType(nodeID, CSIPluginTypeMonolith) 1335 } 1336 1337 // DeleteNodeForType deletes a client node from the list of controllers or node instance of 1338 // a plugin. Called from deleteJobFromPlugin during job deregistration, in a transaction 1339 func (p *CSIPlugin) DeleteNodeForType(nodeID string, pluginType CSIPluginType) error { 1340 switch pluginType { 1341 case CSIPluginTypeController: 1342 if prev, ok := p.Controllers[nodeID]; ok { 1343 if prev == nil { 1344 return fmt.Errorf("plugin missing controller: %s", nodeID) 1345 } 1346 if prev.Healthy { 1347 p.ControllersHealthy-- 1348 } 1349 delete(p.Controllers, nodeID) 1350 } 1351 1352 case CSIPluginTypeNode: 1353 if prev, ok := p.Nodes[nodeID]; ok { 1354 if prev == nil { 1355 return fmt.Errorf("plugin missing node: %s", nodeID) 1356 } 1357 if prev.Healthy { 1358 p.NodesHealthy-- 1359 } 1360 delete(p.Nodes, nodeID) 1361 } 1362 1363 case CSIPluginTypeMonolith: 1364 var result error 1365 1366 err := p.DeleteNodeForType(nodeID, CSIPluginTypeController) 1367 if err != nil { 1368 result = multierror.Append(result, err) 1369 } 1370 1371 err = p.DeleteNodeForType(nodeID, CSIPluginTypeNode) 1372 if err != nil { 1373 result = multierror.Append(result, err) 1374 } 1375 1376 return result 1377 } 1378 1379 return nil 1380 } 1381 1382 // DeleteAlloc removes the fingerprint info for the allocation 1383 func (p *CSIPlugin) DeleteAlloc(allocID, nodeID string) error { 1384 prev, ok := p.Controllers[nodeID] 1385 if ok { 1386 if prev == nil { 1387 return fmt.Errorf("plugin missing controller: %s", nodeID) 1388 } 1389 if prev.AllocID == allocID { 1390 if prev.Healthy { 1391 p.ControllersHealthy -= 1 1392 } 1393 delete(p.Controllers, nodeID) 1394 } 1395 } 1396 1397 prev, ok = p.Nodes[nodeID] 1398 if ok { 1399 if prev == nil { 1400 return fmt.Errorf("plugin missing node: %s", nodeID) 1401 } 1402 if prev.AllocID == allocID { 1403 if prev.Healthy { 1404 p.NodesHealthy -= 1 1405 } 1406 delete(p.Nodes, nodeID) 1407 } 1408 } 1409 1410 return nil 1411 } 1412 1413 // AddJob adds a job to the plugin and increments expected 1414 func (p *CSIPlugin) AddJob(job *Job, summary *JobSummary) { 1415 p.UpdateExpectedWithJob(job, summary, false) 1416 } 1417 1418 // DeleteJob removes the job from the plugin and decrements expected 1419 func (p *CSIPlugin) DeleteJob(job *Job, summary *JobSummary) { 1420 p.UpdateExpectedWithJob(job, summary, true) 1421 } 1422 1423 // UpdateExpectedWithJob maintains the expected instance count 1424 // we use the summary to add non-allocation expected counts 1425 func (p *CSIPlugin) UpdateExpectedWithJob(job *Job, summary *JobSummary, terminal bool) { 1426 var count int 1427 1428 for _, tg := range job.TaskGroups { 1429 if job.Type == JobTypeSystem { 1430 if summary == nil { 1431 continue 1432 } 1433 1434 s, ok := summary.Summary[tg.Name] 1435 if !ok { 1436 continue 1437 } 1438 1439 count = s.Running + s.Queued + s.Starting 1440 } else { 1441 count = tg.Count 1442 } 1443 1444 for _, t := range tg.Tasks { 1445 if t.CSIPluginConfig == nil || 1446 t.CSIPluginConfig.ID != p.ID { 1447 continue 1448 } 1449 1450 // Change the correct plugin expected, monolith should change both 1451 if t.CSIPluginConfig.Type == CSIPluginTypeController || 1452 t.CSIPluginConfig.Type == CSIPluginTypeMonolith { 1453 if terminal { 1454 p.ControllerJobs.Delete(job) 1455 } else { 1456 p.ControllerJobs.Add(job, count) 1457 } 1458 } 1459 1460 if t.CSIPluginConfig.Type == CSIPluginTypeNode || 1461 t.CSIPluginConfig.Type == CSIPluginTypeMonolith { 1462 if terminal { 1463 p.NodeJobs.Delete(job) 1464 } else { 1465 p.NodeJobs.Add(job, count) 1466 } 1467 } 1468 } 1469 } 1470 1471 p.ControllersExpected = p.ControllerJobs.Count() 1472 p.NodesExpected = p.NodeJobs.Count() 1473 } 1474 1475 // JobDescription records Job identification and the count of expected plugin instances 1476 type JobDescription struct { 1477 Namespace string 1478 ID string 1479 Expected int 1480 } 1481 1482 // JobNamespacedDescriptions maps Job.ID to JobDescription 1483 type JobNamespacedDescriptions map[string]JobDescription 1484 1485 func (j JobNamespacedDescriptions) Copy() JobNamespacedDescriptions { 1486 copy := JobNamespacedDescriptions{} 1487 for k, v := range j { 1488 copy[k] = v 1489 } 1490 return copy 1491 } 1492 1493 // JobDescriptions maps Namespace to a mapping of Job.ID to JobDescription 1494 type JobDescriptions map[string]JobNamespacedDescriptions 1495 1496 // Add the Job to the JobDescriptions, creating maps as necessary 1497 func (j JobDescriptions) Add(job *Job, expected int) { 1498 if j == nil { 1499 j = make(JobDescriptions) 1500 } 1501 if j[job.Namespace] == nil { 1502 j[job.Namespace] = make(JobNamespacedDescriptions) 1503 } 1504 j[job.Namespace][job.ID] = JobDescription{ 1505 Namespace: job.Namespace, 1506 ID: job.ID, 1507 Expected: expected, 1508 } 1509 } 1510 1511 // Count the Expected instances for all JobDescriptions 1512 func (j JobDescriptions) Count() int { 1513 if j == nil { 1514 return 0 1515 } 1516 count := 0 1517 for _, jnd := range j { 1518 for _, jd := range jnd { 1519 count += jd.Expected 1520 } 1521 } 1522 return count 1523 } 1524 1525 // Delete the Job from the JobDescriptions 1526 func (j JobDescriptions) Delete(job *Job) { 1527 if j != nil && 1528 j[job.Namespace] != nil { 1529 delete(j[job.Namespace], job.ID) 1530 } 1531 } 1532 1533 type CSIPluginListStub struct { 1534 ID string 1535 Provider string 1536 ControllerRequired bool 1537 ControllersHealthy int 1538 ControllersExpected int 1539 NodesHealthy int 1540 NodesExpected int 1541 CreateIndex uint64 1542 ModifyIndex uint64 1543 } 1544 1545 func (p *CSIPlugin) Stub() *CSIPluginListStub { 1546 return &CSIPluginListStub{ 1547 ID: p.ID, 1548 Provider: p.Provider, 1549 ControllerRequired: p.ControllerRequired, 1550 ControllersHealthy: p.ControllersHealthy, 1551 ControllersExpected: p.ControllersExpected, 1552 NodesHealthy: p.NodesHealthy, 1553 NodesExpected: p.NodesExpected, 1554 CreateIndex: p.CreateIndex, 1555 ModifyIndex: p.ModifyIndex, 1556 } 1557 } 1558 1559 func (p *CSIPlugin) IsEmpty() bool { 1560 return p == nil || 1561 len(p.Controllers) == 0 && 1562 len(p.Nodes) == 0 && 1563 p.ControllerJobs.Count() == 0 && 1564 p.NodeJobs.Count() == 0 1565 } 1566 1567 type CSIPluginListRequest struct { 1568 QueryOptions 1569 } 1570 1571 type CSIPluginListResponse struct { 1572 Plugins []*CSIPluginListStub 1573 QueryMeta 1574 } 1575 1576 type CSIPluginGetRequest struct { 1577 ID string 1578 QueryOptions 1579 } 1580 1581 type CSIPluginGetResponse struct { 1582 Plugin *CSIPlugin 1583 QueryMeta 1584 } 1585 1586 type CSIPluginDeleteRequest struct { 1587 ID string 1588 QueryOptions 1589 } 1590 1591 type CSIPluginDeleteResponse struct { 1592 QueryMeta 1593 }