github.com/ThomasObenaus/nomad@v0.11.1/nomad/structs/csi.go (about) 1 package structs 2 3 import ( 4 "fmt" 5 "strings" 6 "time" 7 ) 8 9 // CSISocketName is the filename that Nomad expects plugins to create inside the 10 // PluginMountDir. 11 const CSISocketName = "csi.sock" 12 13 // CSIIntermediaryDirname is the name of the directory inside the PluginMountDir 14 // where Nomad will expect plugins to create intermediary mounts for volumes. 15 const CSIIntermediaryDirname = "volumes" 16 17 // VolumeTypeCSI is the type in the volume stanza of a TaskGroup 18 const VolumeTypeCSI = "csi" 19 20 // CSIPluginType is an enum string that encapsulates the valid options for a 21 // CSIPlugin stanza's Type. These modes will allow the plugin to be used in 22 // different ways by the client. 23 type CSIPluginType string 24 25 const ( 26 // CSIPluginTypeNode indicates that Nomad should only use the plugin for 27 // performing Node RPCs against the provided plugin. 28 CSIPluginTypeNode CSIPluginType = "node" 29 30 // CSIPluginTypeController indicates that Nomad should only use the plugin for 31 // performing Controller RPCs against the provided plugin. 32 CSIPluginTypeController CSIPluginType = "controller" 33 34 // CSIPluginTypeMonolith indicates that Nomad can use the provided plugin for 35 // both controller and node rpcs. 36 CSIPluginTypeMonolith CSIPluginType = "monolith" 37 ) 38 39 // CSIPluginTypeIsValid validates the given CSIPluginType string and returns 40 // true only when a correct plugin type is specified. 41 func CSIPluginTypeIsValid(pt CSIPluginType) bool { 42 switch pt { 43 case CSIPluginTypeNode, CSIPluginTypeController, CSIPluginTypeMonolith: 44 return true 45 default: 46 return false 47 } 48 } 49 50 // TaskCSIPluginConfig contains the data that is required to setup a task as a 51 // CSI plugin. This will be used by the csi_plugin_supervisor_hook to configure 52 // mounts for the plugin and initiate the connection to the plugin catalog. 53 type TaskCSIPluginConfig struct { 54 // ID is the identifier of the plugin. 55 // Ideally this should be the FQDN of the plugin. 56 ID string 57 58 // Type instructs Nomad on how to handle processing a plugin 59 Type CSIPluginType 60 61 // MountDir is the destination that nomad should mount in its CSI 62 // directory for the plugin. It will then expect a file called CSISocketName 63 // to be created by the plugin, and will provide references into 64 // "MountDir/CSIIntermediaryDirname/{VolumeName}/{AllocID} for mounts. 65 MountDir string 66 } 67 68 func (t *TaskCSIPluginConfig) Copy() *TaskCSIPluginConfig { 69 if t == nil { 70 return nil 71 } 72 73 nt := new(TaskCSIPluginConfig) 74 *nt = *t 75 76 return nt 77 } 78 79 // CSIVolumeAttachmentMode chooses the type of storage api that will be used to 80 // interact with the device. 81 type CSIVolumeAttachmentMode string 82 83 const ( 84 CSIVolumeAttachmentModeUnknown CSIVolumeAttachmentMode = "" 85 CSIVolumeAttachmentModeBlockDevice CSIVolumeAttachmentMode = "block-device" 86 CSIVolumeAttachmentModeFilesystem CSIVolumeAttachmentMode = "file-system" 87 ) 88 89 func ValidCSIVolumeAttachmentMode(attachmentMode CSIVolumeAttachmentMode) bool { 90 switch attachmentMode { 91 case CSIVolumeAttachmentModeBlockDevice, CSIVolumeAttachmentModeFilesystem: 92 return true 93 default: 94 return false 95 } 96 } 97 98 // CSIVolumeAccessMode indicates how a volume should be used in a storage topology 99 // e.g whether the provider should make the volume available concurrently. 100 type CSIVolumeAccessMode string 101 102 const ( 103 CSIVolumeAccessModeUnknown CSIVolumeAccessMode = "" 104 105 CSIVolumeAccessModeSingleNodeReader CSIVolumeAccessMode = "single-node-reader-only" 106 CSIVolumeAccessModeSingleNodeWriter CSIVolumeAccessMode = "single-node-writer" 107 108 CSIVolumeAccessModeMultiNodeReader CSIVolumeAccessMode = "multi-node-reader-only" 109 CSIVolumeAccessModeMultiNodeSingleWriter CSIVolumeAccessMode = "multi-node-single-writer" 110 CSIVolumeAccessModeMultiNodeMultiWriter CSIVolumeAccessMode = "multi-node-multi-writer" 111 ) 112 113 // ValidCSIVolumeAccessMode checks to see that the provided access mode is a valid, 114 // non-empty access mode. 115 func ValidCSIVolumeAccessMode(accessMode CSIVolumeAccessMode) bool { 116 switch accessMode { 117 case CSIVolumeAccessModeSingleNodeReader, CSIVolumeAccessModeSingleNodeWriter, 118 CSIVolumeAccessModeMultiNodeReader, CSIVolumeAccessModeMultiNodeSingleWriter, 119 CSIVolumeAccessModeMultiNodeMultiWriter: 120 return true 121 default: 122 return false 123 } 124 } 125 126 // ValidCSIVolumeAccessMode checks for a writable access mode 127 func ValidCSIVolumeWriteAccessMode(accessMode CSIVolumeAccessMode) bool { 128 switch accessMode { 129 case CSIVolumeAccessModeSingleNodeWriter, 130 CSIVolumeAccessModeMultiNodeSingleWriter, 131 CSIVolumeAccessModeMultiNodeMultiWriter: 132 return true 133 default: 134 return false 135 } 136 } 137 138 // CSIMountOptions contain optional additional configuration that can be used 139 // when specifying that a Volume should be used with VolumeAccessTypeMount. 140 type CSIMountOptions struct { 141 // FSType is an optional field that allows an operator to specify the type 142 // of the filesystem. 143 FSType string 144 145 // MountFlags contains additional options that may be used when mounting the 146 // volume by the plugin. This may contain sensitive data and should not be 147 // leaked. 148 MountFlags []string 149 } 150 151 func (o *CSIMountOptions) Copy() *CSIMountOptions { 152 if o == nil { 153 return nil 154 } 155 return &(*o) 156 } 157 158 func (o *CSIMountOptions) Merge(p *CSIMountOptions) { 159 if p == nil { 160 return 161 } 162 if p.FSType != "" { 163 o.FSType = p.FSType 164 } 165 if p.MountFlags != nil { 166 o.MountFlags = p.MountFlags 167 } 168 } 169 170 // VolumeMountOptions implements the Stringer and GoStringer interfaces to prevent 171 // accidental leakage of sensitive mount flags via logs. 172 var _ fmt.Stringer = &CSIMountOptions{} 173 var _ fmt.GoStringer = &CSIMountOptions{} 174 175 func (v *CSIMountOptions) String() string { 176 mountFlagsString := "nil" 177 if len(v.MountFlags) != 0 { 178 mountFlagsString = "[REDACTED]" 179 } 180 181 return fmt.Sprintf("csi.CSIOptions(FSType: %s, MountFlags: %s)", v.FSType, mountFlagsString) 182 } 183 184 func (v *CSIMountOptions) GoString() string { 185 return v.String() 186 } 187 188 // CSIVolume is the full representation of a CSI Volume 189 type CSIVolume struct { 190 // ID is a namespace unique URL safe identifier for the volume 191 ID string 192 // Name is a display name for the volume, not required to be unique 193 Name string 194 // ExternalID identifies the volume for the CSI interface, may be URL unsafe 195 ExternalID string 196 Namespace string 197 Topologies []*CSITopology 198 AccessMode CSIVolumeAccessMode 199 AttachmentMode CSIVolumeAttachmentMode 200 MountOptions *CSIMountOptions 201 202 // Allocations, tracking claim status 203 ReadAllocs map[string]*Allocation 204 WriteAllocs map[string]*Allocation 205 206 // Schedulable is true if all the denormalized plugin health fields are true, and the 207 // volume has not been marked for garbage collection 208 Schedulable bool 209 PluginID string 210 Provider string 211 ProviderVersion string 212 ControllerRequired bool 213 ControllersHealthy int 214 ControllersExpected int 215 NodesHealthy int 216 NodesExpected int 217 ResourceExhausted time.Time 218 219 CreateIndex uint64 220 ModifyIndex uint64 221 } 222 223 // CSIVolListStub is partial representation of a CSI Volume for inclusion in lists 224 type CSIVolListStub struct { 225 ID string 226 Namespace string 227 Name string 228 ExternalID string 229 Topologies []*CSITopology 230 AccessMode CSIVolumeAccessMode 231 AttachmentMode CSIVolumeAttachmentMode 232 MountOptions *CSIMountOptions 233 CurrentReaders int 234 CurrentWriters int 235 Schedulable bool 236 PluginID string 237 Provider string 238 ControllersHealthy int 239 ControllersExpected int 240 NodesHealthy int 241 NodesExpected int 242 CreateIndex uint64 243 ModifyIndex uint64 244 } 245 246 // NewCSIVolume creates the volume struct. No side-effects 247 func NewCSIVolume(volumeID string, index uint64) *CSIVolume { 248 out := &CSIVolume{ 249 ID: volumeID, 250 CreateIndex: index, 251 ModifyIndex: index, 252 } 253 254 out.newStructs() 255 return out 256 } 257 258 func (v *CSIVolume) newStructs() { 259 if v.Topologies == nil { 260 v.Topologies = []*CSITopology{} 261 } 262 263 v.ReadAllocs = map[string]*Allocation{} 264 v.WriteAllocs = map[string]*Allocation{} 265 } 266 267 func (v *CSIVolume) RemoteID() string { 268 if v.ExternalID != "" { 269 return v.ExternalID 270 } 271 return v.ID 272 } 273 274 func (v *CSIVolume) Stub() *CSIVolListStub { 275 stub := CSIVolListStub{ 276 ID: v.ID, 277 Namespace: v.Namespace, 278 Name: v.Name, 279 ExternalID: v.ExternalID, 280 Topologies: v.Topologies, 281 AccessMode: v.AccessMode, 282 AttachmentMode: v.AttachmentMode, 283 MountOptions: v.MountOptions, 284 CurrentReaders: len(v.ReadAllocs), 285 CurrentWriters: len(v.WriteAllocs), 286 Schedulable: v.Schedulable, 287 PluginID: v.PluginID, 288 Provider: v.Provider, 289 ControllersHealthy: v.ControllersHealthy, 290 ControllersExpected: v.ControllersExpected, 291 NodesHealthy: v.NodesHealthy, 292 NodesExpected: v.NodesExpected, 293 CreateIndex: v.CreateIndex, 294 ModifyIndex: v.ModifyIndex, 295 } 296 297 return &stub 298 } 299 300 func (v *CSIVolume) ReadSchedulable() bool { 301 if !v.Schedulable { 302 return false 303 } 304 305 return v.ResourceExhausted == time.Time{} 306 } 307 308 // WriteSchedulable determines if the volume is schedulable for writes, considering only 309 // volume health 310 func (v *CSIVolume) WriteSchedulable() bool { 311 if !v.Schedulable { 312 return false 313 } 314 315 switch v.AccessMode { 316 case CSIVolumeAccessModeSingleNodeWriter, CSIVolumeAccessModeMultiNodeSingleWriter, CSIVolumeAccessModeMultiNodeMultiWriter: 317 return v.ResourceExhausted == time.Time{} 318 default: 319 return false 320 } 321 } 322 323 // WriteFreeClaims determines if there are any free write claims available 324 func (v *CSIVolume) WriteFreeClaims() bool { 325 switch v.AccessMode { 326 case CSIVolumeAccessModeSingleNodeWriter, CSIVolumeAccessModeMultiNodeSingleWriter, CSIVolumeAccessModeMultiNodeMultiWriter: 327 return len(v.WriteAllocs) == 0 328 default: 329 return false 330 } 331 } 332 333 // InUse tests whether any allocations are actively using the volume 334 func (v *CSIVolume) InUse() bool { 335 return len(v.ReadAllocs) != 0 || 336 len(v.WriteAllocs) != 0 337 } 338 339 // Copy returns a copy of the volume, which shares only the Topologies slice 340 func (v *CSIVolume) Copy() *CSIVolume { 341 copy := *v 342 out := © 343 out.newStructs() 344 345 for k, v := range v.ReadAllocs { 346 out.ReadAllocs[k] = v 347 } 348 349 for k, v := range v.WriteAllocs { 350 out.WriteAllocs[k] = v 351 } 352 353 return out 354 } 355 356 // Claim updates the allocations and changes the volume state 357 func (v *CSIVolume) Claim(claim CSIVolumeClaimMode, alloc *Allocation) error { 358 switch claim { 359 case CSIVolumeClaimRead: 360 return v.ClaimRead(alloc) 361 case CSIVolumeClaimWrite: 362 return v.ClaimWrite(alloc) 363 case CSIVolumeClaimRelease: 364 return v.ClaimRelease(alloc) 365 } 366 return nil 367 } 368 369 // ClaimRead marks an allocation as using a volume read-only 370 func (v *CSIVolume) ClaimRead(alloc *Allocation) error { 371 if alloc == nil { 372 return fmt.Errorf("allocation missing") 373 } 374 if _, ok := v.ReadAllocs[alloc.ID]; ok { 375 return nil 376 } 377 378 if !v.ReadSchedulable() { 379 return fmt.Errorf("unschedulable") 380 } 381 382 // Allocations are copy on write, so we want to keep the id but don't need the 383 // pointer. We'll get it from the db in denormalize. 384 v.ReadAllocs[alloc.ID] = nil 385 delete(v.WriteAllocs, alloc.ID) 386 return nil 387 } 388 389 // ClaimWrite marks an allocation as using a volume as a writer 390 func (v *CSIVolume) ClaimWrite(alloc *Allocation) error { 391 if alloc == nil { 392 return fmt.Errorf("allocation missing") 393 } 394 if _, ok := v.WriteAllocs[alloc.ID]; ok { 395 return nil 396 } 397 398 if !v.WriteSchedulable() { 399 return fmt.Errorf("unschedulable") 400 } 401 402 if !v.WriteFreeClaims() { 403 // Check the blocking allocations to see if they belong to this job 404 for _, a := range v.WriteAllocs { 405 if a.Namespace != alloc.Namespace || a.JobID != alloc.JobID { 406 return fmt.Errorf("volume max claim reached") 407 } 408 } 409 } 410 411 // Allocations are copy on write, so we want to keep the id but don't need the 412 // pointer. We'll get it from the db in denormalize. 413 v.WriteAllocs[alloc.ID] = nil 414 delete(v.ReadAllocs, alloc.ID) 415 return nil 416 } 417 418 // ClaimRelease is called when the allocation has terminated and already stopped using the volume 419 func (v *CSIVolume) ClaimRelease(alloc *Allocation) error { 420 if alloc == nil { 421 return fmt.Errorf("allocation missing") 422 } 423 delete(v.ReadAllocs, alloc.ID) 424 delete(v.WriteAllocs, alloc.ID) 425 return nil 426 } 427 428 // Equality by value 429 func (v *CSIVolume) Equal(o *CSIVolume) bool { 430 if v == nil || o == nil { 431 return v == o 432 } 433 434 // Omit the plugin health fields, their values are controlled by plugin jobs 435 if v.ID == o.ID && 436 v.Namespace == o.Namespace && 437 v.AccessMode == o.AccessMode && 438 v.AttachmentMode == o.AttachmentMode && 439 v.PluginID == o.PluginID { 440 // Setwise equality of topologies 441 var ok bool 442 for _, t := range v.Topologies { 443 ok = false 444 for _, u := range o.Topologies { 445 if t.Equal(u) { 446 ok = true 447 break 448 } 449 } 450 if !ok { 451 return false 452 } 453 } 454 return true 455 } 456 return false 457 } 458 459 // Validate validates the volume struct, returning all validation errors at once 460 func (v *CSIVolume) Validate() error { 461 errs := []string{} 462 463 if v.ID == "" { 464 errs = append(errs, "missing volume id") 465 } 466 if v.PluginID == "" { 467 errs = append(errs, "missing plugin id") 468 } 469 if v.Namespace == "" { 470 errs = append(errs, "missing namespace") 471 } 472 if v.AccessMode == "" { 473 errs = append(errs, "missing access mode") 474 } 475 if v.AttachmentMode == "" { 476 errs = append(errs, "missing attachment mode") 477 } 478 479 // TODO: Volume Topologies are optional - We should check to see if the plugin 480 // the volume is being registered with requires them. 481 // var ok bool 482 // for _, t := range v.Topologies { 483 // if t != nil && len(t.Segments) > 0 { 484 // ok = true 485 // break 486 // } 487 // } 488 // if !ok { 489 // errs = append(errs, "missing topology") 490 // } 491 492 if len(errs) > 0 { 493 return fmt.Errorf("validation: %s", strings.Join(errs, ", ")) 494 } 495 return nil 496 } 497 498 // Request and response wrappers 499 type CSIVolumeRegisterRequest struct { 500 Volumes []*CSIVolume 501 WriteRequest 502 } 503 504 type CSIVolumeRegisterResponse struct { 505 QueryMeta 506 } 507 508 type CSIVolumeDeregisterRequest struct { 509 VolumeIDs []string 510 WriteRequest 511 } 512 513 type CSIVolumeDeregisterResponse struct { 514 QueryMeta 515 } 516 517 type CSIVolumeClaimMode int 518 519 const ( 520 CSIVolumeClaimRead CSIVolumeClaimMode = iota 521 CSIVolumeClaimWrite 522 CSIVolumeClaimRelease 523 ) 524 525 type CSIVolumeClaimRequest struct { 526 VolumeID string 527 AllocationID string 528 Claim CSIVolumeClaimMode 529 WriteRequest 530 } 531 532 type CSIVolumeClaimResponse struct { 533 // Opaque static publish properties of the volume. SP MAY use this 534 // field to ensure subsequent `NodeStageVolume` or `NodePublishVolume` 535 // calls calls have contextual information. 536 // The contents of this field SHALL be opaque to nomad. 537 // The contents of this field SHALL NOT be mutable. 538 // The contents of this field SHALL be safe for the nomad to cache. 539 // The contents of this field SHOULD NOT contain sensitive 540 // information. 541 // The contents of this field SHOULD NOT be used for uniquely 542 // identifying a volume. The `volume_id` alone SHOULD be sufficient to 543 // identify the volume. 544 // This field is OPTIONAL and when present MUST be passed to 545 // `NodeStageVolume` or `NodePublishVolume` calls on the client 546 PublishContext map[string]string 547 548 // Volume contains the expanded CSIVolume for use on the client after a Claim 549 // has completed. 550 Volume *CSIVolume 551 552 QueryMeta 553 } 554 555 type CSIVolumeListRequest struct { 556 PluginID string 557 NodeID string 558 QueryOptions 559 } 560 561 type CSIVolumeListResponse struct { 562 Volumes []*CSIVolListStub 563 QueryMeta 564 } 565 566 type CSIVolumeGetRequest struct { 567 ID string 568 QueryOptions 569 } 570 571 type CSIVolumeGetResponse struct { 572 Volume *CSIVolume 573 QueryMeta 574 } 575 576 // CSIPlugin collects fingerprint info context for the plugin for clients 577 type CSIPlugin struct { 578 ID string 579 Provider string // the vendor name from CSI GetPluginInfoResponse 580 Version string // the vendor verson from CSI GetPluginInfoResponse 581 ControllerRequired bool 582 583 // Map Node.IDs to fingerprint results, split by type. Monolith type plugins have 584 // both sets of fingerprinting results. 585 Controllers map[string]*CSIInfo 586 Nodes map[string]*CSIInfo 587 588 // Allocations are populated by denormalize to show running allocations 589 Allocations []*AllocListStub 590 591 // Cache the count of healthy plugins 592 ControllersHealthy int 593 NodesHealthy int 594 595 CreateIndex uint64 596 ModifyIndex uint64 597 } 598 599 // NewCSIPlugin creates the plugin struct. No side-effects 600 func NewCSIPlugin(id string, index uint64) *CSIPlugin { 601 out := &CSIPlugin{ 602 ID: id, 603 CreateIndex: index, 604 ModifyIndex: index, 605 } 606 607 out.newStructs() 608 return out 609 } 610 611 func (p *CSIPlugin) newStructs() { 612 p.Controllers = map[string]*CSIInfo{} 613 p.Nodes = map[string]*CSIInfo{} 614 } 615 616 func (p *CSIPlugin) Copy() *CSIPlugin { 617 copy := *p 618 out := © 619 out.newStructs() 620 621 for k, v := range p.Controllers { 622 out.Controllers[k] = v 623 } 624 625 for k, v := range p.Nodes { 626 out.Nodes[k] = v 627 } 628 629 return out 630 } 631 632 // AddPlugin adds a single plugin running on the node. Called from state.NodeUpdate in a 633 // transaction 634 func (p *CSIPlugin) AddPlugin(nodeID string, info *CSIInfo) error { 635 if info.ControllerInfo != nil { 636 p.ControllerRequired = info.RequiresControllerPlugin && 637 info.ControllerInfo.SupportsAttachDetach 638 639 prev, ok := p.Controllers[nodeID] 640 if ok { 641 if prev == nil { 642 return fmt.Errorf("plugin missing controller: %s", nodeID) 643 } 644 if prev.Healthy { 645 p.ControllersHealthy -= 1 646 } 647 } 648 p.Controllers[nodeID] = info 649 if info.Healthy { 650 p.ControllersHealthy += 1 651 } 652 } 653 654 if info.NodeInfo != nil { 655 prev, ok := p.Nodes[nodeID] 656 if ok { 657 if prev == nil { 658 return fmt.Errorf("plugin missing node: %s", nodeID) 659 } 660 if prev.Healthy { 661 p.NodesHealthy -= 1 662 } 663 } 664 p.Nodes[nodeID] = info 665 if info.Healthy { 666 p.NodesHealthy += 1 667 } 668 } 669 670 return nil 671 } 672 673 // DeleteNode removes all plugins from the node. Called from state.DeleteNode in a 674 // transaction 675 func (p *CSIPlugin) DeleteNode(nodeID string) error { 676 return p.DeleteNodeForType(nodeID, CSIPluginTypeMonolith) 677 } 678 679 // DeleteNodeForType deletes a client node from the list of controllers or node instance of 680 // a plugin. Called from deleteJobFromPlugin during job deregistration, in a transaction 681 func (p *CSIPlugin) DeleteNodeForType(nodeID string, pluginType CSIPluginType) error { 682 switch pluginType { 683 case CSIPluginTypeController: 684 prev, ok := p.Controllers[nodeID] 685 if ok { 686 if prev == nil { 687 return fmt.Errorf("plugin missing controller: %s", nodeID) 688 } 689 if prev.Healthy { 690 p.ControllersHealthy -= 1 691 } 692 } 693 delete(p.Controllers, nodeID) 694 695 case CSIPluginTypeNode: 696 prev, ok := p.Nodes[nodeID] 697 if ok { 698 if prev == nil { 699 return fmt.Errorf("plugin missing node: %s", nodeID) 700 } 701 if prev.Healthy { 702 p.NodesHealthy -= 1 703 } 704 } 705 delete(p.Nodes, nodeID) 706 707 case CSIPluginTypeMonolith: 708 p.DeleteNodeForType(nodeID, CSIPluginTypeController) 709 p.DeleteNodeForType(nodeID, CSIPluginTypeNode) 710 } 711 712 return nil 713 } 714 715 // DeleteAlloc removes the fingerprint info for the allocation 716 func (p *CSIPlugin) DeleteAlloc(allocID, nodeID string) error { 717 prev, ok := p.Controllers[nodeID] 718 if ok { 719 if prev == nil { 720 return fmt.Errorf("plugin missing controller: %s", nodeID) 721 } 722 if prev.AllocID == allocID { 723 if prev.Healthy { 724 p.ControllersHealthy -= 1 725 } 726 delete(p.Controllers, nodeID) 727 } 728 } 729 730 prev, ok = p.Nodes[nodeID] 731 if ok { 732 if prev == nil { 733 return fmt.Errorf("plugin missing node: %s", nodeID) 734 } 735 if prev.AllocID == allocID { 736 if prev.Healthy { 737 p.NodesHealthy -= 1 738 } 739 delete(p.Nodes, nodeID) 740 } 741 } 742 743 return nil 744 } 745 746 type CSIPluginListStub struct { 747 ID string 748 Provider string 749 ControllerRequired bool 750 ControllersHealthy int 751 ControllersExpected int 752 NodesHealthy int 753 NodesExpected int 754 CreateIndex uint64 755 ModifyIndex uint64 756 } 757 758 func (p *CSIPlugin) Stub() *CSIPluginListStub { 759 return &CSIPluginListStub{ 760 ID: p.ID, 761 Provider: p.Provider, 762 ControllerRequired: p.ControllerRequired, 763 ControllersHealthy: p.ControllersHealthy, 764 ControllersExpected: len(p.Controllers), 765 NodesHealthy: p.NodesHealthy, 766 NodesExpected: len(p.Nodes), 767 CreateIndex: p.CreateIndex, 768 ModifyIndex: p.ModifyIndex, 769 } 770 } 771 772 func (p *CSIPlugin) IsEmpty() bool { 773 return len(p.Controllers) == 0 && len(p.Nodes) == 0 774 } 775 776 type CSIPluginListRequest struct { 777 QueryOptions 778 } 779 780 type CSIPluginListResponse struct { 781 Plugins []*CSIPluginListStub 782 QueryMeta 783 } 784 785 type CSIPluginGetRequest struct { 786 ID string 787 QueryOptions 788 } 789 790 type CSIPluginGetResponse struct { 791 Plugin *CSIPlugin 792 QueryMeta 793 }