2019-10-22 13:20:26 +00:00
|
|
|
package structs
|
|
|
|
|
2020-01-09 18:39:18 +00:00
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
2019-10-22 13:20:26 +00:00
|
|
|
// CSISocketName is the filename that Nomad expects plugins to create inside the
|
|
|
|
// PluginMountDir.
|
|
|
|
const CSISocketName = "csi.sock"
|
|
|
|
|
|
|
|
// CSIIntermediaryDirname is the name of the directory inside the PluginMountDir
|
|
|
|
// where Nomad will expect plugins to create intermediary mounts for volumes.
|
|
|
|
const CSIIntermediaryDirname = "volumes"
|
|
|
|
|
2020-01-09 18:39:18 +00:00
|
|
|
// VolumeTypeCSI is the type in the volume stanza of a TaskGroup
|
|
|
|
const VolumeTypeCSI = "csi"
|
|
|
|
|
2019-10-22 13:20:26 +00:00
|
|
|
// CSIPluginType is an enum string that encapsulates the valid options for a
|
|
|
|
// CSIPlugin stanza's Type. These modes will allow the plugin to be used in
|
|
|
|
// different ways by the client.
|
|
|
|
type CSIPluginType string
|
|
|
|
|
|
|
|
const (
|
|
|
|
// CSIPluginTypeNode indicates that Nomad should only use the plugin for
|
|
|
|
// performing Node RPCs against the provided plugin.
|
|
|
|
CSIPluginTypeNode CSIPluginType = "node"
|
|
|
|
|
|
|
|
// CSIPluginTypeController indicates that Nomad should only use the plugin for
|
|
|
|
// performing Controller RPCs against the provided plugin.
|
|
|
|
CSIPluginTypeController CSIPluginType = "controller"
|
|
|
|
|
|
|
|
// CSIPluginTypeMonolith indicates that Nomad can use the provided plugin for
|
|
|
|
// both controller and node rpcs.
|
|
|
|
CSIPluginTypeMonolith CSIPluginType = "monolith"
|
|
|
|
)
|
|
|
|
|
|
|
|
// CSIPluginTypeIsValid validates the given CSIPluginType string and returns
|
|
|
|
// true only when a correct plugin type is specified.
|
|
|
|
func CSIPluginTypeIsValid(pt CSIPluginType) bool {
|
|
|
|
switch pt {
|
|
|
|
case CSIPluginTypeNode, CSIPluginTypeController, CSIPluginTypeMonolith:
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TaskCSIPluginConfig contains the data that is required to setup a task as a
|
|
|
|
// CSI plugin. This will be used by the csi_plugin_supervisor_hook to configure
|
|
|
|
// mounts for the plugin and initiate the connection to the plugin catalog.
|
|
|
|
type TaskCSIPluginConfig struct {
|
|
|
|
// ID is the identifier of the plugin.
|
|
|
|
// Ideally this should be the FQDN of the plugin.
|
|
|
|
ID string
|
|
|
|
|
|
|
|
// Type instructs Nomad on how to handle processing a plugin
|
|
|
|
Type CSIPluginType
|
|
|
|
|
|
|
|
// MountDir is the destination that nomad should mount in its CSI
|
|
|
|
// directory for the plugin. It will then expect a file called CSISocketName
|
|
|
|
// to be created by the plugin, and will provide references into
|
|
|
|
// "MountDir/CSIIntermediaryDirname/{VolumeName}/{AllocID} for mounts.
|
|
|
|
MountDir string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *TaskCSIPluginConfig) Copy() *TaskCSIPluginConfig {
|
|
|
|
if t == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
nt := new(TaskCSIPluginConfig)
|
|
|
|
*nt = *t
|
|
|
|
|
|
|
|
return nt
|
|
|
|
}
|
2020-01-09 18:39:18 +00:00
|
|
|
|
|
|
|
// CSIVolumeAttachmentMode chooses the type of storage api that will be used to
|
|
|
|
// interact with the device.
|
|
|
|
type CSIVolumeAttachmentMode string
|
|
|
|
|
|
|
|
const (
|
|
|
|
CSIVolumeAttachmentModeUnknown CSIVolumeAttachmentMode = ""
|
|
|
|
CSIVolumeAttachmentModeBlockDevice CSIVolumeAttachmentMode = "block-device"
|
|
|
|
CSIVolumeAttachmentModeFilesystem CSIVolumeAttachmentMode = "file-system"
|
|
|
|
)
|
|
|
|
|
|
|
|
func ValidCSIVolumeAttachmentMode(attachmentMode CSIVolumeAttachmentMode) bool {
|
|
|
|
switch attachmentMode {
|
|
|
|
case CSIVolumeAttachmentModeBlockDevice, CSIVolumeAttachmentModeFilesystem:
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// CSIVolumeAccessMode indicates how a volume should be used in a storage topology
|
|
|
|
// e.g whether the provider should make the volume available concurrently.
|
|
|
|
type CSIVolumeAccessMode string
|
|
|
|
|
|
|
|
const (
|
|
|
|
CSIVolumeAccessModeUnknown CSIVolumeAccessMode = ""
|
|
|
|
|
|
|
|
CSIVolumeAccessModeSingleNodeReader CSIVolumeAccessMode = "single-node-reader-only"
|
|
|
|
CSIVolumeAccessModeSingleNodeWriter CSIVolumeAccessMode = "single-node-writer"
|
|
|
|
|
|
|
|
CSIVolumeAccessModeMultiNodeReader CSIVolumeAccessMode = "multi-node-reader-only"
|
|
|
|
CSIVolumeAccessModeMultiNodeSingleWriter CSIVolumeAccessMode = "multi-node-single-writer"
|
|
|
|
CSIVolumeAccessModeMultiNodeMultiWriter CSIVolumeAccessMode = "multi-node-multi-writer"
|
|
|
|
)
|
|
|
|
|
|
|
|
// ValidCSIVolumeAccessMode checks to see that the provided access mode is a valid,
|
|
|
|
// non-empty access mode.
|
|
|
|
func ValidCSIVolumeAccessMode(accessMode CSIVolumeAccessMode) bool {
|
|
|
|
switch accessMode {
|
|
|
|
case CSIVolumeAccessModeSingleNodeReader, CSIVolumeAccessModeSingleNodeWriter,
|
|
|
|
CSIVolumeAccessModeMultiNodeReader, CSIVolumeAccessModeMultiNodeSingleWriter,
|
|
|
|
CSIVolumeAccessModeMultiNodeMultiWriter:
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ValidCSIVolumeAccessMode checks for a writable access mode
|
|
|
|
func ValidCSIVolumeWriteAccessMode(accessMode CSIVolumeAccessMode) bool {
|
|
|
|
switch accessMode {
|
|
|
|
case CSIVolumeAccessModeSingleNodeWriter,
|
|
|
|
CSIVolumeAccessModeMultiNodeSingleWriter,
|
|
|
|
CSIVolumeAccessModeMultiNodeMultiWriter:
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-23 17:55:26 +00:00
|
|
|
// CSIMountOptions contain optional additional configuration that can be used
|
|
|
|
// when specifying that a Volume should be used with VolumeAccessTypeMount.
|
|
|
|
type CSIMountOptions struct {
|
|
|
|
// FSType is an optional field that allows an operator to specify the type
|
|
|
|
// of the filesystem.
|
|
|
|
FSType string
|
|
|
|
|
|
|
|
// MountFlags contains additional options that may be used when mounting the
|
|
|
|
// volume by the plugin. This may contain sensitive data and should not be
|
|
|
|
// leaked.
|
|
|
|
MountFlags []string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *CSIMountOptions) Copy() *CSIMountOptions {
|
|
|
|
if o == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return &(*o)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *CSIMountOptions) Merge(p *CSIMountOptions) {
|
|
|
|
if p == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if p.FSType != "" {
|
|
|
|
o.FSType = p.FSType
|
|
|
|
}
|
|
|
|
if p.MountFlags != nil {
|
|
|
|
o.MountFlags = p.MountFlags
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// VolumeMountOptions implements the Stringer and GoStringer interfaces to prevent
|
|
|
|
// accidental leakage of sensitive mount flags via logs.
|
|
|
|
var _ fmt.Stringer = &CSIMountOptions{}
|
|
|
|
var _ fmt.GoStringer = &CSIMountOptions{}
|
|
|
|
|
|
|
|
func (v *CSIMountOptions) String() string {
|
|
|
|
mountFlagsString := "nil"
|
|
|
|
if len(v.MountFlags) != 0 {
|
|
|
|
mountFlagsString = "[REDACTED]"
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Sprintf("csi.CSIOptions(FSType: %s, MountFlags: %s)", v.FSType, mountFlagsString)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *CSIMountOptions) GoString() string {
|
|
|
|
return v.String()
|
|
|
|
}
|
|
|
|
|
2020-05-11 21:12:51 +00:00
|
|
|
// CSISecrets contain optional additional configuration that can be used
|
|
|
|
// when specifying that a Volume should be used with VolumeAccessTypeMount.
|
|
|
|
type CSISecrets map[string]string
|
|
|
|
|
|
|
|
// CSISecrets implements the Stringer and GoStringer interfaces to prevent
|
|
|
|
// accidental leakage of secrets via logs.
|
|
|
|
var _ fmt.Stringer = &CSISecrets{}
|
|
|
|
var _ fmt.GoStringer = &CSISecrets{}
|
|
|
|
|
|
|
|
func (s *CSISecrets) String() string {
|
|
|
|
redacted := map[string]string{}
|
|
|
|
for k := range *s {
|
|
|
|
redacted[k] = "[REDACTED]"
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("csi.CSISecrets(%v)", redacted)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *CSISecrets) GoString() string {
|
|
|
|
return s.String()
|
|
|
|
}
|
|
|
|
|
2020-04-23 15:06:23 +00:00
|
|
|
type CSIVolumeClaim struct {
|
|
|
|
AllocationID string
|
|
|
|
NodeID string
|
|
|
|
Mode CSIVolumeClaimMode
|
|
|
|
State CSIVolumeClaimState
|
|
|
|
}
|
|
|
|
|
|
|
|
type CSIVolumeClaimState int
|
|
|
|
|
|
|
|
const (
|
|
|
|
CSIVolumeClaimStateTaken CSIVolumeClaimState = iota
|
|
|
|
CSIVolumeClaimStateNodeDetached
|
|
|
|
CSIVolumeClaimStateControllerDetached
|
|
|
|
CSIVolumeClaimStateReadyToFree
|
|
|
|
)
|
|
|
|
|
2020-01-28 15:28:34 +00:00
|
|
|
// CSIVolume is the full representation of a CSI Volume
|
2020-01-09 18:39:18 +00:00
|
|
|
type CSIVolume struct {
|
2020-03-03 15:59:58 +00:00
|
|
|
// ID is a namespace unique URL safe identifier for the volume
|
|
|
|
ID string
|
|
|
|
// Name is a display name for the volume, not required to be unique
|
|
|
|
Name string
|
|
|
|
// ExternalID identifies the volume for the CSI interface, may be URL unsafe
|
|
|
|
ExternalID string
|
2020-01-09 18:39:18 +00:00
|
|
|
Namespace string
|
|
|
|
Topologies []*CSITopology
|
|
|
|
AccessMode CSIVolumeAccessMode
|
|
|
|
AttachmentMode CSIVolumeAttachmentMode
|
2020-03-23 17:55:26 +00:00
|
|
|
MountOptions *CSIMountOptions
|
2020-05-11 21:12:51 +00:00
|
|
|
Secrets CSISecrets
|
2020-05-15 12:16:01 +00:00
|
|
|
Parameters map[string]string
|
|
|
|
Context map[string]string
|
2020-01-09 18:39:18 +00:00
|
|
|
|
|
|
|
// Allocations, tracking claim status
|
2020-04-23 15:06:23 +00:00
|
|
|
ReadAllocs map[string]*Allocation // AllocID -> Allocation
|
|
|
|
WriteAllocs map[string]*Allocation // AllocID -> Allocation
|
|
|
|
|
|
|
|
ReadClaims map[string]*CSIVolumeClaim // AllocID -> claim
|
|
|
|
WriteClaims map[string]*CSIVolumeClaim // AllocID -> claim
|
|
|
|
PastClaims map[string]*CSIVolumeClaim // AllocID -> claim
|
2020-01-09 18:39:18 +00:00
|
|
|
|
2020-03-03 15:59:58 +00:00
|
|
|
// Schedulable is true if all the denormalized plugin health fields are true, and the
|
2020-01-09 18:39:18 +00:00
|
|
|
// volume has not been marked for garbage collection
|
2020-03-03 15:59:58 +00:00
|
|
|
Schedulable bool
|
2020-01-28 15:28:34 +00:00
|
|
|
PluginID string
|
2020-03-09 13:57:59 +00:00
|
|
|
Provider string
|
|
|
|
ProviderVersion string
|
2020-01-31 15:13:21 +00:00
|
|
|
ControllerRequired bool
|
2020-01-28 15:28:34 +00:00
|
|
|
ControllersHealthy int
|
|
|
|
ControllersExpected int
|
|
|
|
NodesHealthy int
|
|
|
|
NodesExpected int
|
|
|
|
ResourceExhausted time.Time
|
|
|
|
|
|
|
|
CreateIndex uint64
|
|
|
|
ModifyIndex uint64
|
2020-01-09 18:39:18 +00:00
|
|
|
}
|
|
|
|
|
2020-01-28 15:28:34 +00:00
|
|
|
// CSIVolListStub is partial representation of a CSI Volume for inclusion in lists
|
2020-01-09 18:39:18 +00:00
|
|
|
type CSIVolListStub struct {
|
2020-01-28 15:28:34 +00:00
|
|
|
ID string
|
|
|
|
Namespace string
|
2020-03-03 15:59:58 +00:00
|
|
|
Name string
|
|
|
|
ExternalID string
|
2020-01-28 15:28:34 +00:00
|
|
|
Topologies []*CSITopology
|
|
|
|
AccessMode CSIVolumeAccessMode
|
|
|
|
AttachmentMode CSIVolumeAttachmentMode
|
|
|
|
CurrentReaders int
|
|
|
|
CurrentWriters int
|
2020-03-03 15:59:58 +00:00
|
|
|
Schedulable bool
|
2020-01-28 15:28:34 +00:00
|
|
|
PluginID string
|
2020-03-09 13:57:59 +00:00
|
|
|
Provider string
|
2020-01-28 15:28:34 +00:00
|
|
|
ControllersHealthy int
|
|
|
|
ControllersExpected int
|
|
|
|
NodesHealthy int
|
|
|
|
NodesExpected int
|
|
|
|
CreateIndex uint64
|
|
|
|
ModifyIndex uint64
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewCSIVolume creates the volume struct. No side-effects
|
2020-03-26 13:43:59 +00:00
|
|
|
func NewCSIVolume(volumeID string, index uint64) *CSIVolume {
|
2020-01-28 15:28:34 +00:00
|
|
|
out := &CSIVolume{
|
2020-03-26 13:43:59 +00:00
|
|
|
ID: volumeID,
|
2020-02-03 16:59:00 +00:00
|
|
|
CreateIndex: index,
|
|
|
|
ModifyIndex: index,
|
2020-01-28 15:28:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
out.newStructs()
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *CSIVolume) newStructs() {
|
|
|
|
if v.Topologies == nil {
|
|
|
|
v.Topologies = []*CSITopology{}
|
2020-01-09 18:39:18 +00:00
|
|
|
}
|
2020-05-15 12:16:01 +00:00
|
|
|
if v.Context == nil {
|
|
|
|
v.Context = map[string]string{}
|
|
|
|
}
|
|
|
|
if v.Parameters == nil {
|
|
|
|
v.Parameters = map[string]string{}
|
|
|
|
}
|
2020-05-11 21:12:51 +00:00
|
|
|
if v.Secrets == nil {
|
|
|
|
v.Secrets = CSISecrets{}
|
|
|
|
}
|
2020-01-28 15:28:34 +00:00
|
|
|
|
|
|
|
v.ReadAllocs = map[string]*Allocation{}
|
|
|
|
v.WriteAllocs = map[string]*Allocation{}
|
2020-04-23 15:06:23 +00:00
|
|
|
|
|
|
|
v.ReadClaims = map[string]*CSIVolumeClaim{}
|
|
|
|
v.WriteClaims = map[string]*CSIVolumeClaim{}
|
|
|
|
v.PastClaims = map[string]*CSIVolumeClaim{}
|
2020-01-09 18:39:18 +00:00
|
|
|
}
|
|
|
|
|
2020-03-12 19:08:19 +00:00
|
|
|
func (v *CSIVolume) RemoteID() string {
|
|
|
|
if v.ExternalID != "" {
|
|
|
|
return v.ExternalID
|
|
|
|
}
|
|
|
|
return v.ID
|
|
|
|
}
|
|
|
|
|
2020-01-09 18:39:18 +00:00
|
|
|
func (v *CSIVolume) Stub() *CSIVolListStub {
|
|
|
|
stub := CSIVolListStub{
|
2020-03-24 13:42:59 +00:00
|
|
|
ID: v.ID,
|
|
|
|
Namespace: v.Namespace,
|
|
|
|
Name: v.Name,
|
|
|
|
ExternalID: v.ExternalID,
|
|
|
|
Topologies: v.Topologies,
|
|
|
|
AccessMode: v.AccessMode,
|
|
|
|
AttachmentMode: v.AttachmentMode,
|
|
|
|
CurrentReaders: len(v.ReadAllocs),
|
|
|
|
CurrentWriters: len(v.WriteAllocs),
|
|
|
|
Schedulable: v.Schedulable,
|
|
|
|
PluginID: v.PluginID,
|
|
|
|
Provider: v.Provider,
|
|
|
|
ControllersHealthy: v.ControllersHealthy,
|
|
|
|
ControllersExpected: v.ControllersExpected,
|
|
|
|
NodesHealthy: v.NodesHealthy,
|
|
|
|
NodesExpected: v.NodesExpected,
|
|
|
|
CreateIndex: v.CreateIndex,
|
|
|
|
ModifyIndex: v.ModifyIndex,
|
2020-01-09 18:39:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return &stub
|
|
|
|
}
|
|
|
|
|
2020-03-24 01:21:04 +00:00
|
|
|
func (v *CSIVolume) ReadSchedulable() bool {
|
2020-03-03 15:59:58 +00:00
|
|
|
if !v.Schedulable {
|
2020-01-09 18:39:18 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return v.ResourceExhausted == time.Time{}
|
|
|
|
}
|
|
|
|
|
2020-03-24 01:21:04 +00:00
|
|
|
// WriteSchedulable determines if the volume is schedulable for writes, considering only
|
|
|
|
// volume health
|
|
|
|
func (v *CSIVolume) WriteSchedulable() bool {
|
2020-03-03 15:59:58 +00:00
|
|
|
if !v.Schedulable {
|
2020-01-09 18:39:18 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
switch v.AccessMode {
|
2020-03-24 01:21:04 +00:00
|
|
|
case CSIVolumeAccessModeSingleNodeWriter, CSIVolumeAccessModeMultiNodeSingleWriter, CSIVolumeAccessModeMultiNodeMultiWriter:
|
2020-01-09 18:39:18 +00:00
|
|
|
return v.ResourceExhausted == time.Time{}
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-24 01:21:04 +00:00
|
|
|
// WriteFreeClaims determines if there are any free write claims available
|
|
|
|
func (v *CSIVolume) WriteFreeClaims() bool {
|
|
|
|
switch v.AccessMode {
|
|
|
|
case CSIVolumeAccessModeSingleNodeWriter, CSIVolumeAccessModeMultiNodeSingleWriter, CSIVolumeAccessModeMultiNodeMultiWriter:
|
|
|
|
return len(v.WriteAllocs) == 0
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-24 14:10:44 +00:00
|
|
|
// InUse tests whether any allocations are actively using the volume
|
|
|
|
func (v *CSIVolume) InUse() bool {
|
|
|
|
return len(v.ReadAllocs) != 0 ||
|
|
|
|
len(v.WriteAllocs) != 0
|
|
|
|
}
|
|
|
|
|
2020-01-28 15:28:34 +00:00
|
|
|
// Copy returns a copy of the volume, which shares only the Topologies slice
|
2020-02-03 16:59:00 +00:00
|
|
|
func (v *CSIVolume) Copy() *CSIVolume {
|
2020-01-28 15:28:34 +00:00
|
|
|
copy := *v
|
|
|
|
out := ©
|
|
|
|
out.newStructs()
|
2020-05-15 12:16:01 +00:00
|
|
|
for k, v := range v.Parameters {
|
|
|
|
out.Parameters[k] = v
|
|
|
|
}
|
|
|
|
for k, v := range v.Context {
|
|
|
|
out.Context[k] = v
|
|
|
|
}
|
2020-05-11 21:12:51 +00:00
|
|
|
for k, v := range v.Secrets {
|
|
|
|
out.Secrets[k] = v
|
|
|
|
}
|
2020-01-28 15:28:34 +00:00
|
|
|
|
|
|
|
for k, v := range v.ReadAllocs {
|
|
|
|
out.ReadAllocs[k] = v
|
|
|
|
}
|
|
|
|
|
|
|
|
for k, v := range v.WriteAllocs {
|
|
|
|
out.WriteAllocs[k] = v
|
|
|
|
}
|
|
|
|
|
2020-04-23 15:06:23 +00:00
|
|
|
for k, v := range v.ReadClaims {
|
|
|
|
claim := *v
|
|
|
|
out.ReadClaims[k] = &claim
|
|
|
|
}
|
|
|
|
for k, v := range v.WriteClaims {
|
|
|
|
claim := *v
|
|
|
|
out.WriteClaims[k] = &claim
|
|
|
|
}
|
|
|
|
for k, v := range v.PastClaims {
|
|
|
|
claim := *v
|
|
|
|
out.PastClaims[k] = &claim
|
|
|
|
}
|
|
|
|
|
2020-01-28 15:28:34 +00:00
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
// Claim updates the allocations and changes the volume state
|
2020-04-23 15:06:23 +00:00
|
|
|
func (v *CSIVolume) Claim(claim *CSIVolumeClaim, alloc *Allocation) error {
|
|
|
|
switch claim.Mode {
|
2020-01-09 18:39:18 +00:00
|
|
|
case CSIVolumeClaimRead:
|
2020-04-23 15:06:23 +00:00
|
|
|
return v.ClaimRead(claim, alloc)
|
2020-01-09 18:39:18 +00:00
|
|
|
case CSIVolumeClaimWrite:
|
2020-04-23 15:06:23 +00:00
|
|
|
return v.ClaimWrite(claim, alloc)
|
2020-01-09 18:39:18 +00:00
|
|
|
case CSIVolumeClaimRelease:
|
2020-04-23 15:06:23 +00:00
|
|
|
return v.ClaimRelease(claim)
|
2020-01-09 18:39:18 +00:00
|
|
|
}
|
2020-03-24 01:21:04 +00:00
|
|
|
return nil
|
2020-01-09 18:39:18 +00:00
|
|
|
}
|
|
|
|
|
2020-01-28 15:28:34 +00:00
|
|
|
// ClaimRead marks an allocation as using a volume read-only
|
2020-04-23 15:06:23 +00:00
|
|
|
func (v *CSIVolume) ClaimRead(claim *CSIVolumeClaim, alloc *Allocation) error {
|
|
|
|
if _, ok := v.ReadAllocs[claim.AllocationID]; ok {
|
2020-03-24 01:21:04 +00:00
|
|
|
return nil
|
2020-03-13 13:39:24 +00:00
|
|
|
}
|
2020-04-23 15:06:23 +00:00
|
|
|
if alloc == nil {
|
|
|
|
return fmt.Errorf("allocation missing: %s", claim.AllocationID)
|
|
|
|
}
|
2020-03-13 13:39:24 +00:00
|
|
|
|
2020-03-24 01:21:04 +00:00
|
|
|
if !v.ReadSchedulable() {
|
|
|
|
return fmt.Errorf("unschedulable")
|
2020-01-09 18:39:18 +00:00
|
|
|
}
|
2020-03-24 01:21:04 +00:00
|
|
|
|
2020-01-28 15:28:34 +00:00
|
|
|
// Allocations are copy on write, so we want to keep the id but don't need the
|
|
|
|
// pointer. We'll get it from the db in denormalize.
|
2020-04-23 15:06:23 +00:00
|
|
|
v.ReadAllocs[claim.AllocationID] = nil
|
|
|
|
delete(v.WriteAllocs, claim.AllocationID)
|
|
|
|
|
|
|
|
v.ReadClaims[claim.AllocationID] = claim
|
|
|
|
delete(v.WriteClaims, claim.AllocationID)
|
|
|
|
delete(v.PastClaims, claim.AllocationID)
|
|
|
|
|
2020-03-24 01:21:04 +00:00
|
|
|
return nil
|
2020-01-09 18:39:18 +00:00
|
|
|
}
|
|
|
|
|
2020-01-28 15:28:34 +00:00
|
|
|
// ClaimWrite marks an allocation as using a volume as a writer
|
2020-04-23 15:06:23 +00:00
|
|
|
func (v *CSIVolume) ClaimWrite(claim *CSIVolumeClaim, alloc *Allocation) error {
|
|
|
|
if _, ok := v.WriteAllocs[claim.AllocationID]; ok {
|
2020-03-24 01:21:04 +00:00
|
|
|
return nil
|
2020-03-13 13:39:24 +00:00
|
|
|
}
|
2020-04-23 15:06:23 +00:00
|
|
|
if alloc == nil {
|
|
|
|
return fmt.Errorf("allocation missing: %s", claim.AllocationID)
|
|
|
|
}
|
2020-03-13 13:39:24 +00:00
|
|
|
|
2020-03-24 01:21:04 +00:00
|
|
|
if !v.WriteSchedulable() {
|
|
|
|
return fmt.Errorf("unschedulable")
|
2020-01-09 18:39:18 +00:00
|
|
|
}
|
2020-03-24 01:21:04 +00:00
|
|
|
|
|
|
|
if !v.WriteFreeClaims() {
|
|
|
|
// Check the blocking allocations to see if they belong to this job
|
|
|
|
for _, a := range v.WriteAllocs {
|
|
|
|
if a.Namespace != alloc.Namespace || a.JobID != alloc.JobID {
|
|
|
|
return fmt.Errorf("volume max claim reached")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-28 15:28:34 +00:00
|
|
|
// Allocations are copy on write, so we want to keep the id but don't need the
|
|
|
|
// pointer. We'll get it from the db in denormalize.
|
|
|
|
v.WriteAllocs[alloc.ID] = nil
|
2020-01-09 18:39:18 +00:00
|
|
|
delete(v.ReadAllocs, alloc.ID)
|
2020-04-23 15:06:23 +00:00
|
|
|
|
|
|
|
v.WriteClaims[alloc.ID] = claim
|
|
|
|
delete(v.ReadClaims, alloc.ID)
|
|
|
|
delete(v.PastClaims, alloc.ID)
|
|
|
|
|
2020-03-24 01:21:04 +00:00
|
|
|
return nil
|
2020-01-09 18:39:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-23 15:06:23 +00:00
|
|
|
// ClaimRelease is called when the allocation has terminated and
|
|
|
|
// already stopped using the volume
|
|
|
|
func (v *CSIVolume) ClaimRelease(claim *CSIVolumeClaim) error {
|
|
|
|
if claim.State == CSIVolumeClaimStateReadyToFree {
|
2020-04-30 21:11:31 +00:00
|
|
|
delete(v.ReadAllocs, claim.AllocationID)
|
|
|
|
delete(v.WriteAllocs, claim.AllocationID)
|
|
|
|
delete(v.ReadClaims, claim.AllocationID)
|
|
|
|
delete(v.WriteClaims, claim.AllocationID)
|
2020-04-23 15:06:23 +00:00
|
|
|
delete(v.PastClaims, claim.AllocationID)
|
|
|
|
} else {
|
|
|
|
v.PastClaims[claim.AllocationID] = claim
|
2020-04-21 12:32:24 +00:00
|
|
|
}
|
2020-03-24 01:21:04 +00:00
|
|
|
return nil
|
2020-01-09 18:39:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Equality by value
|
|
|
|
func (v *CSIVolume) Equal(o *CSIVolume) bool {
|
|
|
|
if v == nil || o == nil {
|
|
|
|
return v == o
|
|
|
|
}
|
|
|
|
|
|
|
|
// Omit the plugin health fields, their values are controlled by plugin jobs
|
|
|
|
if v.ID == o.ID &&
|
|
|
|
v.Namespace == o.Namespace &&
|
|
|
|
v.AccessMode == o.AccessMode &&
|
|
|
|
v.AttachmentMode == o.AttachmentMode &&
|
2020-01-28 15:28:34 +00:00
|
|
|
v.PluginID == o.PluginID {
|
2020-01-09 18:39:18 +00:00
|
|
|
// Setwise equality of topologies
|
|
|
|
var ok bool
|
|
|
|
for _, t := range v.Topologies {
|
|
|
|
ok = false
|
|
|
|
for _, u := range o.Topologies {
|
|
|
|
if t.Equal(u) {
|
|
|
|
ok = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate validates the volume struct, returning all validation errors at once
|
|
|
|
func (v *CSIVolume) Validate() error {
|
|
|
|
errs := []string{}
|
|
|
|
|
|
|
|
if v.ID == "" {
|
|
|
|
errs = append(errs, "missing volume id")
|
|
|
|
}
|
2020-01-28 15:28:34 +00:00
|
|
|
if v.PluginID == "" {
|
|
|
|
errs = append(errs, "missing plugin id")
|
2020-01-09 18:39:18 +00:00
|
|
|
}
|
|
|
|
if v.Namespace == "" {
|
|
|
|
errs = append(errs, "missing namespace")
|
|
|
|
}
|
|
|
|
if v.AccessMode == "" {
|
|
|
|
errs = append(errs, "missing access mode")
|
|
|
|
}
|
|
|
|
if v.AttachmentMode == "" {
|
|
|
|
errs = append(errs, "missing attachment mode")
|
|
|
|
}
|
|
|
|
|
2020-02-07 13:18:21 +00:00
|
|
|
// TODO: Volume Topologies are optional - We should check to see if the plugin
|
|
|
|
// the volume is being registered with requires them.
|
|
|
|
// var ok bool
|
|
|
|
// for _, t := range v.Topologies {
|
|
|
|
// if t != nil && len(t.Segments) > 0 {
|
|
|
|
// ok = true
|
|
|
|
// break
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// if !ok {
|
|
|
|
// errs = append(errs, "missing topology")
|
|
|
|
// }
|
2020-01-09 18:39:18 +00:00
|
|
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
return fmt.Errorf("validation: %s", strings.Join(errs, ", "))
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Request and response wrappers
|
|
|
|
type CSIVolumeRegisterRequest struct {
|
|
|
|
Volumes []*CSIVolume
|
|
|
|
WriteRequest
|
|
|
|
}
|
|
|
|
|
|
|
|
type CSIVolumeRegisterResponse struct {
|
|
|
|
QueryMeta
|
|
|
|
}
|
|
|
|
|
|
|
|
type CSIVolumeDeregisterRequest struct {
|
|
|
|
VolumeIDs []string
|
|
|
|
WriteRequest
|
|
|
|
}
|
|
|
|
|
|
|
|
type CSIVolumeDeregisterResponse struct {
|
|
|
|
QueryMeta
|
|
|
|
}
|
|
|
|
|
|
|
|
type CSIVolumeClaimMode int
|
|
|
|
|
|
|
|
const (
|
|
|
|
CSIVolumeClaimRead CSIVolumeClaimMode = iota
|
|
|
|
CSIVolumeClaimWrite
|
|
|
|
CSIVolumeClaimRelease
|
|
|
|
)
|
|
|
|
|
2020-04-30 13:13:00 +00:00
|
|
|
type CSIVolumeClaimBatchRequest struct {
|
|
|
|
Claims []CSIVolumeClaimRequest
|
|
|
|
}
|
|
|
|
|
2020-01-09 18:39:18 +00:00
|
|
|
type CSIVolumeClaimRequest struct {
|
2020-02-17 12:50:37 +00:00
|
|
|
VolumeID string
|
|
|
|
AllocationID string
|
2020-04-23 15:06:23 +00:00
|
|
|
NodeID string
|
2020-02-17 12:50:37 +00:00
|
|
|
Claim CSIVolumeClaimMode
|
2020-04-23 15:06:23 +00:00
|
|
|
State CSIVolumeClaimState
|
2020-01-09 18:39:18 +00:00
|
|
|
WriteRequest
|
|
|
|
}
|
|
|
|
|
2020-04-23 15:06:23 +00:00
|
|
|
func (req *CSIVolumeClaimRequest) ToClaim() *CSIVolumeClaim {
|
|
|
|
return &CSIVolumeClaim{
|
|
|
|
AllocationID: req.AllocationID,
|
|
|
|
NodeID: req.NodeID,
|
|
|
|
Mode: req.Claim,
|
|
|
|
State: req.State,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-04 13:00:00 +00:00
|
|
|
type CSIVolumeClaimResponse struct {
|
2020-02-10 21:32:40 +00:00
|
|
|
// Opaque static publish properties of the volume. SP MAY use this
|
|
|
|
// field to ensure subsequent `NodeStageVolume` or `NodePublishVolume`
|
|
|
|
// calls calls have contextual information.
|
|
|
|
// The contents of this field SHALL be opaque to nomad.
|
|
|
|
// The contents of this field SHALL NOT be mutable.
|
|
|
|
// The contents of this field SHALL be safe for the nomad to cache.
|
|
|
|
// The contents of this field SHOULD NOT contain sensitive
|
|
|
|
// information.
|
|
|
|
// The contents of this field SHOULD NOT be used for uniquely
|
|
|
|
// identifying a volume. The `volume_id` alone SHOULD be sufficient to
|
|
|
|
// identify the volume.
|
|
|
|
// This field is OPTIONAL and when present MUST be passed to
|
|
|
|
// `NodeStageVolume` or `NodePublishVolume` calls on the client
|
|
|
|
PublishContext map[string]string
|
2020-02-17 12:50:37 +00:00
|
|
|
|
|
|
|
// Volume contains the expanded CSIVolume for use on the client after a Claim
|
|
|
|
// has completed.
|
|
|
|
Volume *CSIVolume
|
|
|
|
|
2020-02-04 13:00:00 +00:00
|
|
|
QueryMeta
|
|
|
|
}
|
|
|
|
|
2020-01-09 18:39:18 +00:00
|
|
|
type CSIVolumeListRequest struct {
|
2020-01-28 15:28:34 +00:00
|
|
|
PluginID string
|
2020-03-11 16:47:14 +00:00
|
|
|
NodeID string
|
2020-01-09 18:39:18 +00:00
|
|
|
QueryOptions
|
|
|
|
}
|
|
|
|
|
|
|
|
type CSIVolumeListResponse struct {
|
|
|
|
Volumes []*CSIVolListStub
|
|
|
|
QueryMeta
|
|
|
|
}
|
|
|
|
|
|
|
|
type CSIVolumeGetRequest struct {
|
|
|
|
ID string
|
|
|
|
QueryOptions
|
|
|
|
}
|
|
|
|
|
|
|
|
type CSIVolumeGetResponse struct {
|
|
|
|
Volume *CSIVolume
|
|
|
|
QueryMeta
|
|
|
|
}
|
2020-01-28 15:28:34 +00:00
|
|
|
|
2020-02-21 19:48:16 +00:00
|
|
|
// CSIPlugin collects fingerprint info context for the plugin for clients
|
2020-01-28 15:28:34 +00:00
|
|
|
type CSIPlugin struct {
|
2020-02-21 19:48:16 +00:00
|
|
|
ID string
|
2020-03-09 13:57:59 +00:00
|
|
|
Provider string // the vendor name from CSI GetPluginInfoResponse
|
|
|
|
Version string // the vendor verson from CSI GetPluginInfoResponse
|
2020-02-21 19:48:16 +00:00
|
|
|
ControllerRequired bool
|
2020-01-28 15:28:34 +00:00
|
|
|
|
2020-02-21 19:48:16 +00:00
|
|
|
// Map Node.IDs to fingerprint results, split by type. Monolith type plugins have
|
|
|
|
// both sets of fingerprinting results.
|
|
|
|
Controllers map[string]*CSIInfo
|
|
|
|
Nodes map[string]*CSIInfo
|
2020-01-28 15:28:34 +00:00
|
|
|
|
2020-02-21 19:48:16 +00:00
|
|
|
// Allocations are populated by denormalize to show running allocations
|
|
|
|
Allocations []*AllocListStub
|
|
|
|
|
|
|
|
// Cache the count of healthy plugins
|
2020-01-28 15:28:34 +00:00
|
|
|
ControllersHealthy int
|
|
|
|
NodesHealthy int
|
|
|
|
|
|
|
|
CreateIndex uint64
|
|
|
|
ModifyIndex uint64
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewCSIPlugin creates the plugin struct. No side-effects
|
|
|
|
func NewCSIPlugin(id string, index uint64) *CSIPlugin {
|
|
|
|
out := &CSIPlugin{
|
|
|
|
ID: id,
|
|
|
|
CreateIndex: index,
|
|
|
|
ModifyIndex: index,
|
|
|
|
}
|
|
|
|
|
|
|
|
out.newStructs()
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *CSIPlugin) newStructs() {
|
|
|
|
p.Controllers = map[string]*CSIInfo{}
|
|
|
|
p.Nodes = map[string]*CSIInfo{}
|
|
|
|
}
|
|
|
|
|
2020-02-03 16:59:00 +00:00
|
|
|
func (p *CSIPlugin) Copy() *CSIPlugin {
|
2020-01-28 15:28:34 +00:00
|
|
|
copy := *p
|
|
|
|
out := ©
|
|
|
|
out.newStructs()
|
|
|
|
|
|
|
|
for k, v := range p.Controllers {
|
|
|
|
out.Controllers[k] = v
|
|
|
|
}
|
|
|
|
|
|
|
|
for k, v := range p.Nodes {
|
|
|
|
out.Nodes[k] = v
|
|
|
|
}
|
|
|
|
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddPlugin adds a single plugin running on the node. Called from state.NodeUpdate in a
|
|
|
|
// transaction
|
2020-03-26 21:07:18 +00:00
|
|
|
func (p *CSIPlugin) AddPlugin(nodeID string, info *CSIInfo) error {
|
2020-01-28 15:28:34 +00:00
|
|
|
if info.ControllerInfo != nil {
|
2020-03-10 14:22:42 +00:00
|
|
|
p.ControllerRequired = info.RequiresControllerPlugin &&
|
2020-05-05 19:39:57 +00:00
|
|
|
(info.ControllerInfo.SupportsAttachDetach ||
|
|
|
|
info.ControllerInfo.SupportsReadOnlyAttach)
|
2020-03-10 14:22:42 +00:00
|
|
|
|
2020-01-28 15:28:34 +00:00
|
|
|
prev, ok := p.Controllers[nodeID]
|
2020-03-26 21:07:18 +00:00
|
|
|
if ok {
|
|
|
|
if prev == nil {
|
|
|
|
return fmt.Errorf("plugin missing controller: %s", nodeID)
|
|
|
|
}
|
|
|
|
if prev.Healthy {
|
|
|
|
p.ControllersHealthy -= 1
|
|
|
|
}
|
2020-01-28 15:28:34 +00:00
|
|
|
}
|
2020-05-05 19:39:57 +00:00
|
|
|
|
2020-05-05 14:30:50 +00:00
|
|
|
// note: for this to work as expected, only a single
|
|
|
|
// controller for a given plugin can be on a given Nomad
|
|
|
|
// client, they also conflict on the client so this should be
|
|
|
|
// ok
|
2020-05-05 19:39:57 +00:00
|
|
|
if prev != nil || info.Healthy {
|
|
|
|
p.Controllers[nodeID] = info
|
|
|
|
}
|
2020-01-28 15:28:34 +00:00
|
|
|
if info.Healthy {
|
|
|
|
p.ControllersHealthy += 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if info.NodeInfo != nil {
|
|
|
|
prev, ok := p.Nodes[nodeID]
|
2020-03-26 21:07:18 +00:00
|
|
|
if ok {
|
|
|
|
if prev == nil {
|
|
|
|
return fmt.Errorf("plugin missing node: %s", nodeID)
|
|
|
|
}
|
|
|
|
if prev.Healthy {
|
|
|
|
p.NodesHealthy -= 1
|
|
|
|
}
|
2020-01-28 15:28:34 +00:00
|
|
|
}
|
2020-05-05 19:39:57 +00:00
|
|
|
if prev != nil || info.Healthy {
|
|
|
|
p.Nodes[nodeID] = info
|
|
|
|
}
|
2020-01-28 15:28:34 +00:00
|
|
|
if info.Healthy {
|
|
|
|
p.NodesHealthy += 1
|
|
|
|
}
|
|
|
|
}
|
2020-03-26 21:07:18 +00:00
|
|
|
|
|
|
|
return nil
|
2020-01-28 15:28:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteNode removes all plugins from the node. Called from state.DeleteNode in a
|
|
|
|
// transaction
|
2020-03-26 21:07:18 +00:00
|
|
|
func (p *CSIPlugin) DeleteNode(nodeID string) error {
|
|
|
|
return p.DeleteNodeForType(nodeID, CSIPluginTypeMonolith)
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteNodeForType deletes a client node from the list of controllers or node instance of
|
|
|
|
// a plugin. Called from deleteJobFromPlugin during job deregistration, in a transaction
|
|
|
|
func (p *CSIPlugin) DeleteNodeForType(nodeID string, pluginType CSIPluginType) error {
|
|
|
|
switch pluginType {
|
|
|
|
case CSIPluginTypeController:
|
|
|
|
prev, ok := p.Controllers[nodeID]
|
|
|
|
if ok {
|
|
|
|
if prev == nil {
|
|
|
|
return fmt.Errorf("plugin missing controller: %s", nodeID)
|
|
|
|
}
|
|
|
|
if prev.Healthy {
|
|
|
|
p.ControllersHealthy -= 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
delete(p.Controllers, nodeID)
|
|
|
|
|
|
|
|
case CSIPluginTypeNode:
|
|
|
|
prev, ok := p.Nodes[nodeID]
|
|
|
|
if ok {
|
|
|
|
if prev == nil {
|
|
|
|
return fmt.Errorf("plugin missing node: %s", nodeID)
|
|
|
|
}
|
|
|
|
if prev.Healthy {
|
|
|
|
p.NodesHealthy -= 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
delete(p.Nodes, nodeID)
|
|
|
|
|
|
|
|
case CSIPluginTypeMonolith:
|
|
|
|
p.DeleteNodeForType(nodeID, CSIPluginTypeController)
|
|
|
|
p.DeleteNodeForType(nodeID, CSIPluginTypeNode)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteAlloc removes the fingerprint info for the allocation
|
|
|
|
func (p *CSIPlugin) DeleteAlloc(allocID, nodeID string) error {
|
2020-01-28 15:28:34 +00:00
|
|
|
prev, ok := p.Controllers[nodeID]
|
2020-03-26 21:07:18 +00:00
|
|
|
if ok {
|
|
|
|
if prev == nil {
|
|
|
|
return fmt.Errorf("plugin missing controller: %s", nodeID)
|
|
|
|
}
|
|
|
|
if prev.AllocID == allocID {
|
|
|
|
if prev.Healthy {
|
|
|
|
p.ControllersHealthy -= 1
|
|
|
|
}
|
|
|
|
delete(p.Controllers, nodeID)
|
|
|
|
}
|
2020-01-28 15:28:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
prev, ok = p.Nodes[nodeID]
|
2020-03-26 21:07:18 +00:00
|
|
|
if ok {
|
|
|
|
if prev == nil {
|
|
|
|
return fmt.Errorf("plugin missing node: %s", nodeID)
|
|
|
|
}
|
|
|
|
if prev.AllocID == allocID {
|
|
|
|
if prev.Healthy {
|
|
|
|
p.NodesHealthy -= 1
|
|
|
|
}
|
|
|
|
delete(p.Nodes, nodeID)
|
|
|
|
}
|
2020-01-28 15:28:34 +00:00
|
|
|
}
|
2020-03-26 21:07:18 +00:00
|
|
|
|
|
|
|
return nil
|
2020-01-28 15:28:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type CSIPluginListStub struct {
|
|
|
|
ID string
|
2020-03-09 13:57:59 +00:00
|
|
|
Provider string
|
2020-03-03 15:59:58 +00:00
|
|
|
ControllerRequired bool
|
2020-01-28 15:28:34 +00:00
|
|
|
ControllersHealthy int
|
|
|
|
ControllersExpected int
|
|
|
|
NodesHealthy int
|
|
|
|
NodesExpected int
|
|
|
|
CreateIndex uint64
|
|
|
|
ModifyIndex uint64
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *CSIPlugin) Stub() *CSIPluginListStub {
|
|
|
|
return &CSIPluginListStub{
|
|
|
|
ID: p.ID,
|
2020-03-09 13:57:59 +00:00
|
|
|
Provider: p.Provider,
|
2020-03-03 15:59:58 +00:00
|
|
|
ControllerRequired: p.ControllerRequired,
|
2020-01-28 15:28:34 +00:00
|
|
|
ControllersHealthy: p.ControllersHealthy,
|
|
|
|
ControllersExpected: len(p.Controllers),
|
|
|
|
NodesHealthy: p.NodesHealthy,
|
|
|
|
NodesExpected: len(p.Nodes),
|
|
|
|
CreateIndex: p.CreateIndex,
|
|
|
|
ModifyIndex: p.ModifyIndex,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *CSIPlugin) IsEmpty() bool {
|
2020-02-21 19:48:16 +00:00
|
|
|
return len(p.Controllers) == 0 && len(p.Nodes) == 0
|
2020-01-28 15:28:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type CSIPluginListRequest struct {
|
|
|
|
QueryOptions
|
|
|
|
}
|
|
|
|
|
|
|
|
type CSIPluginListResponse struct {
|
|
|
|
Plugins []*CSIPluginListStub
|
|
|
|
QueryMeta
|
|
|
|
}
|
|
|
|
|
|
|
|
type CSIPluginGetRequest struct {
|
|
|
|
ID string
|
|
|
|
QueryOptions
|
|
|
|
}
|
|
|
|
|
|
|
|
type CSIPluginGetResponse struct {
|
|
|
|
Plugin *CSIPlugin
|
|
|
|
QueryMeta
|
|
|
|
}
|
2020-05-06 20:49:12 +00:00
|
|
|
|
|
|
|
type CSIPluginDeleteRequest struct {
|
|
|
|
ID string
|
|
|
|
QueryOptions
|
|
|
|
}
|
|
|
|
|
|
|
|
type CSIPluginDeleteResponse struct {
|
|
|
|
QueryMeta
|
|
|
|
}
|