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